Skip to content

Commit 504c89e

Browse files
Maxime Turcottetimgraham
Maxime Turcotte
authored andcommitted
Fixed django#6327 -- Added has_module_permission method to BaseModelAdmin
Thanks chrj for the suggestion.
1 parent bf743a4 commit 504c89e

File tree

9 files changed

+201
-7
lines changed

9 files changed

+201
-7
lines changed

django/contrib/admin/options.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,19 @@ def has_delete_permission(self, request, obj=None):
473473
codename = get_permission_codename('delete', opts)
474474
return request.user.has_perm("%s.%s" % (opts.app_label, codename))
475475

476+
def has_module_permission(self, request):
477+
"""
478+
Returns True if the given request has any permission in the given
479+
app label.
480+
481+
Can be overridden by the user in subclasses. In such case it should
482+
return True if the given request has permission to view the module on
483+
the admin index page and access the module's index page. Overriding it
484+
does not restrict access to the add, change or delete views. Use
485+
`ModelAdmin.has_(add|change|delete)_permission` for that.
486+
"""
487+
return request.user.has_module_perms(self.opts.app_label)
488+
476489

477490
@python_2_unicode_compatible
478491
class ModelAdmin(BaseModelAdmin):

django/contrib/admin/sites.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -367,10 +367,9 @@ def index(self, request, extra_context=None):
367367
apps that have been registered in this site.
368368
"""
369369
app_dict = {}
370-
user = request.user
371370
for model, model_admin in self._registry.items():
372371
app_label = model._meta.app_label
373-
has_module_perms = user.has_module_perms(app_label)
372+
has_module_perms = model_admin.has_module_permission(request)
374373

375374
if has_module_perms:
376375
perms = model_admin.get_model_perms(request)
@@ -424,14 +423,14 @@ def index(self, request, extra_context=None):
424423
current_app=self.name)
425424

426425
def app_index(self, request, app_label, extra_context=None):
427-
user = request.user
428426
app_name = apps.get_app_config(app_label).verbose_name
429-
has_module_perms = user.has_module_perms(app_label)
430-
if not has_module_perms:
431-
raise PermissionDenied
432427
app_dict = {}
433428
for model, model_admin in self._registry.items():
434429
if app_label == model._meta.app_label:
430+
has_module_perms = model_admin.has_module_permission(request)
431+
if not has_module_perms:
432+
raise PermissionDenied
433+
435434
perms = model_admin.get_model_perms(request)
436435

437436
# Check whether user has any perm for this module.

docs/ref/contrib/admin/index.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1631,6 +1631,19 @@ templates used by the :class:`ModelAdmin` views:
16311631
be interpreted as meaning that the current user is not permitted to delete
16321632
any object of this type).
16331633

1634+
.. method:: ModelAdmin.has_module_permission(request)
1635+
1636+
.. versionadded:: 1.8
1637+
1638+
Should return ``True`` if displaying the module on the admin index page and
1639+
accessing the module's index page is permitted, ``False`` otherwise.
1640+
Uses :meth:`User.has_module_perms()
1641+
<django.contrib.auth.models.User.has_module_perms>` by default. Overriding
1642+
it does not restrict access to the add, change or delete views,
1643+
:meth:`~ModelAdmin.has_add_permission`,
1644+
:meth:`~ModelAdmin.has_change_permission`, and
1645+
:meth:`~ModelAdmin.has_delete_permission` should be used for that.
1646+
16341647
.. method:: ModelAdmin.get_queryset(request)
16351648

16361649
The ``get_queryset`` method on a ``ModelAdmin`` returns a
@@ -1909,6 +1922,7 @@ adds some of its own (the shared features are actually defined in the
19091922
- :meth:`~ModelAdmin.has_add_permission`
19101923
- :meth:`~ModelAdmin.has_change_permission`
19111924
- :meth:`~ModelAdmin.has_delete_permission`
1925+
- :meth:`~ModelAdmin.has_module_permission`
19121926

19131927
The ``InlineModelAdmin`` class adds:
19141928

docs/releases/1.8.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ Minor features
3131
:mod:`django.contrib.admin`
3232
^^^^^^^^^^^^^^^^^^^^^^^^^^^
3333

34-
* ...
34+
* :class:`~django.contrib.admin.ModelAdmin` now has a
35+
:meth:`~django.contrib.admin.ModelAdmin.has_module_permission`
36+
method to allow limiting access to the module on the admin index page.
3537

3638
:mod:`django.contrib.auth`
3739
^^^^^^^^^^^^^^^^^^^^^^^^^^

tests/admin_ordering/tests.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ class MockSuperUser(object):
1717
def has_perm(self, perm):
1818
return True
1919

20+
def has_module_perms(self, module):
21+
return True
22+
2023
request = MockRequest()
2124
request.user = MockSuperUser()
2225

tests/admin_views/admin.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,12 @@ def save_model(self, request, obj, form, change=True):
124124
return super(ArticleAdmin, self).save_model(request, obj, form, change)
125125

126126

127+
class ArticleAdmin2(admin.ModelAdmin):
128+
129+
def has_module_permission(self, request):
130+
return False
131+
132+
127133
class RowLevelChangePermissionModelAdmin(admin.ModelAdmin):
128134
def has_change_permission(self, request, obj=None):
129135
""" Only allow changing objects with even id number """
@@ -923,3 +929,5 @@ def get_changeform_initial_data(self, request):
923929
site2 = admin.AdminSite(name="namespaced_admin")
924930
site2.register(User, UserAdmin)
925931
site2.register(Group, GroupAdmin)
932+
site7 = admin.AdminSite(name="admin7")
933+
site7.register(Article, ArticleAdmin2)

tests/admin_views/tests.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1493,6 +1493,70 @@ def test_shortcut_view_only_available_to_staff(self):
14931493
self.assertEqual(response.status_code, 302)
14941494
self.assertEqual(response.url, 'http://example.com/dummy/foo/')
14951495

1496+
def test_has_module_permission(self):
1497+
"""
1498+
Ensure that has_module_permission() returns True for all users who
1499+
have any permission for that module (add, change, or delete), so that
1500+
the module is displayed on the admin index page.
1501+
"""
1502+
login_url = reverse('admin:login') + '?next=/test_admin/admin/'
1503+
1504+
self.client.post(login_url, self.super_login)
1505+
response = self.client.get('/test_admin/admin/')
1506+
self.assertContains(response, 'admin_views')
1507+
self.assertContains(response, 'Articles')
1508+
self.client.get('/test_admin/admin/logout/')
1509+
1510+
self.client.post(login_url, self.adduser_login)
1511+
response = self.client.get('/test_admin/admin/')
1512+
self.assertContains(response, 'admin_views')
1513+
self.assertContains(response, 'Articles')
1514+
self.client.get('/test_admin/admin/logout/')
1515+
1516+
self.client.post(login_url, self.changeuser_login)
1517+
response = self.client.get('/test_admin/admin/')
1518+
self.assertContains(response, 'admin_views')
1519+
self.assertContains(response, 'Articles')
1520+
self.client.get('/test_admin/admin/logout/')
1521+
1522+
self.client.post(login_url, self.deleteuser_login)
1523+
response = self.client.get('/test_admin/admin/')
1524+
self.assertContains(response, 'admin_views')
1525+
self.assertContains(response, 'Articles')
1526+
self.client.get('/test_admin/admin/logout/')
1527+
1528+
def test_overriding_has_module_permission(self):
1529+
"""
1530+
Ensure that overriding has_module_permission() has the desired effect.
1531+
In this case, it always returns False, so the module should not be
1532+
displayed on the admin index page for any users.
1533+
"""
1534+
login_url = reverse('admin:login') + '?next=/test_admin/admin7/'
1535+
1536+
self.client.post(login_url, self.super_login)
1537+
response = self.client.get('/test_admin/admin7/')
1538+
self.assertNotContains(response, 'admin_views')
1539+
self.assertNotContains(response, 'Articles')
1540+
self.client.get('/test_admin/admin7/logout/')
1541+
1542+
self.client.post(login_url, self.adduser_login)
1543+
response = self.client.get('/test_admin/admin7/')
1544+
self.assertNotContains(response, 'admin_views')
1545+
self.assertNotContains(response, 'Articles')
1546+
self.client.get('/test_admin/admin7/logout/')
1547+
1548+
self.client.post(login_url, self.changeuser_login)
1549+
response = self.client.get('/test_admin/admin7/')
1550+
self.assertNotContains(response, 'admin_views')
1551+
self.assertNotContains(response, 'Articles')
1552+
self.client.get('/test_admin/admin7/logout/')
1553+
1554+
self.client.post(login_url, self.deleteuser_login)
1555+
response = self.client.get('/test_admin/admin7/')
1556+
self.assertNotContains(response, 'admin_views')
1557+
self.assertNotContains(response, 'Articles')
1558+
self.client.get('/test_admin/admin7/logout/')
1559+
14961560

14971561
@override_settings(PASSWORD_HASHERS=('django.contrib.auth.hashers.SHA1PasswordHasher',),
14981562
ROOT_URLCONF="admin_views.urls")

tests/admin_views/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@
1111
url(r'^test_admin/admin3/', include(admin.site.urls), dict(form_url='pony')),
1212
url(r'^test_admin/admin4/', include(customadmin.simple_site.urls)),
1313
url(r'^test_admin/admin5/', include(admin.site2.urls)),
14+
url(r'^test_admin/admin7/', include(admin.site7.urls)),
1415
]

tests/modeladmin/tests.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1542,3 +1542,93 @@ class ProductAdmin(ModelAdmin):
15421542
list_editable = ['name', 'slug']
15431543
list_display_links = ['pub_date']
15441544
self.assertIsValid(ProductAdmin, ValidationTestModel)
1545+
1546+
1547+
class ModelAdminPermissionTests(TestCase):
1548+
1549+
class MockUser(object):
1550+
def has_module_perms(self, app_label):
1551+
if app_label == "modeladmin":
1552+
return True
1553+
return False
1554+
1555+
class MockAddUser(MockUser):
1556+
def has_perm(self, perm):
1557+
if perm == "modeladmin.add_band":
1558+
return True
1559+
return False
1560+
1561+
class MockChangeUser(MockUser):
1562+
def has_perm(self, perm):
1563+
if perm == "modeladmin.change_band":
1564+
return True
1565+
return False
1566+
1567+
class MockDeleteUser(MockUser):
1568+
def has_perm(self, perm):
1569+
if perm == "modeladmin.delete_band":
1570+
return True
1571+
return False
1572+
1573+
def test_has_add_permission(self):
1574+
"""
1575+
Ensure that has_add_permission returns True for users who can add
1576+
objects and False for users who can't.
1577+
"""
1578+
ma = ModelAdmin(Band, AdminSite())
1579+
request = MockRequest()
1580+
request.user = self.MockAddUser()
1581+
self.assertTrue(ma.has_add_permission(request))
1582+
request.user = self.MockChangeUser()
1583+
self.assertFalse(ma.has_add_permission(request))
1584+
request.user = self.MockDeleteUser()
1585+
self.assertFalse(ma.has_add_permission(request))
1586+
1587+
def test_has_change_permission(self):
1588+
"""
1589+
Ensure that has_change_permission returns True for users who can edit
1590+
objects and False for users who can't.
1591+
"""
1592+
ma = ModelAdmin(Band, AdminSite())
1593+
request = MockRequest()
1594+
request.user = self.MockAddUser()
1595+
self.assertFalse(ma.has_change_permission(request))
1596+
request.user = self.MockChangeUser()
1597+
self.assertTrue(ma.has_change_permission(request))
1598+
request.user = self.MockDeleteUser()
1599+
self.assertFalse(ma.has_change_permission(request))
1600+
1601+
def test_has_delete_permission(self):
1602+
"""
1603+
Ensure that has_delete_permission returns True for users who can delete
1604+
objects and False for users who can't.
1605+
"""
1606+
ma = ModelAdmin(Band, AdminSite())
1607+
request = MockRequest()
1608+
request.user = self.MockAddUser()
1609+
self.assertFalse(ma.has_delete_permission(request))
1610+
request.user = self.MockChangeUser()
1611+
self.assertFalse(ma.has_delete_permission(request))
1612+
request.user = self.MockDeleteUser()
1613+
self.assertTrue(ma.has_delete_permission(request))
1614+
1615+
def test_has_module_permission(self):
1616+
"""
1617+
Ensure that has_module_permission returns True for users who have any
1618+
permission for the module and False for users who don't.
1619+
"""
1620+
ma = ModelAdmin(Band, AdminSite())
1621+
request = MockRequest()
1622+
request.user = self.MockAddUser()
1623+
self.assertTrue(ma.has_module_permission(request))
1624+
request.user = self.MockChangeUser()
1625+
self.assertTrue(ma.has_module_permission(request))
1626+
request.user = self.MockDeleteUser()
1627+
self.assertTrue(ma.has_module_permission(request))
1628+
ma.opts.app_label = "anotherapp"
1629+
request.user = self.MockAddUser()
1630+
self.assertFalse(ma.has_module_permission(request))
1631+
request.user = self.MockChangeUser()
1632+
self.assertFalse(ma.has_module_permission(request))
1633+
request.user = self.MockDeleteUser()
1634+
self.assertFalse(ma.has_module_permission(request))

0 commit comments

Comments
 (0)