Ticket #8060: admin_inline_permissions_v2.diff

File admin_inline_permissions_v2.diff, 20.8 KB (added by Stephan Jaensch, 13 years ago)
  • django/contrib/admin/options.py

    diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
    index f05b5cb..96f5ede 100644
    a b class BaseModelAdmin(object):  
    270270            clean_lookup = LOOKUP_SEP.join(parts)
    271271            return clean_lookup in self.list_filter or clean_lookup == self.date_hierarchy
    272272
     273    def has_add_permission(self, request):
     274        """
     275        Returns True if the given request has permission to add an object.
     276        Can be overriden by the user in subclasses.
     277        """
     278        opts = self.opts
     279        return request.user.has_perm(opts.app_label + '.' + opts.get_add_permission())
     280
     281    def has_change_permission(self, request, obj=None):
     282        """
     283        Returns True if the given request has permission to change the given
     284        Django model instance, the default implementation doesn't examine the
     285        `obj` parameter.
     286
     287        Can be overriden by the user in subclasses. In such case it should
     288        return True if the given request has permission to change the `obj`
     289        model instance. If `obj` is None, this should return True if the given
     290        request has permission to change *any* object of the given type.
     291        """
     292        opts = self.opts
     293        return request.user.has_perm(opts.app_label + '.' + opts.get_change_permission())
     294
     295    def has_delete_permission(self, request, obj=None):
     296        """
     297        Returns True if the given request has permission to change the given
     298        Django model instance, the default implementation doesn't examine the
     299        `obj` parameter.
     300
     301        Can be overriden by the user in subclasses. In such case it should
     302        return True if the given request has permission to delete the `obj`
     303        model instance. If `obj` is None, this should return True if the given
     304        request has permission to delete *any* object of the given type.
     305        """
     306        opts = self.opts
     307        return request.user.has_perm(opts.app_label + '.' + opts.get_delete_permission())
    273308
    274309class ModelAdmin(BaseModelAdmin):
    275310    "Encapsulates all admin options and functionality for a given model."
    class ModelAdmin(BaseModelAdmin):  
    307342        self.model = model
    308343        self.opts = model._meta
    309344        self.admin_site = admin_site
    310         self.inline_instances = []
    311         for inline_class in self.inlines:
    312             inline_instance = inline_class(self.model, self.admin_site)
    313             self.inline_instances.append(inline_instance)
    314345        if 'action_checkbox' not in self.list_display and self.actions is not None:
    315346            self.list_display = ['action_checkbox'] +  list(self.list_display)
    316347        if not self.list_display_links:
    class ModelAdmin(BaseModelAdmin):  
    320351                    break
    321352        super(ModelAdmin, self).__init__()
    322353
     354    def get_inline_instances(self, request=None, action=None):
     355        inline_instances = []
     356        for inline_class in self.inlines:
     357            inline = inline_class(self.model, self.admin_site)
     358            if request:
     359                if not ((action == 'add' and inline.has_add_permission(request)) or
     360                    (inline.has_add_permission(request) or inline.has_change_permission(request) or inline.has_delete_permission(request))):
     361                    continue
     362                if action == 'change' and not inline.has_add_permission(request):
     363                    inline.max_num = 0
     364            inline_instances.append(inline)
     365
     366        return inline_instances
     367
     368    @property
     369    def inline_instances(self):
     370        return self.get_inline_instances()
     371
    323372    def get_urls(self):
    324373        from django.conf.urls import patterns, url
    325374
    class ModelAdmin(BaseModelAdmin):  
    369418            js.extend(['getElementsBySelector.js', 'dom-drag.js' , 'admin/ordering.js'])
    370419        return forms.Media(js=[static('admin/js/%s' % url) for url in js])
    371420
    372     def has_add_permission(self, request):
    373         """
    374         Returns True if the given request has permission to add an object.
    375         Can be overriden by the user in subclasses.
    376         """
    377         opts = self.opts
    378         return request.user.has_perm(opts.app_label + '.' + opts.get_add_permission())
    379 
    380     def has_change_permission(self, request, obj=None):
    381         """
    382         Returns True if the given request has permission to change the given
    383         Django model instance, the default implementation doesn't examine the
    384         `obj` parameter.
    385 
    386         Can be overriden by the user in subclasses. In such case it should
    387         return True if the given request has permission to change the `obj`
    388         model instance. If `obj` is None, this should return True if the given
    389         request has permission to change *any* object of the given type.
    390         """
    391         opts = self.opts
    392         return request.user.has_perm(opts.app_label + '.' + opts.get_change_permission())
    393 
    394     def has_delete_permission(self, request, obj=None):
    395         """
    396         Returns True if the given request has permission to change the given
    397         Django model instance, the default implementation doesn't examine the
    398         `obj` parameter.
    399 
    400         Can be overriden by the user in subclasses. In such case it should
    401         return True if the given request has permission to delete the `obj`
    402         model instance. If `obj` is None, this should return True if the given
    403         request has permission to delete *any* object of the given type.
    404         """
    405         opts = self.opts
    406         return request.user.has_perm(opts.app_label + '.' + opts.get_delete_permission())
    407 
    408421    def get_model_perms(self, request):
    409422        """
    410423        Returns a dict of all perms for this model. This dict has the keys
    class ModelAdmin(BaseModelAdmin):  
    500513            fields=self.list_editable, **defaults)
    501514
    502515    def get_formsets(self, request, obj=None):
    503         for inline in self.inline_instances:
     516        action = ('change' if obj else 'add')
     517        for inline in self.get_inline_instances(request, action):
    504518            yield inline.get_formset(request, obj)
    505519
    506520    def get_paginator(self, request, queryset, per_page, orphans=0, allow_empty_first_page=True):
    class ModelAdmin(BaseModelAdmin):  
    914928
    915929        ModelForm = self.get_form(request)
    916930        formsets = []
     931        inline_instances = self.get_inline_instances(request, 'add')
    917932        if request.method == 'POST':
    918933            form = ModelForm(request.POST, request.FILES)
    919934            if form.is_valid():
    class ModelAdmin(BaseModelAdmin):  
    923938                form_validated = False
    924939                new_object = self.model()
    925940            prefixes = {}
    926             for FormSet, inline in zip(self.get_formsets(request), self.inline_instances):
     941            for FormSet, inline in zip(self.get_formsets(request), inline_instances):
    927942                prefix = FormSet.get_default_prefix()
    928943                prefixes[prefix] = prefixes.get(prefix, 0) + 1
    929944                if prefixes[prefix] != 1 or not prefix:
    class ModelAdmin(BaseModelAdmin):  
    951966                    initial[k] = initial[k].split(",")
    952967            form = ModelForm(initial=initial)
    953968            prefixes = {}
    954             for FormSet, inline in zip(self.get_formsets(request),
    955                                        self.inline_instances):
     969            for FormSet, inline in zip(self.get_formsets(request), inline_instances):
    956970                prefix = FormSet.get_default_prefix()
    957971                prefixes[prefix] = prefixes.get(prefix, 0) + 1
    958972                if prefixes[prefix] != 1 or not prefix:
    class ModelAdmin(BaseModelAdmin):  
    968982        media = self.media + adminForm.media
    969983
    970984        inline_admin_formsets = []
    971         for inline, formset in zip(self.inline_instances, formsets):
     985        for inline, formset in zip(inline_instances, formsets):
    972986            fieldsets = list(inline.get_fieldsets(request))
    973987            readonly = list(inline.get_readonly_fields(request))
    974988            prepopulated = dict(inline.get_prepopulated_fields(request))
    class ModelAdmin(BaseModelAdmin):  
    10121026
    10131027        ModelForm = self.get_form(request, obj)
    10141028        formsets = []
     1029        inline_instances = self.get_inline_instances(request, 'change')
    10151030        if request.method == 'POST':
    10161031            form = ModelForm(request.POST, request.FILES, instance=obj)
    10171032            if form.is_valid():
    class ModelAdmin(BaseModelAdmin):  
    10211036                form_validated = False
    10221037                new_object = obj
    10231038            prefixes = {}
    1024             for FormSet, inline in zip(self.get_formsets(request, new_object),
    1025                                        self.inline_instances):
     1039            for FormSet, inline in zip(self.get_formsets(request, new_object), inline_instances):
    10261040                prefix = FormSet.get_default_prefix()
    10271041                prefixes[prefix] = prefixes.get(prefix, 0) + 1
    10281042                if prefixes[prefix] != 1 or not prefix:
    class ModelAdmin(BaseModelAdmin):  
    10431057        else:
    10441058            form = ModelForm(instance=obj)
    10451059            prefixes = {}
    1046             for FormSet, inline in zip(self.get_formsets(request, obj), self.inline_instances):
     1060            for FormSet, inline in zip(self.get_formsets(request, obj), inline_instances):
    10471061                prefix = FormSet.get_default_prefix()
    10481062                prefixes[prefix] = prefixes.get(prefix, 0) + 1
    10491063                if prefixes[prefix] != 1 or not prefix:
    class ModelAdmin(BaseModelAdmin):  
    10591073        media = self.media + adminForm.media
    10601074
    10611075        inline_admin_formsets = []
    1062         for inline, formset in zip(self.inline_instances, formsets):
     1076        for inline, formset in zip(inline_instances, formsets):
    10631077            fieldsets = list(inline.get_fieldsets(request, obj))
    10641078            readonly = list(inline.get_readonly_fields(request, obj))
    10651079            prepopulated = dict(inline.get_prepopulated_fields(request, obj))
    class InlineModelAdmin(BaseModelAdmin):  
    13771391        # if exclude is an empty list we use None, since that's the actual
    13781392        # default
    13791393        exclude = exclude or None
     1394        can_delete = self.can_delete
     1395        if request:  # some tests pass None as request
     1396            can_delete = can_delete and self.has_delete_permission(request, obj)
    13801397        defaults = {
    13811398            "form": self.form,
    13821399            "formset": self.formset,
    class InlineModelAdmin(BaseModelAdmin):  
    13861403            "formfield_callback": partial(self.formfield_for_dbfield, request=request),
    13871404            "extra": self.extra,
    13881405            "max_num": self.max_num,
    1389             "can_delete": self.can_delete,
     1406            "can_delete": can_delete,
    13901407        }
    13911408        defaults.update(kwargs)
    13921409        return inlineformset_factory(self.parent_model, self.model, **defaults)
    class InlineModelAdmin(BaseModelAdmin):  
    13981415        fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj))
    13991416        return [(None, {'fields': fields})]
    14001417
     1418    def queryset(self, request):
     1419        queryset = super(InlineModelAdmin, self).queryset(request)
     1420        if request and not self.has_change_permission(request):
     1421            queryset = queryset.filter(pk__isnull=True)
     1422        return queryset
     1423
     1424    def get_permission_opts(self):
     1425        opts = self.opts
     1426        if opts.auto_created:
     1427            # The model was auto-created as intermediary for a
     1428            # ManyToMany-relationship, find out the destination model
     1429            for field in opts.fields:
     1430                if isinstance(field, models.ForeignKey) and field.rel.to != opts.auto_created:
     1431                    opts = field.rel.to._meta
     1432                    break
     1433        return opts
     1434
     1435    def has_add_permission(self, request):
     1436        opts = self.get_permission_opts()
     1437        return request.user.has_perm(opts.app_label + '.' + opts.get_add_permission())
     1438
     1439    def has_change_permission(self, request, obj=None):
     1440        opts = self.get_permission_opts()
     1441        return request.user.has_perm(opts.app_label + '.' + opts.get_change_permission())
     1442
     1443    def has_delete_permission(self, request, obj=None):
     1444        opts = self.get_permission_opts()
     1445        return request.user.has_perm(opts.app_label + '.' + opts.get_delete_permission())
     1446
    14011447class StackedInline(InlineModelAdmin):
    14021448    template = 'admin/edit_inline/stacked.html'
    14031449
  • django/contrib/contenttypes/generic.py

    diff --git a/django/contrib/contenttypes/generic.py b/django/contrib/contenttypes/generic.py
    index 69dd527..e558aee 100644
    a b class GenericInlineModelAdmin(InlineModelAdmin):  
    411411            # GenericInlineModelAdmin doesn't define its own.
    412412            exclude.extend(self.form._meta.exclude)
    413413        exclude = exclude or None
     414        can_delete = self.can_delete
     415        if request:  # some tests pass None as request
     416            can_delete = can_delete and self.has_delete_permission(request, obj)
    414417        defaults = {
    415418            "ct_field": self.ct_field,
    416419            "fk_field": self.ct_fk_field,
    class GenericInlineModelAdmin(InlineModelAdmin):  
    418421            "formfield_callback": partial(self.formfield_for_dbfield, request=request),
    419422            "formset": self.formset,
    420423            "extra": self.extra,
    421             "can_delete": self.can_delete,
     424            "can_delete": can_delete,
    422425            "can_order": False,
    423426            "fields": fields,
    424427            "max_num": self.max_num,
  • tests/regressiontests/admin_inlines/tests.py

    diff --git a/tests/regressiontests/admin_inlines/tests.py b/tests/regressiontests/admin_inlines/tests.py
    index 955d620..7a12724 100644
    a b  
    11from django.contrib.admin.helpers import InlineAdminForm
     2from django.contrib.auth.models import User, Permission
    23from django.contrib.contenttypes.models import ContentType
    34from django.test import TestCase
    45
    56# local test models
    67from models import (Holder, Inner, Holder2, Inner2, Holder3,
    78    Inner3, Person, OutfitItem, Fashionista, Teacher, Parent, Child,
    8     CapoFamiglia, Consigliere, SottoCapo)
     9    Author, Book)
    910from admin import InnerInline
    1011
    1112
    class TestInline(TestCase):  
    141142                '<input id="id_-2-0-name" type="text" class="vTextField" '
    142143                'name="-2-0-name" maxlength="100" />')
    143144
     145    def test_inline_permissions(self):
     146        """
     147        Make sure the admin respects permissions for objects that are edited
     148        inline. Ref #8060.
     149        """
     150        user = User.objects.get(username='super')
     151        user.is_superuser = False
     152        user.save()
     153
     154        author_ct = ContentType.objects.get_for_model(Author)
     155        holder_ct = ContentType.objects.get_for_model(Holder)
     156        book_ct = ContentType.objects.get_for_model(Book)
     157        inner_ct = ContentType.objects.get_for_model(Inner)
     158
     159        permission = Permission.objects.get(codename='add_author', content_type=author_ct)
     160        user.user_permissions.add(permission)
     161        permission = Permission.objects.get(codename='change_author', content_type=author_ct)
     162        user.user_permissions.add(permission)
     163        permission = Permission.objects.get(codename='add_holder', content_type=holder_ct)
     164        user.user_permissions.add(permission)
     165        permission = Permission.objects.get(codename='change_holder', content_type=holder_ct)
     166        user.user_permissions.add(permission)
     167
     168        author = Author.objects.create(pk=1, name=u'The Author')
     169        author.books.create(name=u'The inline Book')
     170
     171        # Make sure both ForeignKey as well as ManyToMany inlines are properly removed
     172        response = self.client.get('/admin/admin_inlines/author/add/')
     173        # This would be a TabularInline
     174        self.assertNotContains(response, '<h2>Author-book relationships</h2>')
     175        self.assertNotContains(response, 'Add another Author-Book Relationship')
     176        self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
     177        response = self.client.get('/admin/admin_inlines/author/1/')
     178        self.assertNotContains(response, '<h2>Author-book relationships</h2>')
     179        self.assertNotContains(response, 'Add another Author-Book Relationship')
     180        self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
     181
     182        response = self.client.get('/admin/admin_inlines/holder/add/')
     183        # This would be a StackedInline
     184        self.assertNotContains(response, '<h2>Inners</h2>')
     185        self.assertNotContains(response, 'Add another Inner')
     186        self.assertNotContains(response, 'id="id_inner_set-TOTAL_FORMS"')
     187        response = self.client.get(self.change_url)
     188        self.assertNotContains(response, '<h2>Inners</h2>')
     189        self.assertNotContains(response, 'Add another Inner')
     190        self.assertNotContains(response, 'id="id_inner_set-TOTAL_FORMS"')
     191
     192        # Now let's add the missing add permissions and make sure the inlines are shown
     193        permission = Permission.objects.get(codename='add_book', content_type=book_ct)
     194        user.user_permissions.add(permission)
     195        permission = Permission.objects.get(codename='add_inner', content_type=inner_ct)
     196        user.user_permissions.add(permission)
     197
     198        response = self.client.get('/admin/admin_inlines/author/add/')
     199        self.assertContains(response, '<h2>Author-book relationships</h2>')
     200        self.assertContains(response, 'Add another Author-Book Relationship')
     201        self.assertContains(response, 'value="3" id="id_Author_books-TOTAL_FORMS"')
     202        response = self.client.get('/admin/admin_inlines/holder/add/')
     203        self.assertContains(response, '<h2>Inners</h2>')
     204        self.assertContains(response, 'Add another Inner')
     205        self.assertContains(response, 'value="3" id="id_inner_set-TOTAL_FORMS"')
     206
     207        # The inlines should be in the change view as well, but existing data
     208        # should not be shown
     209        response = self.client.get('/admin/admin_inlines/author/1/')
     210        self.assertContains(response, '<h2>Author-book relationships</h2>')
     211        self.assertContains(response, 'Add another Author-Book Relationship')
     212        self.assertContains(response, 'value="3" id="id_Author_books-TOTAL_FORMS"')
     213        self.assertNotContains(response, '<input type="hidden" name="Author_books-0-id" value="1"')
     214        response = self.client.get(self.change_url)
     215        self.assertContains(response, '<h2>Inners</h2>')
     216        self.assertContains(response, 'Add another Inner')
     217        self.assertContains(response, 'value="3" id="id_inner_set-TOTAL_FORMS"')
     218        self.assertNotContains(response, '<input type="hidden" name="inner_set-0-id" value="1"')
     219
     220        # Add the change permissions and check that existing data is shown.
     221        permission = Permission.objects.get(codename='change_book', content_type=book_ct)
     222        user.user_permissions.add(permission)
     223        permission = Permission.objects.get(codename='change_inner', content_type=inner_ct)
     224        user.user_permissions.add(permission)
     225        response = self.client.get('/admin/admin_inlines/author/1/')
     226        self.assertContains(response, '<input type="hidden" name="Author_books-0-id" value="1"')
     227        # Deletion should not be possible.
     228        self.assertNotContains(response, 'id="id_Author_books-0-DELETE"')
     229        response = self.client.get(self.change_url)
     230        self.assertContains(response, '<input type="hidden" name="inner_set-0-id" value="1"')
     231
     232        # Remove the add permissions. inlines should still be there, but
     233        # no possibility to add data
     234        permission = Permission.objects.get(codename='add_book', content_type=book_ct)
     235        user.user_permissions.remove(permission)
     236        permission = Permission.objects.get(codename='add_inner', content_type=inner_ct)
     237        user.user_permissions.remove(permission)
     238        response = self.client.get('/admin/admin_inlines/author/1/')
     239        self.assertContains(response, '<h2>Author-book relationships</h2>')
     240        self.assertContains(response, '<input type="hidden" name="Author_books-0-id" value="1"')
     241        self.assertContains(response, 'value="1" id="id_Author_books-TOTAL_FORMS"')
     242        response = self.client.get(self.change_url)
     243        self.assertContains(response, '<h2>Inners</h2>')
     244        self.assertContains(response, '<input type="hidden" name="inner_set-0-id" value="1"')
     245        self.assertContains(response, 'value="1" id="id_inner_set-TOTAL_FORMS"')
     246
     247        # Check that deletion is possible with the appropriate permissions.
     248        # Deletion is only possible for the Author-Book relationship since the
     249        # foreign key from Inner to Holder does not allow NULL values.
     250        permission = Permission.objects.get(codename='delete_book', content_type=book_ct)
     251        user.user_permissions.add(permission)
     252        response = self.client.get('/admin/admin_inlines/author/1/')
     253        self.assertContains(response, 'id="id_Author_books-0-DELETE"')
    144254
    145255class TestInlineMedia(TestCase):
    146256    urls = "regressiontests.admin_inlines.urls"
Back to Top