diff --git a/example/app/tests/test_admin.py b/example/app/tests/test_admin.py index afe18bd..d214b04 100644 --- a/example/app/tests/test_admin.py +++ b/example/app/tests/test_admin.py @@ -1,7 +1,12 @@ -from django.contrib.auth.models import User +from unittest import skipIf + +from django import VERSION +from django.contrib.auth.models import Permission, User from django.template.response import TemplateResponse from django.test import TestCase +from example.app.models import TopLevel + try: from django.urls import reverse except ImportError: @@ -9,22 +14,51 @@ class TopLevelAdminTestCase(TestCase): - @classmethod - def setUpTestData(cls): - cls.superuser = User.objects.create_superuser(username='super', password='secret', email='super@example.com') - def setUp(self): + self.superuser = User.objects.create_superuser(username='super', password='secret', email='super@example.com') + self.user = User.objects.create_user( + username='user', password='secret', email='user@example.com', + ) + self.user.is_staff = True + self.user.save() + + def login(self, user): try: - self.client.force_login(self.superuser) + self.client.force_login(user) except AttributeError: - self.client.login(username=self.superuser.username, password='secret') + self.client.login(username=user.username, password='secret') def test_changelist(self): + self.login(self.superuser) + response = self.client.get(reverse('admin:app_toplevel_changelist')) self.assertIsInstance(response, TemplateResponse) self.assertEqual(response.status_code, 200) def test_add_view(self): + self.login(self.superuser) + response = self.client.get(reverse('admin:app_toplevel_add')) self.assertIsInstance(response, TemplateResponse) self.assertEqual(response.status_code, 200) + + def test_change_view(self): + change_perm = Permission.objects.get(codename='change_toplevel') + self.user.user_permissions.add(change_perm) + self.login(self.user) + + obj = TopLevel.objects.create() + response = self.client.get(reverse('admin:app_toplevel_change', args=(obj.pk,))) + self.assertIsInstance(response, TemplateResponse) + self.assertEqual(response.status_code, 200) + + @skipIf(VERSION < (2, 1), 'view permission was added in Django 2.1') + def test_read_only_change_view(self): + view_perm = Permission.objects.get(codename='view_toplevel') + self.user.user_permissions.add(view_perm) + self.login(self.user) + + obj = TopLevel.objects.create() + response = self.client.get(reverse('admin:app_toplevel_change', args=(obj.pk,))) + self.assertIsInstance(response, TemplateResponse) + self.assertEqual(response.status_code, 200) diff --git a/nested_inline/admin.py b/nested_inline/admin.py index a1906f5..adfecda 100644 --- a/nested_inline/admin.py +++ b/nested_inline/admin.py @@ -3,7 +3,8 @@ from django.contrib import admin from django.contrib.admin import helpers from django.contrib.admin.options import InlineModelAdmin, reverse -from django.contrib.admin.utils import unquote +from django.contrib.admin.utils import flatten_fieldsets, unquote +from django.contrib.auth import get_permission_codename from django.core.exceptions import PermissionDenied from django.db import models, transaction from django.forms.formsets import all_valid @@ -95,7 +96,7 @@ def add_nested_inline_formsets(self, request, inline, formset, depth=0): self.add_nested_inline_formsets(request, nested_inline, nested_formset, depth=depth + 1) form.nested_formsets = nested_formsets - def wrap_nested_inline_formsets(self, request, inline, formset): + def wrap_nested_inline_formsets(self, request, inline, formset, read_only): media = None def get_media(extra_media): @@ -112,8 +113,14 @@ def get_media(extra_media): else: instance = None fieldsets = list(nested_inline.get_fieldsets(request, instance)) - readonly = list(nested_inline.get_readonly_fields(request, instance)) - prepopulated = dict(nested_inline.get_prepopulated_fields(request, instance)) + + if read_only: + readonly = flatten_fieldsets(list(nested_inline.get_fieldsets(request, instance))) + prepopulated = {} + else: + readonly = list(nested_inline.get_readonly_fields(request, instance)) + prepopulated = dict(nested_inline.get_prepopulated_fields(request, instance)) + wrapped_nested_formset = helpers.InlineAdminFormSet( nested_inline, nested_formset, fieldsets, prepopulated, readonly, model_admin=self, @@ -121,7 +128,9 @@ def get_media(extra_media): wrapped_nested_formsets.append(wrapped_nested_formset) media = get_media(wrapped_nested_formset.media) if nested_inline.inlines: - media = get_media(self.wrap_nested_inline_formsets(request, nested_inline, nested_formset)) + media = get_media( + self.wrap_nested_inline_formsets(request, nested_inline, nested_formset, read_only) + ) form.nested_formsets = wrapped_nested_formsets return media @@ -249,7 +258,7 @@ def add_view(self, request, form_url='', extra_context=None): media = media + inline_admin_formset.media if hasattr(inline, 'inlines') and inline.inlines: extra_media = self.wrap_nested_inline_formsets( - request, inline, formset) + request, inline, formset, False) if extra_media: media += extra_media @@ -277,8 +286,12 @@ def change_view(self, request, object_id, form_url='', extra_context=None): obj = self.get_object(request, unquote(object_id)) - if not self.has_change_permission(request, obj): - raise PermissionDenied + if request.method == 'POST': + if not self.has_change_permission(request, obj): + raise PermissionDenied + else: + if not self.has_view_or_change_permission(request, obj): + raise PermissionDenied if obj is None: raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % { @@ -335,10 +348,15 @@ def change_view(self, request, object_id, form_url='', extra_context=None): if hasattr(inline, 'inlines') and inline.inlines: self.add_nested_inline_formsets(request, inline, formset) + if not self.has_change_permission(request, obj): + readonly_fields = flatten_fieldsets(self.get_fieldsets(request, obj)) + else: + readonly_fields = self.get_readonly_fields(request, obj) + adminForm = helpers.AdminForm( form, self.get_fieldsets(request, obj), - self.get_prepopulated_fields(request, obj), - self.get_readonly_fields(request, obj), + self.get_prepopulated_fields(request, obj) if self.has_change_permission(request, obj) else {}, + readonly_fields, model_admin=self, ) media = self.media + adminForm.media @@ -354,12 +372,19 @@ def change_view(self, request, object_id, form_url='', extra_context=None): inline_admin_formsets.append(inline_admin_formset) media = media + inline_admin_formset.media if hasattr(inline, 'inlines') and inline.inlines: - extra_media = self.wrap_nested_inline_formsets(request, inline, formset) + extra_media = self.wrap_nested_inline_formsets( + request, inline, formset, self.has_change_permission(request, obj), + ) if extra_media: media += extra_media + if self.has_change_permission(request, obj): + title = _('Change %s') + else: + title = _('View %s') + context = { - 'title': _('Change %s') % force_text(opts.verbose_name), + 'title': title % force_text(opts.verbose_name), 'adminform': adminForm, 'object_id': object_id, 'original': obj, @@ -373,6 +398,27 @@ def change_view(self, request, object_id, form_url='', extra_context=None): context.update(extra_context or {}) return self.render_change_form(request, context, change=True, obj=obj, form_url=form_url) + def has_view_permission(self, request, obj=None): + """ + Return True if the given request has permission to view the given + Django model instance. The default implementation doesn't examine the + `obj` parameter. + If overridden by the user in subclasses, it should return True if the + given request has permission to view the `obj` model instance. If `obj` + is None, it should return True if the request has permission to view + any object of the given type. + """ + opts = self.opts + codename_view = get_permission_codename('view', opts) + codename_change = get_permission_codename('change', opts) + return ( + request.user.has_perm('%s.%s' % (opts.app_label, codename_view)) or + request.user.has_perm('%s.%s' % (opts.app_label, codename_change)) + ) + + def has_view_or_change_permission(self, request, obj=None): + return self.has_view_permission(request, obj) or self.has_change_permission(request, obj) + class NestedInline(InlineInstancesMixin, InlineModelAdmin): inlines = []