Ticket #8060: admin_inline_permissions_v3.diff
File admin_inline_permissions_v3.diff, 24.8 KB (added by , 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): 270 270 clean_lookup = LOOKUP_SEP.join(parts) 271 271 return clean_lookup in self.list_filter or clean_lookup == self.date_hierarchy 272 272 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()) 273 308 274 309 class ModelAdmin(BaseModelAdmin): 275 310 "Encapsulates all admin options and functionality for a given model." … … class ModelAdmin(BaseModelAdmin): 307 342 self.model = model 308 343 self.opts = model._meta 309 344 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)314 345 if 'action_checkbox' not in self.list_display and self.actions is not None: 315 346 self.list_display = ['action_checkbox'] + list(self.list_display) 316 347 if not self.list_display_links: … … class ModelAdmin(BaseModelAdmin): 320 351 break 321 352 super(ModelAdmin, self).__init__() 322 353 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 323 372 def get_urls(self): 324 373 from django.conf.urls import patterns, url 325 374 … … class ModelAdmin(BaseModelAdmin): 369 418 js.extend(['getElementsBySelector.js', 'dom-drag.js' , 'admin/ordering.js']) 370 419 return forms.Media(js=[static('admin/js/%s' % url) for url in js]) 371 420 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.opts378 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 given383 Django model instance, the default implementation doesn't examine the384 `obj` parameter.385 386 Can be overriden by the user in subclasses. In such case it should387 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 given389 request has permission to change *any* object of the given type.390 """391 opts = self.opts392 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 given397 Django model instance, the default implementation doesn't examine the398 `obj` parameter.399 400 Can be overriden by the user in subclasses. In such case it should401 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 given403 request has permission to delete *any* object of the given type.404 """405 opts = self.opts406 return request.user.has_perm(opts.app_label + '.' + opts.get_delete_permission())407 408 421 def get_model_perms(self, request): 409 422 """ 410 423 Returns a dict of all perms for this model. This dict has the keys … … class ModelAdmin(BaseModelAdmin): 500 513 fields=self.list_editable, **defaults) 501 514 502 515 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): 504 518 yield inline.get_formset(request, obj) 505 519 506 520 def get_paginator(self, request, queryset, per_page, orphans=0, allow_empty_first_page=True): … … class ModelAdmin(BaseModelAdmin): 914 928 915 929 ModelForm = self.get_form(request) 916 930 formsets = [] 931 inline_instances = self.get_inline_instances(request, 'add') 917 932 if request.method == 'POST': 918 933 form = ModelForm(request.POST, request.FILES) 919 934 if form.is_valid(): … … class ModelAdmin(BaseModelAdmin): 923 938 form_validated = False 924 939 new_object = self.model() 925 940 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): 927 942 prefix = FormSet.get_default_prefix() 928 943 prefixes[prefix] = prefixes.get(prefix, 0) + 1 929 944 if prefixes[prefix] != 1 or not prefix: … … class ModelAdmin(BaseModelAdmin): 951 966 initial[k] = initial[k].split(",") 952 967 form = ModelForm(initial=initial) 953 968 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): 956 970 prefix = FormSet.get_default_prefix() 957 971 prefixes[prefix] = prefixes.get(prefix, 0) + 1 958 972 if prefixes[prefix] != 1 or not prefix: … … class ModelAdmin(BaseModelAdmin): 968 982 media = self.media + adminForm.media 969 983 970 984 inline_admin_formsets = [] 971 for inline, formset in zip( self.inline_instances, formsets):985 for inline, formset in zip(inline_instances, formsets): 972 986 fieldsets = list(inline.get_fieldsets(request)) 973 987 readonly = list(inline.get_readonly_fields(request)) 974 988 prepopulated = dict(inline.get_prepopulated_fields(request)) … … class ModelAdmin(BaseModelAdmin): 1012 1026 1013 1027 ModelForm = self.get_form(request, obj) 1014 1028 formsets = [] 1029 inline_instances = self.get_inline_instances(request, 'change') 1015 1030 if request.method == 'POST': 1016 1031 form = ModelForm(request.POST, request.FILES, instance=obj) 1017 1032 if form.is_valid(): … … class ModelAdmin(BaseModelAdmin): 1021 1036 form_validated = False 1022 1037 new_object = obj 1023 1038 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): 1026 1040 prefix = FormSet.get_default_prefix() 1027 1041 prefixes[prefix] = prefixes.get(prefix, 0) + 1 1028 1042 if prefixes[prefix] != 1 or not prefix: … … class ModelAdmin(BaseModelAdmin): 1043 1057 else: 1044 1058 form = ModelForm(instance=obj) 1045 1059 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): 1047 1061 prefix = FormSet.get_default_prefix() 1048 1062 prefixes[prefix] = prefixes.get(prefix, 0) + 1 1049 1063 if prefixes[prefix] != 1 or not prefix: … … class ModelAdmin(BaseModelAdmin): 1059 1073 media = self.media + adminForm.media 1060 1074 1061 1075 inline_admin_formsets = [] 1062 for inline, formset in zip( self.inline_instances, formsets):1076 for inline, formset in zip(inline_instances, formsets): 1063 1077 fieldsets = list(inline.get_fieldsets(request, obj)) 1064 1078 readonly = list(inline.get_readonly_fields(request, obj)) 1065 1079 prepopulated = dict(inline.get_prepopulated_fields(request, obj)) … … class InlineModelAdmin(BaseModelAdmin): 1377 1391 # if exclude is an empty list we use None, since that's the actual 1378 1392 # default 1379 1393 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) 1380 1397 defaults = { 1381 1398 "form": self.form, 1382 1399 "formset": self.formset, … … class InlineModelAdmin(BaseModelAdmin): 1386 1403 "formfield_callback": partial(self.formfield_for_dbfield, request=request), 1387 1404 "extra": self.extra, 1388 1405 "max_num": self.max_num, 1389 "can_delete": self.can_delete,1406 "can_delete": can_delete, 1390 1407 } 1391 1408 defaults.update(kwargs) 1392 1409 return inlineformset_factory(self.parent_model, self.model, **defaults) … … class InlineModelAdmin(BaseModelAdmin): 1398 1415 fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj)) 1399 1416 return [(None, {'fields': fields})] 1400 1417 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 1401 1459 class StackedInline(InlineModelAdmin): 1402 1460 template = 'admin/edit_inline/stacked.html' 1403 1461 -
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): 411 411 # GenericInlineModelAdmin doesn't define its own. 412 412 exclude.extend(self.form._meta.exclude) 413 413 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) 414 417 defaults = { 415 418 "ct_field": self.ct_field, 416 419 "fk_field": self.ct_fk_field, … … class GenericInlineModelAdmin(InlineModelAdmin): 418 421 "formfield_callback": partial(self.formfield_for_dbfield, request=request), 419 422 "formset": self.formset, 420 423 "extra": self.extra, 421 "can_delete": self.can_delete,424 "can_delete": can_delete, 422 425 "can_order": False, 423 426 "fields": fields, 424 427 "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 1 1 from django.contrib.admin.helpers import InlineAdminForm 2 from django.contrib.auth.models import User, Permission 2 3 from django.contrib.contenttypes.models import ContentType 3 4 from django.test import TestCase 4 5 5 6 # local test models 6 7 from models import (Holder, Inner, Holder2, Inner2, Holder3, 7 8 Inner3, Person, OutfitItem, Fashionista, Teacher, Parent, Child, 8 CapoFamiglia, Consigliere, SottoCapo)9 Author, Book, TitleCollection, Title) 9 10 from admin import InnerInline 10 11 11 12 … … class TestInline(TestCase): 141 142 '<input id="id_-2-0-name" type="text" class="vTextField" ' 142 143 'name="-2-0-name" maxlength="100" />') 143 144 144 145 145 class TestInlineMedia(TestCase): 146 146 urls = "regressiontests.admin_inlines.urls" 147 147 fixtures = ['admin-views-users.xml'] … … class TestInlineAdminForm(TestCase): 196 196 iaf = InlineAdminForm(None, None, {}, {}, joe) 197 197 parent_ct = ContentType.objects.get_for_model(Parent) 198 198 self.assertEqual(iaf.original.content_type, parent_ct) 199 200 class 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"')