implement Account Settings page for Marketplace (bug 735767)
This commit is contained in:
Родитель
63859c8b12
Коммит
3ce76cd13f
|
@ -172,9 +172,12 @@ class UserRegisterForm(happyforms.ModelForm, UsernameMixin, PasswordMixin):
|
|||
details here, so we'd have to rewrite most of it anyway.
|
||||
"""
|
||||
username = forms.CharField(max_length=50)
|
||||
display_name = forms.CharField(max_length=50, required=False)
|
||||
location = forms.CharField(max_length=100, required=False)
|
||||
occupation = forms.CharField(max_length=100, required=False)
|
||||
display_name = forms.CharField(label=_lazy('Display Name'), max_length=50,
|
||||
required=False)
|
||||
location = forms.CharField(label=_lazy('Location'), max_length=100,
|
||||
required=False)
|
||||
occupation = forms.CharField(label=_lazy('Occupation'), max_length=100,
|
||||
required=False)
|
||||
password = forms.CharField(max_length=255,
|
||||
min_length=PasswordMixin.min_length,
|
||||
error_messages=PasswordMixin.error_msg,
|
||||
|
@ -362,13 +365,7 @@ class UserEditForm(UserRegisterForm, PasswordMixin):
|
|||
return u
|
||||
|
||||
|
||||
class AdminUserEditForm(UserEditForm):
|
||||
admin_log = forms.CharField(required=True, label=_('Reason for change'),
|
||||
widget=forms.Textarea())
|
||||
confirmationcode = forms.CharField(required=False, max_length=255,
|
||||
label=_('Confirmation code'))
|
||||
notes = forms.CharField(required=False, widget=forms.Textarea())
|
||||
anonymize = forms.BooleanField(required=False)
|
||||
class BaseAdminUserEditForm(object):
|
||||
|
||||
def changed_fields(self):
|
||||
"""Returns changed_data ignoring these fields."""
|
||||
|
@ -392,6 +389,17 @@ class AdminUserEditForm(UserEditForm):
|
|||
' other field.'))
|
||||
return self.cleaned_data['anonymize']
|
||||
|
||||
|
||||
class AdminUserEditForm(BaseAdminUserEditForm, UserEditForm):
|
||||
"""This is the form used by admins to edit users' info."""
|
||||
admin_log = forms.CharField(required=True, label='Reason for change',
|
||||
widget=forms.Textarea(attrs={'rows': 4}))
|
||||
confirmationcode = forms.CharField(required=False, max_length=255,
|
||||
label='Confirmation code')
|
||||
notes = forms.CharField(required=False, label='Notes',
|
||||
widget=forms.Textarea(attrs={'rows': 4}))
|
||||
anonymize = forms.BooleanField(required=False)
|
||||
|
||||
def save(self, *args, **kw):
|
||||
profile = super(AdminUserEditForm, self).save(log_for_developer=False)
|
||||
if self.cleaned_data['anonymize']:
|
||||
|
|
|
@ -120,15 +120,15 @@ class individual_contact(_NOTIFICATION):
|
|||
|
||||
class app_individual_contact(individual_contact):
|
||||
app = True
|
||||
label = loc('Mozilla needs to contact me about my individual app')
|
||||
label = _('Mozilla needs to contact me about my individual app')
|
||||
|
||||
|
||||
class app_surveys(_NOTIFICATION):
|
||||
id = 13
|
||||
group = 'user'
|
||||
group = 'dev'
|
||||
short = 'surveys'
|
||||
label = loc('Mozilla may email me with relevant App Developer news and '
|
||||
'surveys')
|
||||
label = _('Mozilla wants to contact me about relevant App Developer news '
|
||||
'and surveys')
|
||||
mandatory = False
|
||||
default_checked = False
|
||||
app = True
|
||||
|
@ -137,10 +137,22 @@ class app_surveys(_NOTIFICATION):
|
|||
NOTIFICATION_GROUPS = {'dev': _('Developer'),
|
||||
'user': _('User Notifications')}
|
||||
|
||||
APP_NOTIFICATIONS = [app_reply, app_new_review, app_individual_contact,
|
||||
app_surveys]
|
||||
APP_NOTIFICATIONS_BY_ID = dict((l.id, l) for l in APP_NOTIFICATIONS)
|
||||
APP_NOTIFICATIONS_DEFAULT = [l.id for l in APP_NOTIFICATIONS]
|
||||
APP_NOTIFICATIONS_CHOICES = [(l.id, l.label) for l in APP_NOTIFICATIONS]
|
||||
APP_NOTIFICATIONS_CHOICES_NOT_DEV = [(l.id, l.label) for l in APP_NOTIFICATIONS
|
||||
if l.group != 'dev']
|
||||
|
||||
NOTIFICATIONS = [x for x in vars().values()
|
||||
if isclass(x) and issubclass(x, _NOTIFICATION)
|
||||
and x != _NOTIFICATION and not getattr(x, 'app', False)]
|
||||
NOTIFICATIONS_BY_ID = dict((l.id, l) for l in NOTIFICATIONS)
|
||||
|
||||
ALL_NOTIFICATIONS_BY_ID = dict((l.id, l) for l in
|
||||
NOTIFICATIONS + APP_NOTIFICATIONS)
|
||||
|
||||
NOTIFICATIONS_BY_SHORT = dict((l.short, l) for l in NOTIFICATIONS)
|
||||
NOTIFICATION = AttributeDict((l.__name__, l) for l in NOTIFICATIONS)
|
||||
|
||||
|
@ -149,8 +161,3 @@ NOTIFICATIONS_CHOICES = [(l.id, l.label) for l in NOTIFICATIONS]
|
|||
NOTIFICATIONS_CHOICES_NOT_DEV = [(l.id, l.label) for l in NOTIFICATIONS
|
||||
if l.group != 'dev']
|
||||
|
||||
APP_NOTIFICATIONS = [app_reply, app_new_review, app_individual_contact]
|
||||
APP_NOTIFICATIONS_DEFAULT = [l.id for l in APP_NOTIFICATIONS]
|
||||
APP_NOTIFICATIONS_CHOICES = [(l.id, l.label) for l in APP_NOTIFICATIONS]
|
||||
APP_NOTIFICATIONS_CHOICES_NOT_DEV = [(l.id, l.label) for l in APP_NOTIFICATIONS
|
||||
if l.group != 'dev']
|
||||
|
|
|
@ -229,7 +229,7 @@ class TestEdit(UserViewBase):
|
|||
r = self.client.post(self.url, self.data)
|
||||
self.assertRedirects(r, self.url, 302)
|
||||
|
||||
eq_(UserNotification.objects.count(), len(email.NOTIFICATION))
|
||||
eq_(UserNotification.objects.count(), len(email.NOTIFICATIONS))
|
||||
eq_(UserNotification.objects.filter(enabled=True).count(),
|
||||
len(filter(lambda x: x.mandatory, email.NOTIFICATIONS)))
|
||||
self.check_default_choices(choices, checked=False)
|
||||
|
@ -248,7 +248,7 @@ class TestEdit(UserViewBase):
|
|||
|
||||
mandatory = [n.id for n in email.NOTIFICATIONS if n.mandatory]
|
||||
total = len(self.data['notifications'] + mandatory)
|
||||
eq_(UserNotification.objects.count(), len(email.NOTIFICATION))
|
||||
eq_(UserNotification.objects.count(), len(email.NOTIFICATIONS))
|
||||
eq_(UserNotification.objects.filter(enabled=True).count(), total)
|
||||
|
||||
doc = pq(self.client.get(self.url, self.data).content)
|
||||
|
@ -257,31 +257,14 @@ class TestEdit(UserViewBase):
|
|||
eq_(doc('.more-none').length, len(email.NOTIFICATION_GROUPS))
|
||||
eq_(doc('.more-all').length, len(email.NOTIFICATION_GROUPS))
|
||||
|
||||
@patch.object(settings, 'APP_PREVIEW', True)
|
||||
def test_edit_app_notifications(self):
|
||||
AddonUser.objects.create(user=self.user,
|
||||
addon=Addon.objects.create(type=amo.ADDON_EXTENSION))
|
||||
self.post_notifications(email.APP_NOTIFICATIONS_CHOICES)
|
||||
|
||||
def test_edit_notifications_non_dev(self):
|
||||
self.post_notifications(email.NOTIFICATIONS_CHOICES_NOT_DEV)
|
||||
|
||||
@patch.object(settings, 'APP_PREVIEW', True)
|
||||
def test_edit_app_notifications_non_dev(self):
|
||||
self.post_notifications(email.APP_NOTIFICATIONS_CHOICES_NOT_DEV)
|
||||
|
||||
def _test_edit_notifications_non_dev_error(self):
|
||||
def test_edit_notifications_non_dev_error(self):
|
||||
self.data['notifications'] = [2, 4, 6]
|
||||
r = self.client.post(self.url, self.data)
|
||||
assert r.context['form'].errors['notifications']
|
||||
|
||||
def test_edit_notifications_non_dev_error(self):
|
||||
self._test_edit_notifications_non_dev_error()
|
||||
|
||||
@patch.object(settings, 'APP_PREVIEW', True)
|
||||
def test_edit_app_notifications_non_dev_error(self):
|
||||
self._test_edit_notifications_non_dev_error()
|
||||
|
||||
def test_collections_toggles(self):
|
||||
r = self.client.get(self.url)
|
||||
eq_(r.status_code, 200)
|
||||
|
@ -289,14 +272,6 @@ class TestEdit(UserViewBase):
|
|||
eq_(doc('#profile-misc').length, 1,
|
||||
'Collections options should be visible.')
|
||||
|
||||
@patch.object(settings, 'APP_PREVIEW', True)
|
||||
def test_apps_collections_toggles(self):
|
||||
r = self.client.get(self.url)
|
||||
eq_(r.status_code, 200)
|
||||
doc = pq(r.content)
|
||||
eq_(doc('#profile-misc').length, 0,
|
||||
'Collections options should not be visible.')
|
||||
|
||||
|
||||
class TestEditAdmin(UserViewBase):
|
||||
fixtures = ['base/users']
|
||||
|
|
|
@ -28,7 +28,7 @@ users_patterns = patterns('',
|
|||
url('^delete_photo$', views.delete_photo, name='users.delete_photo'),
|
||||
url('^edit$', views.edit, name='users.edit'),
|
||||
url('^edit(?:/(?P<user_id>\d+))?$', views.admin_edit,
|
||||
name='users.admin_edit'),
|
||||
name='users.admin_edit'),
|
||||
url('^browserid-login', views.browserid_login,
|
||||
name='users.browserid_login'),
|
||||
url('^login/modal', views.login_modal, name='users.login_modal'),
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.template import Context, loader
|
||||
from django.utils.encoding import force_unicode
|
||||
from django.utils.html import conditional_escape
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from tower import ugettext as _
|
||||
|
||||
import users.notifications as email
|
||||
|
@ -16,12 +14,17 @@ class NotificationsSelectMultiple(forms.CheckboxSelectMultiple):
|
|||
super(self.__class__, self).__init__(**kwargs)
|
||||
|
||||
def render(self, name, value, attrs=None):
|
||||
str_values = set([int(v) for v in value]) or []
|
||||
str_values = [int(v) for v in value] or []
|
||||
final_attrs = self.build_attrs(attrs, name=name)
|
||||
groups = {}
|
||||
|
||||
# Mark the mandatory fields.
|
||||
mandatory = [k for k, v in
|
||||
email.ALL_NOTIFICATIONS_BY_ID.iteritems() if v.mandatory]
|
||||
str_values = set(mandatory + str_values)
|
||||
|
||||
for idx, label in sorted(self.choices):
|
||||
notification = email.NOTIFICATIONS_BY_ID[idx]
|
||||
notification = email.ALL_NOTIFICATIONS_BY_ID[idx]
|
||||
cb_attrs = dict(final_attrs, id='%s_%s' % (attrs['id'], idx))
|
||||
notes = []
|
||||
|
||||
|
@ -29,7 +32,8 @@ class NotificationsSelectMultiple(forms.CheckboxSelectMultiple):
|
|||
cb_attrs = dict(cb_attrs, disabled=1)
|
||||
notes.append(u'<span title="required" class="req">*</span>')
|
||||
|
||||
if self.form_instance.choices_status.get(idx):
|
||||
if (hasattr(self.form_instance, 'choices_status') and
|
||||
self.form_instance.choices_status.get(idx)):
|
||||
notes.append(u'<sup class="msg">%s</sup>' % _('new'))
|
||||
|
||||
cb = forms.CheckboxInput(
|
||||
|
@ -48,7 +52,7 @@ class NotificationsSelectMultiple(forms.CheckboxSelectMultiple):
|
|||
for e, name in email.NOTIFICATION_GROUPS.items():
|
||||
if e in groups:
|
||||
context = {'title': name, 'options': groups[e]}
|
||||
output.append(loader.get_template(template_url).render(Context(context)))
|
||||
output.append(loader.get_template(template_url).render(
|
||||
Context(context)))
|
||||
|
||||
return mark_safe(u'<ol class="complex">%s</ol>' % u'\n'.join(output))
|
||||
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
@import '../mkt/lib';
|
||||
|
||||
.edit_with_prefix {
|
||||
.border-box;
|
||||
border: 1px solid @light-gray;
|
||||
background: @faint-gray;
|
||||
.border-radius(5px);
|
||||
color: @medium-gray;
|
||||
padding: 0;
|
||||
width: 400px;
|
||||
span, input {
|
||||
.border-box;
|
||||
}
|
||||
span {
|
||||
border-right: 0 none;
|
||||
display: block;
|
||||
float: left;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
max-width: 215px;
|
||||
padding: 5px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
input[type=text] {
|
||||
.border-radius(0 5px 5px 0);
|
||||
.box-shadow(none);
|
||||
border: 0;
|
||||
float: right;
|
||||
width: 170px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.error .edit_with_prefix {
|
||||
border-color: @red;
|
||||
}
|
||||
|
||||
.simple-field {
|
||||
.hint {
|
||||
color: @gray;
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
|
@ -178,10 +178,6 @@ textarea {
|
|||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
.hint {
|
||||
font-size: 11px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
ul {
|
||||
li {
|
||||
display: inline-block;
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
@import 'lib';
|
||||
|
||||
#user-notifications {
|
||||
line-height: 16px;
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
ol,
|
||||
ol > li + li {
|
||||
margin-top: 10px;
|
||||
}
|
||||
ul li {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
h3 {
|
||||
display: none;
|
||||
}
|
||||
p.note {
|
||||
font-style: italic;
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border: 5px solid @white;
|
||||
.box-shadow(1px 1px 3px #B2C8E0);
|
||||
}
|
||||
|
||||
#account-settings {
|
||||
img, .button {
|
||||
.border-box;
|
||||
}
|
||||
img {
|
||||
.max-width(1.5);
|
||||
}
|
||||
img[src*="anon_user"], .button {
|
||||
.width(1.5);
|
||||
}
|
||||
.button {
|
||||
font-size: 12px;
|
||||
padding: 0;
|
||||
}
|
||||
#profile-photo {
|
||||
img {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.errorlist, a {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.invisible-upload a {
|
||||
margin-top: 0;
|
||||
}
|
||||
a {
|
||||
font-size: 11px;
|
||||
}
|
||||
a {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.form-footer {
|
||||
.delete {
|
||||
color: @red;
|
||||
font-size: 12px;
|
||||
&:hover {
|
||||
color: darken(@red, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#account-delete {
|
||||
.notification-box, form {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
|
@ -5,9 +5,13 @@
|
|||
@btn-g-dark: darken(spin(@btn-g, 3), 8%); //hsl(88, 60%, 38%)
|
||||
@btn-g-lite: saturate(spin(@btn-g, 4), 4%); //hsl(89, 76%, 47%)
|
||||
|
||||
@btn-r: lighten(spin(@btn-g, -90), 10%);
|
||||
@btn-r-dark: darken(spin(@btn-r, 3), 8%);
|
||||
@btn-r-lite: saturate(spin(@btn-r, 4), 60%);
|
||||
|
||||
@btn-b: lighten(spin(@btn-g, 130), 10%);
|
||||
@btn-b-dark: darken(spin(@btn-b, 3), 8%);
|
||||
@btn-b-lite: saturate(spin(@btn-b, 4), 4%);
|
||||
@btn-b-lite: saturate(spin(@btn-b, 4), 60%);
|
||||
|
||||
@btn-y: lighten(spin(@btn-g, -50), 10%);
|
||||
@btn-y-dark: darken(spin(@btn-y, 3), 8%);
|
||||
|
@ -21,7 +25,7 @@
|
|||
background-color: @gray;
|
||||
color: @white;
|
||||
display: inline-block;
|
||||
font: 14px/48px @open-stack;
|
||||
font: 14px/28px @open-stack;
|
||||
font-weight: 600;
|
||||
padding: 0 15px;
|
||||
text-align: center;
|
||||
|
@ -45,6 +49,14 @@
|
|||
inset 0 12px 24px 2px @btn-g-lite);
|
||||
}
|
||||
}
|
||||
&.delete {
|
||||
.gradient-two-color(@btn-r, @btn-r-dark);
|
||||
&:hover {
|
||||
.box-shadow(0 2px 0 0 rgba(0,0,0,.1),
|
||||
inset 0 -2px 0 0 rgba(0,0,0,.2),
|
||||
inset 0 12px 24px 2px @btn-r-lite);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
margin-bottom: 16px;
|
||||
.button {
|
||||
display: block;
|
||||
line-height: 48px;
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
|
|
|
@ -7,6 +7,25 @@ button, input, select, textarea {
|
|||
vertical-align: middle;
|
||||
}
|
||||
|
||||
input {
|
||||
&[type=file] {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
&[type=checkbox],
|
||||
&[type=radio] {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.html-rtl {
|
||||
input {
|
||||
&[type=checkbox],
|
||||
&[type=radio] {
|
||||
margin: 0 0 0 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button, input {
|
||||
line-height: normal;
|
||||
}
|
||||
|
@ -98,6 +117,7 @@ label.choice {
|
|||
}
|
||||
|
||||
.errorlist {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
@ -124,3 +144,113 @@ form div[style]:first-child + p {
|
|||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
margin: 10px 0;
|
||||
.note,
|
||||
.html-support {
|
||||
color: @medium-gray;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.html-support {
|
||||
margin: 5px 0 0;
|
||||
text-align: right;
|
||||
span {
|
||||
border-bottom: 1px dotted @light-gray;
|
||||
cursor: help;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
.simple-field {
|
||||
.border-box;
|
||||
border-top: 1px solid @light-gray;
|
||||
padding: 10px 0;
|
||||
width: 100%;
|
||||
label {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.form-label,
|
||||
.form-col {
|
||||
.border-box;
|
||||
float: left;
|
||||
}
|
||||
.form-label {
|
||||
line-height: 27px;
|
||||
font-weight: bold;
|
||||
min-height: 1px;
|
||||
width: 25%;
|
||||
}
|
||||
.form-col {
|
||||
width: 75%;
|
||||
li {
|
||||
line-height: 1;
|
||||
}
|
||||
p,
|
||||
.errorlist {
|
||||
margin: 5px 0 0;
|
||||
}
|
||||
}
|
||||
p,
|
||||
input + a {
|
||||
color: @medium-gray;
|
||||
}
|
||||
p,
|
||||
input + a,
|
||||
.errorlist {
|
||||
font-size: 11px;
|
||||
line-height: 13px;
|
||||
}
|
||||
footer {
|
||||
border-top: 1px solid @light-gray;
|
||||
padding-top: 15px;
|
||||
}
|
||||
input {
|
||||
&[type=text] {
|
||||
.width(4);
|
||||
}
|
||||
+ a {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
.edit_with_prefix {
|
||||
.width(4);
|
||||
}
|
||||
}
|
||||
|
||||
.html-rtl .form-grid {
|
||||
input {
|
||||
+ a {
|
||||
margin: 0 10px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.form-footer {
|
||||
button {
|
||||
margin-right: 5px;
|
||||
}
|
||||
a {
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
&:only-child {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.html-rtl .form-footer {
|
||||
button {
|
||||
margin: 0 0 0 5px;
|
||||
}
|
||||
a {
|
||||
margin-left: 0 5px 0 0;
|
||||
&:only-child {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
}
|
||||
form {
|
||||
float: right;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -65,6 +65,11 @@ body {
|
|||
float: right;
|
||||
}
|
||||
}
|
||||
ol, ul {
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin: .5em 0 0;
|
||||
}
|
||||
|
@ -206,7 +211,7 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
#site-footer {
|
||||
border-top: 1px solid black;
|
||||
margin-top: 20px;
|
||||
padding: 16px;
|
||||
|
|
|
@ -1,21 +1,6 @@
|
|||
@import 'lib';
|
||||
|
||||
#support {
|
||||
form {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.form-footer {
|
||||
button {
|
||||
margin-right: 5px;
|
||||
}
|
||||
a {
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
&:only-child {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
p, ul {
|
||||
font-size: 15px;
|
||||
line-height: 20px;
|
||||
|
|
|
@ -34,6 +34,26 @@
|
|||
file in the upload box when the "onchange" event is fired.]
|
||||
*/
|
||||
|
||||
|
||||
// Get an object URL across browsers.
|
||||
$.fn.objectUrl = function(offset) {
|
||||
var files = $(this)[0].files,
|
||||
url = false;
|
||||
if (z.capabilities.fileAPI && files.length) {
|
||||
offset = offset || 0;
|
||||
var f = files[offset];
|
||||
if (typeof window.URL == 'object') {
|
||||
url = window.URL.createObjectURL(f);
|
||||
} else if (typeof window.webkitURL == 'object') {
|
||||
url = window.webkitURL.createObjectURL(f);
|
||||
} else if(typeof f.getAsDataURL == 'function') {
|
||||
url = f.getAsDataURL();
|
||||
}
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
|
||||
(function($) {
|
||||
var instance_id = 0,
|
||||
boundary = "BoUnDaRyStRiNg";
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
(function() {
|
||||
if (!$('#account-settings').length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avatar handling.
|
||||
var $photo = $('#profile-photo'),
|
||||
$avatar = $photo.find('.avatar'),
|
||||
$a = $('<a>', {'text': gettext('Use original'),
|
||||
'class': 'use-original',
|
||||
'href': '#'}).hide();
|
||||
|
||||
// Doing a POST on click because deleting on a GET is the worst thing ever.
|
||||
$photo.find('.delete').click(_pd(function() {
|
||||
$.post(this.getAttribute('data-posturl')).success(function() {
|
||||
// Redirect back to this page.
|
||||
window.location = window.location;
|
||||
});
|
||||
}));
|
||||
|
||||
$avatar.attr('data-original', $avatar.attr('src'));
|
||||
function use_original() {
|
||||
$photo.find('.use-original').hide();
|
||||
$photo.find('#id_photo').val('');
|
||||
$avatar.attr('src', $avatar.attr('data-original'));
|
||||
}
|
||||
$a.click(_pd(use_original));
|
||||
$avatar.after($a);
|
||||
|
||||
$('#id_photo').change(function() {
|
||||
var $this = $(this),
|
||||
$parent = $this.closest('.form-col'),
|
||||
file = $this[0].files[0],
|
||||
file_name = file.name || file.fileName;
|
||||
$parent.find('.errorlist').remove();
|
||||
if (!file_name.match(/\.(jpg|png|jpeg)$/i)) {
|
||||
$ul = $('<ul>', {'class': 'errorlist'});
|
||||
$ul.append($('<li>',
|
||||
{'text': gettext('Images must be either PNG or JPG.')}));
|
||||
$parent.append($ul);
|
||||
use_original();
|
||||
return;
|
||||
}
|
||||
var img = $this.objectUrl();
|
||||
if (img) {
|
||||
$a.show();
|
||||
$avatar.attr('src', img);
|
||||
}
|
||||
});
|
||||
})();
|
|
@ -21,9 +21,20 @@ var escape_ = function(s) {
|
|||
};
|
||||
|
||||
|
||||
// .exists()
|
||||
// This returns true if length > 0.
|
||||
$.fn.exists = function(callback, args) {
|
||||
var $this = $(this),
|
||||
len = $this.length;
|
||||
if (len && callback) {
|
||||
callback.apply(null, args);
|
||||
}
|
||||
return !!len;
|
||||
};
|
||||
|
||||
|
||||
// CSRF Tokens
|
||||
// Hijack the AJAX requests, and insert a CSRF token as a header.
|
||||
|
||||
$('html').ajaxSend(function(event, xhr, ajaxSettings) {
|
||||
var csrf, $meta;
|
||||
// Block anything that starts with 'http:', 'https:', '://' or '//'.
|
||||
|
|
|
@ -575,24 +575,3 @@ $.fn.exists = function(callback, args){
|
|||
}
|
||||
return len > 0;
|
||||
};
|
||||
|
||||
|
||||
// Get an object URL across browsers
|
||||
$.fn.objectUrl = function(offset) {
|
||||
var files = $(this)[0].files,
|
||||
url = false;
|
||||
|
||||
if (z.capabilities.fileAPI && files.length) {
|
||||
offset = offset || 0;
|
||||
var f = files[offset];
|
||||
|
||||
if (typeof window.URL == 'object') {
|
||||
url = window.URL.createObjectURL(f);
|
||||
} else if (typeof window.webkitURL == 'object') {
|
||||
url = window.webkitURL.createObjectURL(f);
|
||||
} else if(typeof f.getAsDataURL == 'function') {
|
||||
url = f.getAsDataURL();
|
||||
}
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
import os
|
||||
import re
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
|
||||
import commonware.log
|
||||
from tower import ugettext as _, ugettext_lazy as _lazy
|
||||
|
||||
import amo
|
||||
from translations.fields import TransField, TransTextarea
|
||||
from users.forms import BaseAdminUserEditForm, UserRegisterForm
|
||||
from users.models import UserNotification, UserProfile
|
||||
import users.notifications as email
|
||||
from users.tasks import resize_photo
|
||||
from users.widgets import NotificationsSelectMultiple
|
||||
|
||||
log = commonware.log.getLogger('z.users')
|
||||
admin_re = re.compile('(?=.*\d)(?=.*[a-zA-Z])')
|
||||
|
||||
|
||||
class UserEditForm(UserRegisterForm):
|
||||
photo = forms.FileField(label=_lazy(u'Profile Photo'), required=False,
|
||||
help_text=_lazy(u'PNG and JPG supported. Large images will be resized '
|
||||
'to fit 200 x 200 px.'))
|
||||
display_name = forms.CharField(label=_lazy(u'Display Name'), max_length=50,
|
||||
required=False,
|
||||
help_text=_lazy(u'This will be publicly displayed next to your '
|
||||
'ratings, collections, and other contributions.'))
|
||||
notifications = forms.MultipleChoiceField(required=False, choices=[],
|
||||
widget=NotificationsSelectMultiple,
|
||||
initial=email.APP_NOTIFICATIONS_DEFAULT)
|
||||
password = forms.CharField(required=False)
|
||||
password2 = forms.CharField(required=False)
|
||||
bio = TransField(label=_lazy(u'Bio'), required=False,
|
||||
widget=TransTextarea(attrs={'rows': 4}))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.request = kwargs.pop('request', None)
|
||||
super(UserEditForm, self).__init__(*args, **kwargs)
|
||||
|
||||
if self.instance:
|
||||
default = dict((i, n.default_checked) for i, n
|
||||
in email.APP_NOTIFICATIONS_BY_ID.items())
|
||||
user = dict((n.notification_id, n.enabled) for n
|
||||
in self.instance.notifications.all())
|
||||
default.update(user)
|
||||
|
||||
# Add choices to Notification.
|
||||
choices = email.APP_NOTIFICATIONS_CHOICES
|
||||
if not self.instance.read_dev_agreement:
|
||||
choices = email.APP_NOTIFICATIONS_CHOICES_NOT_DEV
|
||||
|
||||
self.fields['notifications'].choices = choices
|
||||
self.fields['notifications'].initial = [i for i, v in
|
||||
default.items() if v]
|
||||
self.fields['notifications'].widget.form_instance = self
|
||||
|
||||
# TODO: We should inherit from a base form not UserRegisterForm.
|
||||
if self.fields.get('recaptcha'):
|
||||
del self.fields['recaptcha']
|
||||
|
||||
class Meta:
|
||||
model = UserProfile
|
||||
fields = ('username', 'display_name', 'location', 'occupation', 'bio',
|
||||
'homepage')
|
||||
|
||||
def clean_photo(self):
|
||||
photo = self.cleaned_data.get('photo')
|
||||
if photo:
|
||||
if photo.content_type not in ('image/png', 'image/jpeg'):
|
||||
raise forms.ValidationError(
|
||||
_('Images must be either PNG or JPG.'))
|
||||
if photo.size > settings.MAX_PHOTO_UPLOAD_SIZE:
|
||||
raise forms.ValidationError(
|
||||
_('Please use images smaller than %dMB.' %
|
||||
(settings.MAX_PHOTO_UPLOAD_SIZE / 1024 / 1024 - 1)))
|
||||
return photo
|
||||
|
||||
def save(self):
|
||||
u = super(UserEditForm, self).save(commit=False)
|
||||
data = self.cleaned_data
|
||||
|
||||
photo = data['photo']
|
||||
if photo:
|
||||
u.picture_type = 'image/png'
|
||||
tmp_destination = u.picture_path + '__unconverted'
|
||||
|
||||
if not os.path.exists(u.picture_dir):
|
||||
os.makedirs(u.picture_dir)
|
||||
|
||||
fh = open(tmp_destination, 'w')
|
||||
for chunk in photo.chunks():
|
||||
fh.write(chunk)
|
||||
|
||||
fh.close()
|
||||
resize_photo.delay(tmp_destination, u.picture_path,
|
||||
set_modified_on=[u])
|
||||
|
||||
for i, n in email.APP_NOTIFICATIONS_BY_ID.iteritems():
|
||||
enabled = n.mandatory or (str(i) in data['notifications'])
|
||||
UserNotification.update_or_create(user=u, notification_id=i,
|
||||
update={'enabled': enabled})
|
||||
|
||||
log.debug(u'User (%s) updated their profile' % u)
|
||||
|
||||
u.save()
|
||||
return u
|
||||
|
||||
|
||||
class AdminUserEditForm(BaseAdminUserEditForm, UserEditForm):
|
||||
"""
|
||||
This extends from the old `AdminUserEditForm` but using our new fancy
|
||||
`UserEditForm`.
|
||||
"""
|
||||
admin_log = forms.CharField(required=True, label='Reason for change',
|
||||
widget=forms.Textarea(attrs={'rows': 4}))
|
||||
notes = forms.CharField(required=False, label='Notes',
|
||||
widget=forms.Textarea(attrs={'rows': 4}))
|
||||
anonymize = forms.BooleanField(required=False)
|
||||
|
||||
def save(self, *args, **kw):
|
||||
profile = super(UserEditForm, self).save()
|
||||
if self.cleaned_data['anonymize']:
|
||||
amo.log(amo.LOG.ADMIN_USER_ANONYMIZED, self.instance,
|
||||
self.cleaned_data['admin_log'])
|
||||
profile.anonymize() # This also logs.
|
||||
else:
|
||||
amo.log(amo.LOG.ADMIN_USER_EDITED, self.instance,
|
||||
self.cleaned_data['admin_log'], details=self.changes())
|
||||
log.info('Admin edit user: %s changed fields: %s' %
|
||||
(self.instance, self.changed_fields()))
|
||||
return profile
|
||||
|
||||
|
||||
class UserDeleteForm(forms.Form):
|
||||
confirm = forms.BooleanField()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.request = kwargs.pop('request', None)
|
||||
super(UserDeleteForm, self).__init__(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
amouser = self.request.user.get_profile()
|
||||
if amouser.is_developer:
|
||||
# This is tampering because the form isn't shown on the page if
|
||||
# the user is a developer.
|
||||
log.warning(u'[Tampering] Attempt to delete developer account (%s)'
|
||||
% self.request.user)
|
||||
raise forms.ValidationError('Developers cannot delete their '
|
||||
'accounts.')
|
|
@ -0,0 +1,54 @@
|
|||
{% extends 'mkt/base.html' %}
|
||||
{% from 'includes/forms.html' import required %}
|
||||
|
||||
{% set title = _('Delete User Account') %}
|
||||
{% block title %}{{ page_title(title) }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section id="account-delete">
|
||||
<div class="primary island">
|
||||
<h1>{{ title }}</h1>
|
||||
{% if amouser.is_developer %}
|
||||
<div class="notification-box info">
|
||||
{% trans link=url('mkt.developers.apps') %}
|
||||
You cannot delete your account if you are listed as an
|
||||
<a href="{{ link }}">author of any apps</a>. To delete your
|
||||
account, please have another person in your development group
|
||||
delete you from the list of authors for your apps. Afterwards you
|
||||
will be able to delete your account here.
|
||||
{% endtrans %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% if form %}
|
||||
<div class="notification-box error prose">
|
||||
{% trans site=settings.DOMAIN %}
|
||||
By clicking "delete" your account is going
|
||||
to be <strong>permanently removed</strong>. That means:
|
||||
<ul>
|
||||
<li>You will not be able to log into {{ site }} anymore.</li>
|
||||
<li>Your reviews and ratings will not be deleted, but they
|
||||
will no longer be associated with you.</li>
|
||||
</ul>
|
||||
{% endtrans %}
|
||||
</div>
|
||||
<form method="post" action="">
|
||||
{{ csrf() }}
|
||||
<h2>{{ _('Confirm account deletion') }}</h2>
|
||||
<p>
|
||||
<label for="{{ form.confirm.auto_id }}" class="choice">
|
||||
{{ form.confirm }}
|
||||
{{ _('I understand this step cannot be undone.') }}
|
||||
{{ required() }}
|
||||
</label>
|
||||
</p>
|
||||
<footer class="form-footer">
|
||||
<button type="submit" class="delete">
|
||||
{{ _('Delete my user account now') }}
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}{# /amouser.is_developer #}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -0,0 +1,113 @@
|
|||
{% extends 'mkt/base.html' %}
|
||||
{% from 'developers/includes/macros.html' import some_html_tip %}
|
||||
|
||||
{% set title = _('Account Settings') %}
|
||||
{% block title %}{{ mkt_page_title(title) }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section id="account-settings">
|
||||
{{ mkt_breadcrumbs(product, [(None, title)]) }}
|
||||
<h1>{{ title }}</h1>
|
||||
<form class="form-grid" enctype="multipart/form-data" method="post">
|
||||
{{ csrf() }}
|
||||
|
||||
<div id="profile-photo" class="simple-field c">
|
||||
<div class="form-label">
|
||||
<label for="id_photo">{{ _('Profile Photo') }}</label>
|
||||
</div>
|
||||
<div class="form-col">
|
||||
<div class="invisible-upload">
|
||||
<a href="#" class="button">{{ _('Choose Photo…') }}</a>
|
||||
<span class="hint">{{ form.photo.help_text }}</span>
|
||||
<input type="file" id="id_photo" name="photo">
|
||||
</div>
|
||||
<img src="{{ amouser.picture_url }}" class="avatar photo"
|
||||
{%- if not amouser.picture_type %}
|
||||
alt="{{ _('No Photo') }}"
|
||||
{%- endif %}>
|
||||
{% if amouser.picture_type %}
|
||||
<a href="#" data-posturl="{{ url('account.delete_photo') }}"
|
||||
class="delete button">{{ _('Delete current photo') }}</a>
|
||||
{% endif %}
|
||||
{{ form.photo.errors }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="simple-field c">
|
||||
<div class="form-label">
|
||||
{{ _('PersonaID Email') }}
|
||||
</div>
|
||||
<div class="form-col">
|
||||
<input type="text" disabled value="{{ amo_user.email }}">
|
||||
<a href="https://browserid.org/signin" target="_blank">
|
||||
{{ _('Change Email') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="simple-field c">
|
||||
<div class="form-label">
|
||||
{{ _('PersonaID Password') }}
|
||||
</div>
|
||||
<div class="form-col">
|
||||
<input type="text" disabled id="fake-password" value="•••••">
|
||||
<a href="https://browserid.org/signin" target="_blank">
|
||||
{{ _('Change Password') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ grid_field(form.display_name, hint=True) }}
|
||||
|
||||
<div class="simple-field c">
|
||||
<div class="form-label">
|
||||
<label for="{{ form.username.auto_id }}">
|
||||
{{ _('Username') }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-col">
|
||||
<div class="edit_with_prefix c">
|
||||
<span>{{ settings.SITE_URL }}/user/</span>{{ form.username }}
|
||||
</div>
|
||||
{{ form.username.errors }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ grid_field(form.location) }}
|
||||
{{ grid_field(form.occupation) }}
|
||||
{{ grid_field(form.homepage) }}
|
||||
{{ grid_field(form.bio, some_html=True) }}
|
||||
|
||||
<div id="user-notifications" class="simple-field c">
|
||||
<div class="form-label">
|
||||
{{ _('Email me when') }}
|
||||
</div>
|
||||
<div class="form-col">
|
||||
<div class="choice">
|
||||
{{ form.notifications }}
|
||||
<p class="note">
|
||||
{{ _('Mozilla reserves the right to contact you individually '
|
||||
'about specific concerns with your apps.') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if 'admin_log' in form.fields %}
|
||||
<h3>Administration</h3>
|
||||
{{ grid_field(form.admin_log) }}
|
||||
{{ grid_field(form.notes) }}
|
||||
{{ grid_field(form.anonymize) }}
|
||||
{% endif %}
|
||||
|
||||
<footer class="form-footer">
|
||||
<button type="submit">{{ _('Save Changes') }}</button>
|
||||
{% if 'admin_log' in form.fields %}
|
||||
<a href="{{ url('admin:users_userprofile_delete', amouser.id) }}"
|
||||
class="delete">Delete Account</a>
|
||||
{% else %}
|
||||
<a href="{{ url('account.delete') }}" class="delete">
|
||||
{{ _('Delete Account') }}</a>
|
||||
{% endif %}
|
||||
</footer>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -1,6 +1,9 @@
|
|||
from datetime import datetime, timedelta
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core import mail
|
||||
from django.core.cache import cache
|
||||
from django.forms.models import model_to_dict
|
||||
|
||||
from jingo.helpers import datetime as datetime_filter
|
||||
from nose.tools import eq_
|
||||
|
@ -10,13 +13,205 @@ import waffle
|
|||
import amo
|
||||
import amo.tests
|
||||
from amo.urlresolvers import reverse
|
||||
from addons.models import AddonPremium
|
||||
from addons.models import AddonPremium, AddonUser
|
||||
from market.models import Price
|
||||
from mkt.developers.models import ActivityLog
|
||||
from stats.models import Contribution
|
||||
from users.models import UserProfile
|
||||
from users.models import UserNotification, UserProfile
|
||||
import users.notifications as email
|
||||
from mkt.webapps.models import Installed, Webapp
|
||||
|
||||
|
||||
class TestAccountSettings(amo.tests.TestCase):
|
||||
fixtures = ['users/test_backends']
|
||||
|
||||
def setUp(self):
|
||||
self.user = self.get_user()
|
||||
self.client.login(username=self.user.email, password='foo')
|
||||
self.url = reverse('account.settings')
|
||||
self.data = {'username': 'jbalogh', 'email': 'jbalogh@mozilla.com',
|
||||
'oldpassword': 'foo', 'password': 'longenough',
|
||||
'password2': 'longenough', 'bio': 'boop'}
|
||||
self.extra_data = {'homepage': 'http://omg.org/',
|
||||
'occupation': 'bro', 'location': 'desk 42',
|
||||
'display_name': 'Fligtar Scott'}
|
||||
self.data.update(self.extra_data)
|
||||
|
||||
def get_user(self):
|
||||
return UserProfile.objects.get(username='jbalogh')
|
||||
|
||||
def test_success(self):
|
||||
r = self.client.post(self.url, self.data, follow=True)
|
||||
self.assertRedirects(r, self.url)
|
||||
doc = pq(r.content)
|
||||
|
||||
# Check that the values got updated appropriately.
|
||||
user = self.get_user()
|
||||
for field, expected in self.extra_data.iteritems():
|
||||
eq_(unicode(getattr(user, field)), expected)
|
||||
eq_(doc('#id_' + field).val(), expected)
|
||||
|
||||
def test_no_password_changes(self):
|
||||
r = self.client.post(self.url, self.data)
|
||||
eq_(self.user.userlog_set
|
||||
.filter(activity_log__action=amo.LOG.CHANGE_PASSWORD.id)
|
||||
.count(), 0)
|
||||
|
||||
def test_email_cant_change(self):
|
||||
data = {'username': 'jbalogh',
|
||||
'email': 'jbalogh.changed@mozilla.com',
|
||||
'display_name': 'DJ SurfNTurf'}
|
||||
r = self.client.post(self.url, data)
|
||||
self.assertRedirects(r, self.url)
|
||||
eq_(len(mail.outbox), 0)
|
||||
eq_(self.get_user().email, self.data['email'],
|
||||
'Email address should not have changed')
|
||||
|
||||
def test_edit_bio(self):
|
||||
eq_(self.get_user().bio, None)
|
||||
|
||||
data = {'username': 'jbalogh',
|
||||
'email': 'jbalogh.changed@mozilla.com',
|
||||
'bio': 'xxx unst unst'}
|
||||
|
||||
r = self.client.post(self.url, data, follow=True)
|
||||
self.assertRedirects(r, self.url)
|
||||
self.assertContains(r, data['bio'])
|
||||
eq_(unicode(self.get_user().bio), data['bio'])
|
||||
|
||||
data['bio'] = 'yyy unst unst'
|
||||
r = self.client.post(self.url, data, follow=True)
|
||||
self.assertRedirects(r, self.url)
|
||||
self.assertContains(r, data['bio'])
|
||||
eq_(unicode(self.get_user().bio), data['bio'])
|
||||
|
||||
def check_default_choices(self, choices, checked=[]):
|
||||
doc = pq(self.client.get(self.url).content)
|
||||
eq_(doc('input[name=notifications]:checkbox').length, len(choices))
|
||||
for id, label in choices:
|
||||
box = doc('input[name=notifications][value=%s]' % id)
|
||||
if id in checked:
|
||||
eq_(box.filter(':checked').length, 1)
|
||||
else:
|
||||
eq_(box.length, 1)
|
||||
parent = box.parent('label')
|
||||
eq_(parent.remove('.req').text(), label)
|
||||
|
||||
def post_notifications(self, choices):
|
||||
self.check_default_choices(choices=choices, checked=choices)
|
||||
|
||||
self.data['notifications'] = []
|
||||
r = self.client.post(self.url, self.data)
|
||||
self.assertRedirects(r, self.url)
|
||||
|
||||
eq_(UserNotification.objects.count(), len(email.APP_NOTIFICATIONS))
|
||||
eq_(UserNotification.objects.filter(enabled=True).count(),
|
||||
len(filter(lambda x: x.mandatory, email.APP_NOTIFICATIONS)))
|
||||
self.check_default_choices(choices=choices, checked=[])
|
||||
|
||||
def test_edit_notifications(self):
|
||||
# Make jbalogh a developer.
|
||||
self.user.update(read_dev_agreement=True)
|
||||
|
||||
self.check_default_choices(choices=email.APP_NOTIFICATIONS_CHOICES,
|
||||
checked=[email.individual_contact.id])
|
||||
|
||||
self.data['notifications'] = [email.app_individual_contact.id,
|
||||
email.app_surveys.id]
|
||||
r = self.client.post(self.url, self.data)
|
||||
self.assertRedirects(r, self.url)
|
||||
|
||||
mandatory = [n.id for n in email.APP_NOTIFICATIONS if n.mandatory]
|
||||
total = len(set(self.data['notifications'] + mandatory))
|
||||
eq_(UserNotification.objects.count(), len(email.APP_NOTIFICATIONS))
|
||||
eq_(UserNotification.objects.filter(enabled=True).count(), total)
|
||||
|
||||
doc = pq(self.client.get(self.url).content)
|
||||
eq_(doc('input[name=notifications]:checked').length, total)
|
||||
|
||||
def test_edit_all_notifications(self):
|
||||
self.user.update(read_dev_agreement=True)
|
||||
self.post_notifications(email.APP_NOTIFICATIONS_CHOICES)
|
||||
|
||||
def test_edit_non_dev_notifications(self):
|
||||
self.post_notifications(email.APP_NOTIFICATIONS_CHOICES_NOT_DEV)
|
||||
|
||||
def test_edit_non_dev_notifications_error(self):
|
||||
# jbalogh isn't a developer so he can't set developer notifications.
|
||||
self.data['notifications'] = [email.app_surveys.id]
|
||||
r = self.client.post(self.url, self.data)
|
||||
assert r.context['form'].errors['notifications']
|
||||
|
||||
|
||||
class TestAdminAccountSettings(amo.tests.TestCase):
|
||||
fixtures = ['base/users']
|
||||
|
||||
def setUp(self):
|
||||
self.client.login(username='admin@mozilla.com', password='password')
|
||||
self.regular = self.get_user()
|
||||
self.url = reverse('users.admin_edit', args=[self.regular.pk])
|
||||
|
||||
def get_data(self, **kw):
|
||||
data = model_to_dict(self.regular)
|
||||
data['admin_log'] = 'test'
|
||||
for key in ['password', 'resetcode_expires']:
|
||||
del data[key]
|
||||
data.update(kw)
|
||||
return data
|
||||
|
||||
def get_user(self):
|
||||
# Using pk so that we can still get the user after anonymize.
|
||||
return UserProfile.objects.get(pk=999)
|
||||
|
||||
def test_get(self):
|
||||
eq_(self.client.get(self.url).status_code, 200)
|
||||
|
||||
def test_forbidden(self):
|
||||
self.client.logout()
|
||||
self.client.login(username='editor@mozilla.com', password='password')
|
||||
eq_(self.client.get(self.url).status_code, 403)
|
||||
|
||||
def test_forbidden_anon(self):
|
||||
self.client.logout()
|
||||
r = self.client.get(self.url)
|
||||
self.assertLoginRedirects(r, self.url)
|
||||
|
||||
def test_anonymize(self):
|
||||
r = self.client.post(self.url, self.get_data(anonymize=True))
|
||||
self.assertRedirects(r, reverse('zadmin.index'))
|
||||
eq_(self.get_user().password, 'sha512$Anonymous$Password')
|
||||
|
||||
def test_anonymize_fails_with_other_changed_fields(self):
|
||||
# We don't let an admin change a field whilst anonymizing.
|
||||
data = self.get_data(anonymize=True, display_name='something@else.com')
|
||||
r = self.client.post(self.url, data)
|
||||
eq_(r.status_code, 200)
|
||||
eq_(self.get_user().password, self.regular.password) # Hasn't changed.
|
||||
|
||||
def test_admin_logs_edit(self):
|
||||
self.client.post(self.url, self.get_data(email='something@else.com'))
|
||||
r = ActivityLog.objects.filter(action=amo.LOG.ADMIN_USER_EDITED.id)
|
||||
eq_(r.count(), 1)
|
||||
assert self.get_data()['admin_log'] in r[0]._arguments
|
||||
|
||||
def test_admin_logs_anonymize(self):
|
||||
self.client.post(self.url, self.get_data(anonymize=True))
|
||||
r = (ActivityLog.objects
|
||||
.filter(action=amo.LOG.ADMIN_USER_ANONYMIZED.id))
|
||||
eq_(r.count(), 1)
|
||||
assert self.get_data()['admin_log'] in r[0]._arguments
|
||||
|
||||
def test_admin_no_password(self):
|
||||
data = self.get_data(password='pass1234', password2='pass1234',
|
||||
oldpassword='password')
|
||||
self.client.post(self.url, data)
|
||||
logs = ActivityLog.objects.filter
|
||||
eq_(logs(action=amo.LOG.CHANGE_PASSWORD.id).count(), 0)
|
||||
r = logs(action=amo.LOG.ADMIN_USER_EDITED.id)
|
||||
eq_(r.count(), 1)
|
||||
eq_(r[0].details['password'][0], u'****')
|
||||
|
||||
|
||||
class PurchaseBase(amo.tests.TestCase):
|
||||
fixtures = ['base/users']
|
||||
|
||||
|
|
|
@ -1,10 +1,21 @@
|
|||
from django.conf.urls.defaults import patterns, url
|
||||
|
||||
from lib.misc.urlconf_decorator import decorate
|
||||
|
||||
from amo.decorators import login_required
|
||||
from . import views
|
||||
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'purchases/$', views.purchases, name='account.purchases'),
|
||||
urlpatterns = decorate(login_required, patterns('',
|
||||
url('purchases/$', views.purchases, name='account.purchases'),
|
||||
url(r'purchases/(?P<product_id>\d+)', views.purchases,
|
||||
name='account.purchases.receipt'),
|
||||
)
|
||||
url('settings/$', views.account_settings, name='account.settings'),
|
||||
url('settings/delete$', views.delete, name='account.delete'),
|
||||
url('settings/delete_photo$', views.delete_photo,
|
||||
name='account.delete_photo'),
|
||||
|
||||
# Keeping the same URL pattern since admin pages already know about this.
|
||||
url(r'user/(?:/(?P<user_id>\d+)/)?edit$', views.admin_edit,
|
||||
name='users.admin_edit'),
|
||||
))
|
||||
|
|
|
@ -1,16 +1,29 @@
|
|||
from django import http
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template import Context, loader
|
||||
|
||||
import commonware.log
|
||||
import jingo
|
||||
from tower import ugettext_lazy as _lazy
|
||||
from tower import ugettext as _, ugettext_lazy as _lazy
|
||||
|
||||
from addons.views import BaseFilter
|
||||
import amo
|
||||
from amo.decorators import login_required
|
||||
from amo.utils import paginate
|
||||
from amo.decorators import permission_required, post_required, write
|
||||
from amo.urlresolvers import reverse
|
||||
from amo.utils import paginate, send_mail
|
||||
from stats.models import Contribution
|
||||
from translations.query import order_by_translation
|
||||
from users.forms import AdminUserEditForm
|
||||
from users.models import UserProfile
|
||||
from users.tasks import delete_photo as delete_photo_task
|
||||
from users.utils import EmailResetCode
|
||||
from users.views import logout
|
||||
from mkt.site import messages
|
||||
from mkt.webapps.models import Webapp
|
||||
from . import forms
|
||||
|
||||
log = commonware.log.getLogger('mkt.account')
|
||||
|
||||
|
||||
class PurchasesFilter(BaseFilter):
|
||||
|
@ -30,7 +43,6 @@ class PurchasesFilter(BaseFilter):
|
|||
return order_by_translation(qs, 'name')
|
||||
|
||||
|
||||
@login_required
|
||||
def purchases(request, product_id=None, template=None):
|
||||
"""A list of purchases that a user has made through the Marketplace."""
|
||||
cs = (Contribution.objects
|
||||
|
@ -67,3 +79,56 @@ def purchases(request, product_id=None, template=None):
|
|||
'listing_filter': listing,
|
||||
'contributions': contributions,
|
||||
'single': bool(product_id)})
|
||||
|
||||
|
||||
@write
|
||||
def account_settings(request):
|
||||
# Don't use `request.amo_user` because it's too cached.
|
||||
amo_user = request.amo_user.user.get_profile()
|
||||
form = forms.UserEditForm(request.POST or None, request.FILES or None,
|
||||
request=request, instance=amo_user)
|
||||
if request.method == 'POST':
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, _('Profile Updated'))
|
||||
return redirect('account.settings')
|
||||
else:
|
||||
messages.form_errors(request)
|
||||
return jingo.render(request, 'account/settings.html',
|
||||
{'form': form, 'amouser': amo_user})
|
||||
|
||||
|
||||
@write
|
||||
@permission_required('Users', 'Edit')
|
||||
def admin_edit(request, user_id):
|
||||
amouser = get_object_or_404(UserProfile, pk=user_id)
|
||||
form = forms.AdminUserEditForm(request.POST or None, request.FILES or None,
|
||||
request=request, instance=amouser)
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, _('Profile Updated'))
|
||||
return redirect('zadmin.index')
|
||||
return jingo.render(request, 'account/settings.html',
|
||||
{'form': form, 'amouser': amouser})
|
||||
|
||||
|
||||
def delete(request):
|
||||
amouser = request.amo_user
|
||||
form = forms.UserDeleteForm(request.POST, request=request)
|
||||
if request.method == 'POST' and form.is_valid():
|
||||
messages.success(request, _('Profile Deleted'))
|
||||
amouser.anonymize()
|
||||
logout(request)
|
||||
form = None
|
||||
return redirect('users.login')
|
||||
return jingo.render(request, 'account/delete.html',
|
||||
{'form': form, 'amouser': amouser})
|
||||
|
||||
|
||||
@post_required
|
||||
def delete_photo(request):
|
||||
request.amo_user.update(picture_type='')
|
||||
delete_photo_task.delay(request.amo_user.picture_path)
|
||||
log.debug(u'User (%s) deleted photo' % request.amo_user)
|
||||
messages.success(request, _('Photo Deleted'))
|
||||
return http.HttpResponse()
|
||||
|
|
|
@ -31,6 +31,7 @@ CSS = {
|
|||
|
||||
# Forms (used for tables on "Manage ..." pages).
|
||||
'css/devreg/devhub-forms.less',
|
||||
'css/common/forms.less',
|
||||
|
||||
# Landing page
|
||||
'css/devreg/landing.less',
|
||||
|
@ -69,6 +70,8 @@ CSS = {
|
|||
'css/mkt/typography.less',
|
||||
'css/mkt/site.less',
|
||||
'css/mkt/forms.less',
|
||||
'css/common/invisible-upload.less',
|
||||
'css/common/forms.less',
|
||||
'css/mkt/header.less',
|
||||
'css/mkt/breadcrumbs.less',
|
||||
'css/mkt/buttons.less',
|
||||
|
@ -80,6 +83,8 @@ CSS = {
|
|||
'css/mkt/suggestions.less',
|
||||
'css/mkt/purchases.less',
|
||||
'css/mkt/support.less',
|
||||
'css/mkt/account.less',
|
||||
'css/devreg/l10n.less',
|
||||
),
|
||||
'mkt/in-app-payments': (
|
||||
# Temporarily re-using PayPal styles for in-app-payments UI
|
||||
|
@ -208,10 +213,15 @@ JS = {
|
|||
'js/mkt/payments.js',
|
||||
'js/mkt/search.js',
|
||||
'js/mkt/apps.js',
|
||||
'js/zamboni/outgoing_links.js',
|
||||
'js/common/upload-image.js',
|
||||
|
||||
# Search suggestions.
|
||||
'js/impala/ajaxcache.js',
|
||||
'js/impala/suggestions.js',
|
||||
|
||||
# Account settings.
|
||||
'js/mkt/account.js',
|
||||
),
|
||||
'marketplace-experiments': (
|
||||
'js/marketplace-experiments/jquery-1.7.1.min.js',
|
||||
|
|
|
@ -164,7 +164,7 @@
|
|||
<tr>
|
||||
<th>
|
||||
{{ tip(_('Device Types'),
|
||||
_('Indicate support for desktop, mobile and tablet
|
||||
_('Indicate support for desktop, mobile, and tablet
|
||||
devices.')) }}
|
||||
{{ req_if_edit }}
|
||||
</th>
|
||||
|
|
|
@ -61,9 +61,11 @@ SUPPORTED_NONAPPS += (
|
|||
'privacy-policy',
|
||||
'purchases',
|
||||
'search',
|
||||
'settings',
|
||||
'submit',
|
||||
'support',
|
||||
'terms-of-use',
|
||||
'user',
|
||||
'users',
|
||||
)
|
||||
|
||||
|
|
|
@ -107,14 +107,22 @@ def price_label(product):
|
|||
@register.function
|
||||
def form_field(field, label=None, tag='div', req=None, opt=False, hint=False,
|
||||
some_html=False, cc_startswith=None, cc_maxlength=None,
|
||||
**attrs):
|
||||
grid=False, **attrs):
|
||||
c = dict(field=field, label=label, tag=tag, req=req, opt=opt, hint=hint,
|
||||
some_html=some_html, cc_startswith=cc_startswith,
|
||||
cc_maxlength=cc_maxlength, attrs=attrs)
|
||||
cc_maxlength=cc_maxlength, grid=grid, attrs=attrs)
|
||||
t = env.get_template('site/helpers/simple_field.html').render(**c)
|
||||
return jinja2.Markup(t)
|
||||
|
||||
|
||||
@register.function
|
||||
def grid_field(field, label=None, tag='div', req=None, opt=False, hint=False,
|
||||
some_html=False, cc_startswith=None, cc_maxlength=None,
|
||||
**attrs):
|
||||
return form_field(field, label, tag, req, opt, hint, some_html,
|
||||
cc_startswith, cc_maxlength, grid=True, attrs=attrs)
|
||||
|
||||
|
||||
@register.function
|
||||
def admin_site_links():
|
||||
return {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import jinja2
|
||||
from jingo import env
|
||||
from tower import ugettext as _
|
||||
|
||||
from amo.messages import debug, info, success, warning, error
|
||||
|
||||
|
@ -10,3 +11,9 @@ def _make_message(message=None, title=None, title_safe=False,
|
|||
'title_safe': title_safe, 'message_safe': message_safe}
|
||||
t = env.get_template('site/messages/content.html').render(**c)
|
||||
return jinja2.Markup(t)
|
||||
|
||||
|
||||
def form_errors(request):
|
||||
return error(request, title=_('Errors Found'),
|
||||
message=_('There were errors in the changes you made. '
|
||||
'Please correct them and resubmit.'))
|
||||
|
|
|
@ -1,18 +1,41 @@
|
|||
{% from 'includes/forms.html' import required, optional, tip %}
|
||||
{% from 'developers/includes/macros.html' import some_html_tip %}
|
||||
|
||||
{% macro simple_label(field, label, opt, req, tooltip, hint) %}
|
||||
<label class="{{ 'choice' if choice }}" for="{{ field.auto_id }}">
|
||||
{{ label or field.label }}
|
||||
</label>
|
||||
|
||||
{% if field.field.required and req != False %}{{ required() -}}{% endif %}
|
||||
{% if opt %}{{ optional() -}}{% endif %}
|
||||
|
||||
{% if not hint and tooltip %}{{ tip(None, tooltip) }}{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% if tag %}
|
||||
<{{ tag }} class="brform simple-field c {{ class }}{{ ' error' if field.errors }}">
|
||||
{% endif %}
|
||||
{% set choice = field|is_choice_field %}
|
||||
{% if choice %}{{ field.as_widget() }}{% endif %}
|
||||
<label class="{{ 'choice' if choice }}" for="{{ field.auto_id }}">
|
||||
{{ label or field.label }}
|
||||
</label>
|
||||
{% if field.field.required and req != False %}{{ required() -}}{% endif %}
|
||||
{% if opt %}{{ optional() -}}{% endif %}
|
||||
|
||||
{% if not tooltip %}{% set tooltip = field.help_text %}{% endif %}
|
||||
{% if not hint and tooltip %}{{ tip(None, tooltip) }}{% endif %}
|
||||
|
||||
{% if grid %}
|
||||
<div class="form-label">
|
||||
{% else %}
|
||||
{% if choice %}{{ field.as_widget() }}{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if not choice %}
|
||||
{{ simple_label(field, label, opt, req, tooltip, hint) }}
|
||||
{% endif %}
|
||||
|
||||
{% if grid %}
|
||||
</div>
|
||||
<div class="form-col">
|
||||
{% if choice %}
|
||||
{{ field.as_widget() }}
|
||||
{{ simple_label(field, label, opt, req, tooltip, hint) }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if not choice %}
|
||||
{{ field.as_widget(attrs=attrs) }}
|
||||
|
@ -32,6 +55,8 @@
|
|||
data-for-startswith="{{ cc_startswith }}"
|
||||
{% endif %}></div>
|
||||
{% endif %}
|
||||
|
||||
{% if grid %}</div>{% endif %}
|
||||
{% if tag %}
|
||||
</{{ tag }}>
|
||||
{% endif %}
|
||||
|
|
|
@ -37,7 +37,9 @@
|
|||
</ol>
|
||||
<form method="post">
|
||||
{{ csrf() }}
|
||||
{{ form_field(agreement_form.newsletter, opt=True) }}
|
||||
{{ form_field(agreement_form.newsletter,
|
||||
label=_('Mozilla may email me with relevant App Developer '
|
||||
'news and surveys'), opt=True) }}
|
||||
{{ agreement_form.newsletter.errors }}
|
||||
{{ agreement_form.read_dev_agreement }}
|
||||
{{ agreement_form.read_dev_agreement.errors }}
|
||||
|
|
|
@ -53,11 +53,15 @@
|
|||
</div>
|
||||
|
||||
{% block sitefooter %}
|
||||
<footer>
|
||||
<footer id="site-footer">
|
||||
{% if request.user.is_authenticated() %}
|
||||
signed in as {{ request.amo_user.email }}
|
||||
<a href="{{ url('users.logout') }}">(log out)</a>
|
||||
<p><a href="{{ url('account.purchases') }}">My Purchases</a></p>
|
||||
<p>
|
||||
<a href="{{ url('account.purchases') }}">My Purchases</a>
|
||||
·
|
||||
<a href="{{ url('account.settings') }}">Account Settings</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
</footer>
|
||||
{% endblock %}
|
||||
|
|
|
@ -37,6 +37,9 @@ urlpatterns = patterns('',
|
|||
# Support (e.g., refunds, FAQs).
|
||||
('^support/', include('mkt.support.urls')),
|
||||
|
||||
# Users (Legacy).
|
||||
('', include('users.urls')),
|
||||
|
||||
# Account info (e.g., purchases, settings).
|
||||
('', include('mkt.account.urls')),
|
||||
|
||||
|
@ -52,9 +55,6 @@ urlpatterns = patterns('',
|
|||
# Services.
|
||||
('', include('apps.amo.urls')),
|
||||
|
||||
# Users.
|
||||
('', include('users.urls')),
|
||||
|
||||
# Javascript translations.
|
||||
url('^jsi18n.js$', cache_page(60 * 60 * 24 * 365)(javascript_catalog),
|
||||
{'domain': 'javascript', 'packages': ['zamboni']}, name='jsi18n'),
|
||||
|
|
Загрузка…
Ссылка в новой задаче