Skip to content

Commit 306f35a

Browse files
author
Andy McKay
committed
admin user edit page (bug 672821)
1 parent 68462af commit 306f35a

File tree

15 files changed

+289
-20
lines changed

15 files changed

+289
-20
lines changed

apps/amo/decorators.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,19 @@ def wrapper(request, *args, **kw):
5050
return wrapper
5151

5252

53+
def permission_required(app, action):
54+
def decorator(f):
55+
@functools.wraps(f)
56+
def wrapper(request, *args, **kw):
57+
from access import acl
58+
if acl.action_allowed(request, app, action):
59+
return f(request, *args, **kw)
60+
else:
61+
return http.HttpResponseForbidden()
62+
return wrapper
63+
return decorator
64+
65+
5366
def json_view(f):
5467
@functools.wraps(f)
5568
def wrapper(*args, **kw):
@@ -93,6 +106,7 @@ def set_modified_on(f):
93106
Looks up objects defined in the set_modified_on kwarg.
94107
"""
95108
from amo.tasks import set_modified_on_object
109+
96110
@functools.wraps(f)
97111
def wrapper(*args, **kw):
98112
objs = kw.pop('set_modified_on', None)

apps/amo/tests/test_decorators.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def test_decorator_syntax(self):
8282
def test_no_redirect_success(self):
8383
func = decorators.login_required(redirect=False)(self.f)
8484
self.request.user.is_authenticated.return_value = True
85-
response = func(self.request)
85+
func(self.request)
8686
assert self.f.called
8787

8888

@@ -106,3 +106,30 @@ def test_not_set_modified_on(self):
106106
for user in users:
107107
date = UserProfile.objects.get(pk=user.pk).modified.date()
108108
assert date < datetime.today().date()
109+
110+
111+
class TestPermissionRequired(test_utils.TestCase):
112+
113+
def setUp(self):
114+
self.f = mock.Mock()
115+
self.f.__name__ = 'function'
116+
self.request = mock.Mock()
117+
118+
@mock.patch('access.acl.action_allowed')
119+
def test_permission_not_allowed(self, action_allowed):
120+
action_allowed.return_value = False
121+
func = decorators.permission_required('', '')(self.f)
122+
eq_(func(self.request).status_code, 403)
123+
124+
@mock.patch('access.acl.action_allowed')
125+
def test_permission_allowed(self, action_allowed):
126+
action_allowed.return_value = True
127+
func = decorators.permission_required('', '')(self.f)
128+
func(self.request)
129+
assert self.f.called
130+
131+
@mock.patch('access.acl.action_allowed')
132+
def test_permission_allowed_correctly(self, action_allowed):
133+
func = decorators.permission_required('Admin', '%')(self.f)
134+
func(self.request)
135+
action_allowed.assert_called_with(self.request, 'Admin', '%')

apps/users/admin.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
class UserAdmin(admin.ModelAdmin):
1212
list_display = ('__unicode__', 'email')
1313
search_fields = ('^email',)
14+
# A custom field used in search json in zadmin, not django.admin.
15+
search_fields_response = 'email'
1416
inlines = (GroupUserInline,)
1517

1618
# XXX TODO: Ability to edit the picture

apps/users/forms.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,30 @@ def save(self):
325325
return u
326326

327327

328+
class AdminUserEditForm(UserEditForm):
329+
admin_log = forms.CharField(required=True, label=_('Reason for change'),
330+
widget=forms.Textarea())
331+
confirmationcode = forms.CharField(required=False, max_length=255,
332+
label=_('Confirmation code'))
333+
notes = forms.CharField(required=False, widget=forms.Textarea())
334+
anonymize = forms.BooleanField(required=False)
335+
336+
def clean_anonymize(self):
337+
if (self.cleaned_data['anonymize'] and
338+
set(self.changed_data) !=
339+
set(['admin_log', 'notifications', 'anonymize'])):
340+
raise forms.ValidationError(_('To anonymize, enter an admin log, '
341+
'but do not change any other field'))
342+
return self.cleaned_data['anonymize']
343+
344+
def save(self, *args, **kw):
345+
profile = super(AdminUserEditForm, self).save()
346+
if self.cleaned_data['anonymize']:
347+
profile.anonymize() # this also logs.
348+
349+
return profile
350+
351+
328352
class BlacklistedUsernameAddForm(forms.Form):
329353
"""Form for adding blacklisted username in bulk fashion."""
330354
usernames = forms.CharField(widget=forms.Textarea(

apps/users/templates/users/edit_impala.html

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,32 @@ <h2>{{ _('My Account') }}</h2>
170170
{% endtrans %}
171171
</p>
172172
</fieldset>
173+
174+
{% if 'admin_log' in form.fields %}
175+
<fieldset id="acct-admin">
176+
<legend>{{ _('Admin') }}</legend>
177+
<p>{{ form.admin_log.label }} {{ required() }}</p>
178+
{{ form.admin_log.errors }}
179+
{{ form.admin_log }}
180+
181+
<p >{{ form.notes.label }}</p>
182+
{{ form.notes.errors }}
183+
{{ form.notes }}
184+
185+
<ul>
186+
<li>
187+
<label for="id_confirmationcode">{{ form.confirmationcode.label }}</label>
188+
{{ form.confirmationcode.errors }}
189+
{{ form.confirmationcode }}
190+
</li>
191+
<li>
192+
<label for="id_anonymize">{{ form.anonymize.label }}</label>
193+
{{ form.anonymize.errors }}
194+
{{ form.anonymize }}
195+
</li>
196+
</ul>
197+
</fieldset>
198+
{% endif %}
173199
</div>{# /#user-profile #}
174200
<div class="listing-footer">
175201
<button type="submit" class="button prominent">{{ _('Update') }}</button>

apps/users/templates/users/profile.html

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,11 @@ <h4>{{ _('In a little more detail...') }}</h4>
2929
</div>
3030
{% endif %}
3131

32-
{% if edit_any_user or own_profile %}
32+
{% if own_profile %}
3333
<p class="editprofile">
3434
{% if own_profile %}
3535
<a href="{{ url("users.edit") }}">{{ _('Edit Profile') }}</a>
3636
{% endif %}
37-
{% if edit_any_user %}
38-
{# TODO XXX Once zamboni can delete users, uncomment this line. bug 595035 #}
39-
{# <a href="{{ url("admin:users_userprofile_change", profile.id) }}">{{ _('Manage User') }}</a> #}
40-
<a href="{{ remora_url("/admin/users/%s" % profile.id) }}">{{ _('Manage User') }}</a>
41-
42-
{% endif %}
4337
</p>
4438
{% endif %}
4539
{% if abuse_form %}

apps/users/tests/test_views.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from django.core.cache import cache
66
from django.contrib.auth.models import User
77
from django.contrib.auth.tokens import default_token_generator
8+
from django.forms.models import model_to_dict
89
from django.test.client import Client
910
from django.utils.http import int_to_base36
1011

@@ -167,6 +168,56 @@ def test_edit_notifications_non_dev(self):
167168
assert len(res.context['form'].errors['notifications'])
168169

169170

171+
class TestEditAdmin(UserViewBase):
172+
fixtures = ['base/users']
173+
174+
def setUp(self):
175+
self.client.login(username='admin@mozilla.com', password='password')
176+
self.regular = self.get_user()
177+
self.url = reverse('users.admin_edit_impala', args=[self.regular.pk])
178+
179+
def get_data(self):
180+
data = model_to_dict(self.regular)
181+
data['admin_log'] = 'test'
182+
for key in ['password', 'resetcode_expires']:
183+
del data[key]
184+
return data
185+
186+
def get_user(self):
187+
# Using pk so that we can still get the user after anonymize.
188+
return UserProfile.objects.get(pk=999)
189+
190+
def test_edit(self):
191+
res = self.client.get(self.url)
192+
eq_(res.status_code, 200)
193+
194+
def test_edit_forbidden(self):
195+
self.client.logout()
196+
self.client.login(username='editor@mozilla.com', password='password')
197+
res = self.client.get(self.url)
198+
eq_(res.status_code, 403)
199+
200+
def test_edit_forbidden_anon(self):
201+
self.client.logout()
202+
res = self.client.get(self.url)
203+
eq_(res.status_code, 302)
204+
205+
def test_anonymize(self):
206+
data = self.get_data()
207+
data['anonymize'] = True
208+
res = self.client.post(self.url, data)
209+
eq_(res.status_code, 302)
210+
eq_(self.get_user().password, "sha512$Anonymous$Password")
211+
212+
def test_anonymize_fails(self):
213+
data = self.get_data()
214+
data['anonymize'] = True
215+
data['email'] = 'something@else.com'
216+
res = self.client.post(self.url, data)
217+
eq_(res.status_code, 200)
218+
eq_(self.get_user().password, self.regular.password) # Hasn't changed.
219+
220+
170221
class TestPasswordAdmin(UserViewBase):
171222
fixtures = ['base/users']
172223

apps/users/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
impala_users_patterns = patterns('',
2424
url('^edit$', views.edit_impala, name='users.edit_impala'),
25+
url('^edit(?:/(?P<user_id>\d+))?$', views.admin_edit_impala,
26+
name='users.admin_edit_impala'),
2527
)
2628

2729
users_patterns = patterns('',

apps/users/views.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919

2020
import amo
2121
from amo import messages
22-
from amo.decorators import login_required, json_view, write
22+
from amo.decorators import (json_view, login_required, permission_required,
23+
write)
2324
from amo.forms import AbuseForm
2425
from amo.urlresolvers import reverse
2526
from amo.utils import send_mail, send_abuse_report
@@ -183,6 +184,25 @@ def edit_impala(request):
183184
{'form': form, 'amouser': amouser})
184185

185186

187+
@write
188+
@login_required
189+
@permission_required('Admin', 'EditAnyUser')
190+
def admin_edit_impala(request, user_id):
191+
amouser = get_object_or_404(UserProfile, pk=user_id)
192+
193+
if request.method == 'POST':
194+
form = forms.AdminUserEditForm(request.POST, request.FILES,
195+
request=request, instance=amouser)
196+
if form.is_valid():
197+
form.save()
198+
messages.success(request, _('Profile Updated'))
199+
return http.HttpResponseRedirect(reverse('zadmin.index'))
200+
else:
201+
form = forms.AdminUserEditForm(instance=amouser)
202+
return jingo.render(request, 'users/edit_impala.html',
203+
{'form': form, 'amouser': amouser})
204+
205+
186206
@write
187207
@login_required
188208
def edit(request):
@@ -413,7 +433,6 @@ def profile(request, user_id):
413433
else:
414434
fav_coll = []
415435

416-
edit_any_user = acl.action_allowed(request, 'Admin', 'EditAnyUser')
417436
own_profile = request.user.is_authenticated() and (
418437
request.amo_user.id == user.id)
419438

@@ -434,8 +453,7 @@ def get_addons(reviews):
434453
reviews = user.reviews.transform(get_addons)
435454

436455
data = {'profile': user, 'own_coll': own_coll, 'reviews': reviews,
437-
'fav_coll': fav_coll, 'edit_any_user': edit_any_user,
438-
'addons': addons, 'own_profile': own_profile,
456+
'fav_coll': fav_coll, 'addons': addons, 'own_profile': own_profile,
439457
'abuse_form': AbuseForm(request=request)}
440458

441459
return jingo.render(request, 'users/profile.html', data)

apps/zadmin/templates/zadmin/index.html

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,28 @@
2424

2525

2626
{% block content %}
27-
<h2>Admin Pages</h2>
28-
<dl>
29-
{% for title, link, desc in links|sort %}
30-
<dt><a href="{{ link }}">{{ title }}</a></dt>
31-
<dd>{{ desc }}</dd>
32-
{% endfor %}
33-
</dl>
3427

28+
{% include "messages.html" %}
29+
30+
<div class="content">
31+
<section>
32+
<h2>Admin Pages</h2>
33+
<dl>
34+
{% for title, link, desc in links|sort %}
35+
<dt><a href="{{ link }}">{{ title }}</a></dt>
36+
<dd>{{ desc }}</dd>
37+
{% endfor %}
38+
</dl>
39+
</section>
40+
<aside>
41+
<h2>Search Users</h2>
42+
<form method="get" action="{{ url('users.admin_edit_impala') }}"
43+
data-search-url="{{ url('zadmin.search', 'users', 'userprofile') }}">
44+
<div>
45+
<input class="searchbar" type="text" value="" name="q" size="40">
46+
<p class="help">Enter the first four characters and select the user from the dropdown.</p>
47+
</div>
48+
</form>
49+
</aside>
50+
</div>
3551
{% endblock %}

0 commit comments

Comments
 (0)