Skip to content

Fix read-only change view #110

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 41 additions & 7 deletions example/app/tests/test_admin.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,64 @@
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:
from django.core.urlresolvers import reverse


class TopLevelAdminTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.superuser = User.objects.create_superuser(username='super', password='secret', email='[email protected]')

def setUp(self):
self.superuser = User.objects.create_superuser(username='super', password='secret', email='[email protected]')
self.user = User.objects.create_user(
username='user', password='secret', email='[email protected]',
)
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)
70 changes: 58 additions & 12 deletions nested_inline/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -112,16 +113,24 @@ 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,
)
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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.') % {
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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 = []
Expand Down