Ticket #8060: admin_inline_permissions_v3.diff

File admin_inline_permissions_v3.diff, 24.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..ba806c4 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.none()
     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        if self.opts.auto_created:
     1437            # We're checking the rights to an auto-created intermediate model. As per
     1438            # the discussion on ticket #8060, the user needs to have the change permission
     1439            # for the related model in order to be able to do anything with the
     1440            # intermediate model.
     1441            return self.has_change_permission(request)
     1442        opts = self.get_permission_opts()
     1443        return request.user.has_perm(opts.app_label + '.' + opts.get_add_permission())
     1444
     1445    def has_change_permission(self, request, obj=None):
     1446        opts = self.get_permission_opts()
     1447        return request.user.has_perm(opts.app_label + '.' + opts.get_change_permission())
     1448
     1449    def has_delete_permission(self, request, obj=None):
     1450        if self.opts.auto_created:
     1451            # We're checking the rights to an auto-created intermediate model. As per
     1452            # the discussion on ticket #8060, the user needs to have the change permission
     1453            # for the related model in order to be able to do anything with the
     1454            # intermediate model.
     1455            return self.has_change_permission(request, obj)
     1456        opts = self.get_permission_opts()
     1457        return request.user.has_perm(opts.app_label + '.' + opts.get_delete_permission())
     1458
    14011459class StackedInline(InlineModelAdmin):
    14021460    template = 'admin/edit_inline/stacked.html'
    14031461
  • 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..ebfe787 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, TitleCollection, Title)
    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
    144 
    145145class TestInlineMedia(TestCase):
    146146    urls = "regressiontests.admin_inlines.urls"
    147147    fixtures = ['admin-views-users.xml']
    class TestInlineAdminForm(TestCase):  
    196196        iaf = InlineAdminForm(None, None, {}, {}, joe)
    197197        parent_ct = ContentType.objects.get_for_model(Parent)
    198198        self.assertEqual(iaf.original.content_type, parent_ct)
     199
     200class TestInlinePermissions(TestCase):
     201    """
     202    Make sure the admin respects permissions for objects that are edited
     203    inline. Ref #8060.
     204    """
     205    urls = "regressiontests.admin_inlines.urls"
     206    fixtures = ['admin-views-users.xml']
     207
     208    def setUp(self):
     209        self.user = User.objects.get(username='super')
     210        self.user.is_superuser = False
     211        self.user.save()
     212
     213        self.author_ct = ContentType.objects.get_for_model(Author)
     214        self.holder_ct = ContentType.objects.get_for_model(Holder)
     215        self.book_ct = ContentType.objects.get_for_model(Book)
     216        self.inner_ct = ContentType.objects.get_for_model(Inner)
     217
     218        author = Author.objects.create(pk=1, name=u'The Author')
     219        author.books.create(name=u'The inline Book')
     220        holder = Holder(dummy=13)
     221        holder.save()
     222        Inner(dummy=42, holder=holder).save()
     223        self.change_url = '/admin/admin_inlines/holder/%i/' % holder.id
     224
     225        result = self.client.login(username='super', password='secret')
     226        self.assertEqual(result, True)
     227
     228    def tearDown(self):
     229        self.client.logout()
     230
     231    def test_inline_add_m2m_noperm(self):
     232        user = self.user
     233        permission = Permission.objects.get(codename='add_author', content_type=self.author_ct)
     234        user.user_permissions.add(permission)
     235        # Make sure the inline is removed
     236        response = self.client.get('/admin/admin_inlines/author/add/')
     237        # This would be a TabularInline
     238        self.assertNotContains(response, '<h2>Author-book relationships</h2>')
     239        self.assertNotContains(response, 'Add another Author-Book Relationship')
     240        self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
     241
     242    def test_inline_add_fk_noperm(self):
     243        user = self.user
     244        permission = Permission.objects.get(codename='add_holder', content_type=self.holder_ct)
     245        user.user_permissions.add(permission)
     246        response = self.client.get('/admin/admin_inlines/holder/add/')
     247        # This would be a StackedInline
     248        self.assertNotContains(response, '<h2>Inners</h2>')
     249        self.assertNotContains(response, 'Add another Inner')
     250        self.assertNotContains(response, 'id="id_inner_set-TOTAL_FORMS"')
     251
     252    def test_inline_change_m2m_noperm(self):
     253        user = self.user
     254        permission = Permission.objects.get(codename='change_author', content_type=self.author_ct)
     255        user.user_permissions.add(permission)
     256        response = self.client.get('/admin/admin_inlines/author/1/')
     257        self.assertNotContains(response, '<h2>Author-book relationships</h2>')
     258        self.assertNotContains(response, 'Add another Author-Book Relationship')
     259        self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
     260
     261    def test_inline_change_fk_noperm(self):
     262        user = self.user
     263        permission = Permission.objects.get(codename='change_holder', content_type=self.holder_ct)
     264        user.user_permissions.add(permission)
     265        response = self.client.get(self.change_url)
     266        self.assertNotContains(response, '<h2>Inners</h2>')
     267        self.assertNotContains(response, 'Add another Inner')
     268        self.assertNotContains(response, 'id="id_inner_set-TOTAL_FORMS"')
     269
     270    def test_inline_add_m2m_add_perm(self):
     271        user = self.user
     272        permission = Permission.objects.get(codename='add_author', content_type=self.author_ct)
     273        user.user_permissions.add(permission)
     274        permission = Permission.objects.get(codename='add_book', content_type=self.book_ct)
     275        user.user_permissions.add(permission)
     276        response = self.client.get('/admin/admin_inlines/author/add/')
     277        self.assertNotContains(response, '<h2>Author-book relationships</h2>')
     278        self.assertNotContains(response, 'Add another Author-Book Relationship')
     279        self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
     280
     281    def test_inline_add_fk_add_perm(self):
     282        user = self.user
     283        permission = Permission.objects.get(codename='add_holder', content_type=self.holder_ct)
     284        user.user_permissions.add(permission)
     285        permission = Permission.objects.get(codename='add_inner', content_type=self.inner_ct)
     286        user.user_permissions.add(permission)
     287        response = self.client.get('/admin/admin_inlines/holder/add/')
     288        self.assertContains(response, '<h2>Inners</h2>')
     289        self.assertContains(response, 'Add another Inner')
     290        self.assertContains(response, 'value="3" id="id_inner_set-TOTAL_FORMS"')
     291
     292    def test_inline_change_m2m_add_perm(self):
     293        # We need the change permission on the related model to make changes to the
     294        # intermediate model.
     295        user = self.user
     296        permission = Permission.objects.get(codename='change_author', content_type=self.author_ct)
     297        user.user_permissions.add(permission)
     298        permission = Permission.objects.get(codename='add_book', content_type=self.book_ct)
     299        user.user_permissions.add(permission)
     300        response = self.client.get('/admin/admin_inlines/author/1/')
     301        self.assertNotContains(response, '<h2>Author-book relationships</h2>')
     302        self.assertNotContains(response, 'Add another Author-Book Relationship')
     303        self.assertNotContains(response, 'id="id_Author_books-TOTAL_FORMS"')
     304        self.assertNotContains(response, 'id="id_Author_books-0-DELETE"')
     305
     306    def test_inline_change_m2m_change_perm(self):
     307        # Editing the preexisting m2m relation as well as adding additional
     308        # ones should be possible.
     309        user = self.user
     310        permission = Permission.objects.get(codename='change_author', content_type=self.author_ct)
     311        user.user_permissions.add(permission)
     312        permission = Permission.objects.get(codename='change_book', content_type=self.book_ct)
     313        user.user_permissions.add(permission)
     314        response = self.client.get('/admin/admin_inlines/author/1/')
     315        self.assertContains(response, '<h2>Author-book relationships</h2>')
     316        self.assertContains(response, 'Add another Author-Book Relationship')
     317        self.assertContains(response, 'value="4" id="id_Author_books-TOTAL_FORMS"')
     318        self.assertContains(response, '<input type="hidden" name="Author_books-0-id" value="1"')
     319        self.assertContains(response, 'id="id_Author_books-0-DELETE"')
     320
     321    def test_inline_change_fk_add_perm(self):
     322        user = self.user
     323        permission = Permission.objects.get(codename='change_holder', content_type=self.holder_ct)
     324        user.user_permissions.add(permission)
     325        permission = Permission.objects.get(codename='add_inner', content_type=self.inner_ct)
     326        user.user_permissions.add(permission)
     327        response = self.client.get(self.change_url)
     328        self.assertContains(response, '<h2>Inners</h2>')
     329        self.assertContains(response, 'Add another Inner')
     330        self.assertContains(response, 'value="3" id="id_inner_set-TOTAL_FORMS"')
     331        self.assertNotContains(response, '<input type="hidden" name="inner_set-0-id" value="1"')
     332
     333    def test_inline_change_fk_change_perm(self):
     334        user = self.user
     335        permission = Permission.objects.get(codename='change_holder', content_type=self.holder_ct)
     336        user.user_permissions.add(permission)
     337        permission = Permission.objects.get(codename='change_inner', content_type=self.inner_ct)
     338        user.user_permissions.add(permission)
     339        response = self.client.get(self.change_url)
     340        self.assertContains(response, '<h2>Inners</h2>')
     341        self.assertContains(response, 'value="1" id="id_inner_set-TOTAL_FORMS"')
     342        self.assertContains(response, '<input type="hidden" name="inner_set-0-id" value="1"')
     343
     344    def test_inline_change_fk_add_change_perm(self):
     345        user = self.user
     346        permission = Permission.objects.get(codename='change_holder', content_type=self.holder_ct)
     347        user.user_permissions.add(permission)
     348        permission = Permission.objects.get(codename='add_inner', content_type=self.inner_ct)
     349        user.user_permissions.add(permission)
     350        permission = Permission.objects.get(codename='change_inner', content_type=self.inner_ct)
     351        user.user_permissions.add(permission)
     352        response = self.client.get(self.change_url)
     353        self.assertContains(response, '<h2>Inners</h2>')
     354        self.assertContains(response, 'value="4" id="id_inner_set-TOTAL_FORMS"')
     355        self.assertContains(response, '<input type="hidden" name="inner_set-0-id" value="1"')
     356
     357    def test_inline_change_fk_del_perm(self):
     358        # The Author ForeignKey in the Book model does not allow NULL values,
     359        # so we use different models this time.
     360        user = self.user
     361        collection = TitleCollection.objects.create(pk=1)
     362        title = Title.objects.create(collection=collection, title1='foo', title2='foo')
     363        collection_ct = ContentType.objects.get_for_model(TitleCollection)
     364        title_ct = ContentType.objects.get_for_model(Title)
     365        permission = Permission.objects.get(codename='change_titlecollection', content_type=collection_ct)
     366        user.user_permissions.add(permission)
     367        permission = Permission.objects.get(codename='change_title', content_type=title_ct)
     368        user.user_permissions.add(permission)
     369        permission = Permission.objects.get(codename='delete_title', content_type=title_ct)
     370        user.user_permissions.add(permission)
     371        response = self.client.get('/admin/admin_inlines/titlecollection/1/')
     372        self.assertContains(response, '<h2>Titles</h2>')
     373        self.assertContains(response, 'id="id_title_set-0-DELETE"')
Back to Top