mirror of
https://github.com/django/django.git
synced 2024-12-22 17:16:24 +00:00
Fixed #30289 -- Prevented admin inlines for a ManyToManyField's implicit through model from being editable if the user only has the view permission.
This commit is contained in:
parent
e245046bb6
commit
8335d59200
@ -2111,46 +2111,50 @@ class InlineModelAdmin(BaseModelAdmin):
|
|||||||
queryset = queryset.none()
|
queryset = queryset.none()
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
def _has_any_perms_for_target_model(self, request, perms):
|
||||||
|
"""
|
||||||
|
This method is called only when the ModelAdmin's model is for an
|
||||||
|
ManyToManyField's implicit through model (if self.opts.auto_created).
|
||||||
|
Return True if the user has any of the given permissions ('add',
|
||||||
|
'change', etc.) for the model that points to the through model.
|
||||||
|
"""
|
||||||
|
opts = self.opts
|
||||||
|
# Find the target model of an auto-created many-to-many relationship.
|
||||||
|
for field in opts.fields:
|
||||||
|
if field.remote_field and field.remote_field.model != self.parent_model:
|
||||||
|
opts = field.remote_field.model._meta
|
||||||
|
break
|
||||||
|
return any(
|
||||||
|
request.user.has_perm('%s.%s' % (opts.app_label, get_permission_codename(perm, opts)))
|
||||||
|
for perm in perms
|
||||||
|
)
|
||||||
|
|
||||||
def has_add_permission(self, request, obj):
|
def has_add_permission(self, request, obj):
|
||||||
if self.opts.auto_created:
|
if self.opts.auto_created:
|
||||||
# We're checking the rights to an auto-created intermediate model,
|
# Auto-created intermediate models don't have their own
|
||||||
# which doesn't have its own individual permissions. The user needs
|
# permissions. The user needs to have the change permission for the
|
||||||
# to have the view permission for the related model in order to
|
# related model in order to be able to do anything with the
|
||||||
# be able to do anything with the intermediate model.
|
# intermediate model.
|
||||||
return self.has_view_permission(request, obj)
|
return self._has_any_perms_for_target_model(request, ['change'])
|
||||||
return super().has_add_permission(request)
|
return super().has_add_permission(request)
|
||||||
|
|
||||||
def has_change_permission(self, request, obj=None):
|
def has_change_permission(self, request, obj=None):
|
||||||
if self.opts.auto_created:
|
if self.opts.auto_created:
|
||||||
# We're checking the rights to an auto-created intermediate model,
|
# Same comment as has_add_permission().
|
||||||
# which doesn't have its own individual permissions. The user needs
|
return self._has_any_perms_for_target_model(request, ['change'])
|
||||||
# to have the view permission for the related model in order to
|
|
||||||
# be able to do anything with the intermediate model.
|
|
||||||
return self.has_view_permission(request, obj)
|
|
||||||
return super().has_change_permission(request)
|
return super().has_change_permission(request)
|
||||||
|
|
||||||
def has_delete_permission(self, request, obj=None):
|
def has_delete_permission(self, request, obj=None):
|
||||||
if self.opts.auto_created:
|
if self.opts.auto_created:
|
||||||
# We're checking the rights to an auto-created intermediate model,
|
# Same comment as has_add_permission().
|
||||||
# which doesn't have its own individual permissions. The user needs
|
return self._has_any_perms_for_target_model(request, ['change'])
|
||||||
# to have the view permission for the related model in order to
|
|
||||||
# be able to do anything with the intermediate model.
|
|
||||||
return self.has_view_permission(request, obj)
|
|
||||||
return super().has_delete_permission(request, obj)
|
return super().has_delete_permission(request, obj)
|
||||||
|
|
||||||
def has_view_permission(self, request, obj=None):
|
def has_view_permission(self, request, obj=None):
|
||||||
if self.opts.auto_created:
|
if self.opts.auto_created:
|
||||||
opts = self.opts
|
# Same comment as has_add_permission(). The 'change' permission
|
||||||
# The model was auto-created as intermediary for a many-to-many
|
# also implies the 'view' permission.
|
||||||
# Many-relationship; find the target model.
|
return self._has_any_perms_for_target_model(request, ['view', 'change'])
|
||||||
for field in opts.fields:
|
|
||||||
if field.remote_field and field.remote_field.model != self.parent_model:
|
|
||||||
opts = field.remote_field.model._meta
|
|
||||||
break
|
|
||||||
return (
|
|
||||||
request.user.has_perm('%s.%s' % (opts.app_label, get_permission_codename('view', opts))) or
|
|
||||||
request.user.has_perm('%s.%s' % (opts.app_label, get_permission_codename('change', opts)))
|
|
||||||
)
|
|
||||||
return super().has_view_permission(request)
|
return super().has_view_permission(request)
|
||||||
|
|
||||||
|
|
||||||
|
@ -9,4 +9,6 @@ Django 2.1.8 fixes a bug in 2.1.7.
|
|||||||
Bugfixes
|
Bugfixes
|
||||||
========
|
========
|
||||||
|
|
||||||
*
|
* Prevented admin inlines for a ``ManyToManyField``\'s implicit through model
|
||||||
|
from being editable if the user only has the view permission
|
||||||
|
(:ticket:`30289`).
|
||||||
|
@ -37,6 +37,9 @@ class Child(models.Model):
|
|||||||
class Book(models.Model):
|
class Book(models.Model):
|
||||||
name = models.CharField(max_length=50)
|
name = models.CharField(max_length=50)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class Author(models.Model):
|
class Author(models.Model):
|
||||||
name = models.CharField(max_length=50)
|
name = models.CharField(max_length=50)
|
||||||
|
@ -595,10 +595,10 @@ class TestInlinePermissions(TestCase):
|
|||||||
cls.user.user_permissions.add(permission)
|
cls.user.user_permissions.add(permission)
|
||||||
|
|
||||||
author = Author.objects.create(pk=1, name='The Author')
|
author = Author.objects.create(pk=1, name='The Author')
|
||||||
book = author.books.create(name='The inline Book')
|
cls.book = author.books.create(name='The inline Book')
|
||||||
cls.author_change_url = reverse('admin:admin_inlines_author_change', args=(author.id,))
|
cls.author_change_url = reverse('admin:admin_inlines_author_change', args=(author.id,))
|
||||||
# Get the ID of the automatically created intermediate model for the Author-Book m2m
|
# Get the ID of the automatically created intermediate model for the Author-Book m2m
|
||||||
author_book_auto_m2m_intermediate = Author.books.through.objects.get(author=author, book=book)
|
author_book_auto_m2m_intermediate = Author.books.through.objects.get(author=author, book=cls.book)
|
||||||
cls.author_book_auto_m2m_intermediate_id = author_book_auto_m2m_intermediate.pk
|
cls.author_book_auto_m2m_intermediate_id = author_book_auto_m2m_intermediate.pk
|
||||||
|
|
||||||
cls.holder = Holder2.objects.create(dummy=13)
|
cls.holder = Holder2.objects.create(dummy=13)
|
||||||
@ -636,6 +636,25 @@ class TestInlinePermissions(TestCase):
|
|||||||
self.assertNotContains(response, 'Add another Inner2')
|
self.assertNotContains(response, 'Add another Inner2')
|
||||||
self.assertNotContains(response, 'id="id_inner2_set-TOTAL_FORMS"')
|
self.assertNotContains(response, 'id="id_inner2_set-TOTAL_FORMS"')
|
||||||
|
|
||||||
|
def test_inline_add_m2m_view_only_perm(self):
|
||||||
|
permission = Permission.objects.get(codename='view_book', content_type=self.book_ct)
|
||||||
|
self.user.user_permissions.add(permission)
|
||||||
|
response = self.client.get(reverse('admin:admin_inlines_author_add'))
|
||||||
|
# View-only inlines. (It could be nicer to hide the empty, non-editable
|
||||||
|
# inlines on the add page.)
|
||||||
|
self.assertIs(response.context['inline_admin_formset'].has_view_permission, True)
|
||||||
|
self.assertIs(response.context['inline_admin_formset'].has_add_permission, False)
|
||||||
|
self.assertIs(response.context['inline_admin_formset'].has_change_permission, False)
|
||||||
|
self.assertIs(response.context['inline_admin_formset'].has_delete_permission, False)
|
||||||
|
self.assertContains(response, '<h2>Author-book relationships</h2>')
|
||||||
|
self.assertContains(
|
||||||
|
response,
|
||||||
|
'<input type="hidden" name="Author_books-TOTAL_FORMS" value="0" '
|
||||||
|
'id="id_Author_books-TOTAL_FORMS">',
|
||||||
|
html=True,
|
||||||
|
)
|
||||||
|
self.assertNotContains(response, 'Add another Author-Book Relationship')
|
||||||
|
|
||||||
def test_inline_add_m2m_add_perm(self):
|
def test_inline_add_m2m_add_perm(self):
|
||||||
permission = Permission.objects.get(codename='add_book', content_type=self.book_ct)
|
permission = Permission.objects.get(codename='add_book', content_type=self.book_ct)
|
||||||
self.user.user_permissions.add(permission)
|
self.user.user_permissions.add(permission)
|
||||||
@ -665,11 +684,39 @@ class TestInlinePermissions(TestCase):
|
|||||||
self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
|
self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
|
||||||
self.assertNotContains(response, 'id="id_Author_books-0-DELETE"')
|
self.assertNotContains(response, 'id="id_Author_books-0-DELETE"')
|
||||||
|
|
||||||
|
def test_inline_change_m2m_view_only_perm(self):
|
||||||
|
permission = Permission.objects.get(codename='view_book', content_type=self.book_ct)
|
||||||
|
self.user.user_permissions.add(permission)
|
||||||
|
response = self.client.get(self.author_change_url)
|
||||||
|
# View-only inlines.
|
||||||
|
self.assertIs(response.context['inline_admin_formset'].has_view_permission, True)
|
||||||
|
self.assertIs(response.context['inline_admin_formset'].has_add_permission, False)
|
||||||
|
self.assertIs(response.context['inline_admin_formset'].has_change_permission, False)
|
||||||
|
self.assertIs(response.context['inline_admin_formset'].has_delete_permission, False)
|
||||||
|
self.assertContains(response, '<h2>Author-book relationships</h2>')
|
||||||
|
self.assertContains(
|
||||||
|
response,
|
||||||
|
'<input type="hidden" name="Author_books-TOTAL_FORMS" value="1" '
|
||||||
|
'id="id_Author_books-TOTAL_FORMS">',
|
||||||
|
html=True,
|
||||||
|
)
|
||||||
|
# The field in the inline is read-only.
|
||||||
|
self.assertContains(response, '<p>%s</p>' % self.book)
|
||||||
|
self.assertNotContains(
|
||||||
|
response,
|
||||||
|
'<input type="checkbox" name="Author_books-0-DELETE" id="id_Author_books-0-DELETE">',
|
||||||
|
html=True,
|
||||||
|
)
|
||||||
|
|
||||||
def test_inline_change_m2m_change_perm(self):
|
def test_inline_change_m2m_change_perm(self):
|
||||||
permission = Permission.objects.get(codename='change_book', content_type=self.book_ct)
|
permission = Permission.objects.get(codename='change_book', content_type=self.book_ct)
|
||||||
self.user.user_permissions.add(permission)
|
self.user.user_permissions.add(permission)
|
||||||
response = self.client.get(self.author_change_url)
|
response = self.client.get(self.author_change_url)
|
||||||
# We have change perm on books, so we can add/change/delete inlines
|
# We have change perm on books, so we can add/change/delete inlines
|
||||||
|
self.assertIs(response.context['inline_admin_formset'].has_view_permission, True)
|
||||||
|
self.assertIs(response.context['inline_admin_formset'].has_add_permission, True)
|
||||||
|
self.assertIs(response.context['inline_admin_formset'].has_change_permission, True)
|
||||||
|
self.assertIs(response.context['inline_admin_formset'].has_delete_permission, True)
|
||||||
self.assertContains(response, '<h2>Author-book relationships</h2>')
|
self.assertContains(response, '<h2>Author-book relationships</h2>')
|
||||||
self.assertContains(response, 'Add another Author-book relationship')
|
self.assertContains(response, 'Add another Author-book relationship')
|
||||||
self.assertContains(response, '<input type="hidden" id="id_Author_books-TOTAL_FORMS" '
|
self.assertContains(response, '<input type="hidden" id="id_Author_books-TOTAL_FORMS" '
|
||||||
|
Loading…
Reference in New Issue
Block a user