Ticket #10609: 10609-1.patch
File 10609-1.patch, 16.3 KB (added by , 12 years ago) |
---|
-
django/contrib/admin/actions.py
diff --git a/django/contrib/admin/actions.py b/django/contrib/admin/actions.py index 2011017..b928a85 100644
a b 2 2 Built-in, globally-available admin actions. 3 3 """ 4 4 5 from django.contrib.auth import REDIRECT_FIELD_NAME 5 6 from django.core.exceptions import PermissionDenied 6 7 from django.contrib.admin import helpers 7 8 from django.contrib.admin.util import get_deleted_objects, model_ngettext 9 from django.contrib.auth.decorators import user_passes_test 8 10 from django.db import router 9 11 from django.template.response import TemplateResponse 10 12 from django.utils.encoding import force_text 11 13 from django.utils.translation import ugettext_lazy, ugettext as _ 12 14 15 16 def action_user_passes_test(test_func, login_url=None, 17 redirect_field_name=REDIRECT_FIELD_NAME): 18 """ 19 Decorator for admin actions that acts like user_passes_test view 20 decorator. It checks user permissions, redirecting to the login-page if 21 necessary. The test_func should be callable that takes the user object 22 and modeladmin object, returning True if user passes. 23 24 """ 25 def decorator(action_func): 26 def _wrapped_action(modeladmin, request, queryset): 27 if test_func(request.user, modeladmin): 28 return action_func(modeladmin, request, queryset) 29 else: 30 # Redirect to login page as user_passes_test do. 31 # Pass dummy function, since we already know the user don't 32 # have required permissions. 33 return user_passes_test(lambda: False, login_url, 34 redirect_field_name) 35 _wrapped_action.test_func = test_func 36 return _wrapped_action 37 return decorator 38 39 40 def action_permission_required(perm, login_url=None, raise_exception=False): 41 """ 42 Decorator for admin actions that acts like permission_required view 43 decorator. It checks user permissions, redirecting to the login-page if 44 necessary. 45 46 Permissions which can be used for multiple models can be specified 47 in glob pattern. 48 49 Usage:: 50 51 # Specific action for given application/model 52 @action_permission_required('poll.can_close_poll') 53 def close_poll_action(modeladmin, request, queryset): 54 ... 55 56 # Shared action for multiple applications/models 57 @action_permission_required('*.delete_*') 58 def delete_selected_action(...): 59 ... 60 61 """ 62 def check_perms(user, modeladmin): 63 # First check if the user has the permission (even anon users) 64 if perm.startswith('*'): 65 # Strip '*' from the beginning and from the end 66 partial_perm = ''.join([modeladmin.model._meta.app_label, perm[1:-1]]) 67 # If any of user perms match, return True 68 all_perms = user.get_all_permissions() 69 filter_func = lambda p: p.startswith(partial_perm) 70 if any(filter(filter_func, all_perms)): 71 return True 72 else: 73 if user.has_perm(perm): 74 return True 75 76 # In case the 403 handler should be called raise the exception 77 if raise_exception: 78 raise PermissionDenied 79 return False 80 return action_user_passes_test(check_perms, login_url) 81 82 83 @action_permission_required('*.delete_*') 13 84 def delete_selected(modeladmin, request, queryset): 14 85 """ 15 86 Default action which deletes the selected objects. -
django/contrib/admin/options.py
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index f4205f2..2586770 100644
a b class ModelAdmin(BaseModelAdmin): 594 594 595 595 # get_action might have returned None, so filter any of those out. 596 596 actions = filter(None, actions) 597 598 # filter actions which user don't have permission to trigger 599 def filter_forbidden(action_tuple): 600 action = action_tuple[0] 601 if hasattr(action, 'test_func'): 602 return action.test_func(request.user, self) 603 return True 604 actions = filter(filter_forbidden, actions) 597 605 598 606 # Convert the actions into a SortedDict keyed by name. 599 607 actions = SortedDict([ -
docs/ref/contrib/admin/actions.txt
diff --git a/docs/ref/contrib/admin/actions.txt b/docs/ref/contrib/admin/actions.txt index 3f3b529..9579bfa 100644
a b Conditionally enabling or disabling actions 348 348 del actions['delete_selected'] 349 349 return actions 350 350 351 Enabling admin actions for specific user permissions only 352 --------------------------------------------------------- 351 353 354 .. versionadded:: 1.5 355 356 Admin actions can be displayed depending on user permissions using 357 :func:`~action_permissions_required` decorator 358 from ``django.contrib.admin.actions`` module. 359 360 .. function:: action_permissions_required(permission, [redirect_field_name=REDIRECT_FIELD_NAME, login_url=None]) 361 362 *permission* is required permission, eg. polls.can_vote. 363 364 If you want to add shared action which is visible site-wide, use glob 365 pattern, \*.delete\_\*. Other variants (\*.detete_entry or poll.add\_\*) 366 are not valid. 367 368 .. function:: action_user_passes_test(test_func, [redirect_field_name=REDIRECT_FIELD_NAME, login_url=None]) 369 370 This is more general version of :func:`action_permissions_required`, where 371 *test_func* function accepts user and modeladmin instances. It returns 372 True, when action should be available and False otherwise. -
tests/regressiontests/admin_views/admin.py
diff --git a/tests/regressiontests/admin_views/admin.py b/tests/regressiontests/admin_views/admin.py index a5476e9..53f2314 100644
a b import os 6 6 7 7 from django import forms 8 8 from django.contrib import admin 9 from django.contrib.admin.actions import action_permission_required 9 10 from django.contrib.admin.views.main import ChangeList 10 11 from django.core.files.storage import FileSystemStorage 11 12 from django.core.mail import EmailMessage … … from .models import (Article, Chapter, Account, Media, Child, Parent, Picture, 27 28 Album, Question, Answer, ComplexSortedPerson, PrePopulatedPostLargeSlug, 28 29 AdminOrderedField, AdminOrderedModelMethod, AdminOrderedAdminMethod, 29 30 AdminOrderedCallable, Report, Color2, UnorderedObject, MainPrepopulated, 30 RelatedPrepopulated, UndeletableObject, Simple )31 RelatedPrepopulated, UndeletableObject, Simple, RestrictedActionsModel) 31 32 32 33 33 34 def callable_year(dt_value): … … class AttributeErrorRaisingAdmin(admin.ModelAdmin): 586 587 list_display = [callable_on_unknown, ] 587 588 588 589 590 class RestrictedActionsAdmin(admin.ModelAdmin): 591 actions = ['public_action', 'shared_delete_action', 'specific_add_action'] 592 593 def public_action(modeladmin, request, queryset): 594 return HttpResponse('Public action!') 595 596 @action_permission_required('*.delete_*') 597 def shared_delete_action(modeladmin, request, queryset): 598 return HttpResponse('Shared delete action!') 599 600 @action_permission_required('admin_views.add_restrictedactionsmodel') 601 def specific_add_action(modeladmin, request, queryset): 602 return HttpResponse('Specific add action!') 603 604 605 589 606 site = admin.AdminSite(name="admin") 590 607 site.register(Article, ArticleAdmin) 591 608 site.register(CustomArticle, CustomArticleAdmin) … … site.register(AdminOrderedAdminMethod, AdminOrderedAdminMethodAdmin) 660 677 site.register(AdminOrderedCallable, AdminOrderedCallableAdmin) 661 678 site.register(Color2, CustomTemplateFilterColorAdmin) 662 679 site.register(Simple, AttributeErrorRaisingAdmin) 680 site.register(RestrictedActionsModel, RestrictedActionsAdmin) 663 681 664 682 # Register core models we need in our tests 665 683 from django.contrib.auth.models import User, Group -
new file tests/regressiontests/admin_views/fixtures/admin-users-permissions.xml
diff --git a/tests/regressiontests/admin_views/fixtures/admin-users-permissions.xml b/tests/regressiontests/admin_views/fixtures/admin-users-permissions.xml new file mode 100644 index 0000000..0a8e8ff
- + 1 <?xml version="1.0" encoding="utf-8"?> 2 <django-objects version="1.0"> 3 <object pk="141" model="auth.user"> 4 <field type="CharField" name="username">anotheradduser</field> 5 <field type="CharField" name="first_name">Add</field> 6 <field type="CharField" name="last_name">User</field> 7 <field type="CharField" name="email">auser@example.com</field> 8 <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field> 9 <field type="BooleanField" name="is_staff">True</field> 10 <field type="BooleanField" name="is_active">True</field> 11 <field type="BooleanField" name="is_superuser">False</field> 12 <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field> 13 <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field> 14 <field to="auth.group" name="groups" rel="ManyToManyRel"></field> 15 <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field> 16 </object> 17 <object pk="142" model="auth.user"> 18 <field type="CharField" name="username">anotherdeleteuser</field> 19 <field type="CharField" name="first_name">Delete</field> 20 <field type="CharField" name="last_name">User</field> 21 <field type="CharField" name="email">duser@example.com</field> 22 <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field> 23 <field type="BooleanField" name="is_staff">True</field> 24 <field type="BooleanField" name="is_active">True</field> 25 <field type="BooleanField" name="is_superuser">False</field> 26 <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field> 27 <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field> 28 <field to="auth.group" name="groups" rel="ManyToManyRel"></field> 29 <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field> 30 </object> 31 <object pk="142" model="contenttypes.contenttype"> 32 <field type="CharField" name="name">anothermodel</field> 33 <field type="CharField" name="app_label">admin_views</field> 34 <field type="CharField" name="model">anothermodel</field> 35 </object> 36 <object pk="141" model="auth.permission"> 37 <field type="CharField" name="name">Can add AnotherModel</field> 38 <field type="CharField" name="codename">add_anothermodel</field> 39 <field to="contenttypes.contenttype" rel="ForeignKey" name="content_type">142</field> 40 </object> 41 <object pk="142" model="auth.permission"> 42 <field type="CharField" name="name">Can delete AnotherModel</field> 43 <field type="CharField" name="codename">delete_anothermodel</field> 44 <field to="contenttypes.contenttype" rel="ForeignKey" name="content_type">142</field> 45 </object> 46 <object pk="142" model="auth.user_user_permissions"> 47 <field to="auth.user" rel="ForeignKey" name="user">101</field> 48 <field to="auth.permissions" rel="ForeignKey" name="permission"> 49 <natural>add_restrictedactionsmodel</natural> 50 <natural>admin_views</natural> 51 <natural>restrictedactionsmodel</natural> 52 </field> 53 </object> 54 <object pk="143" model="auth.user_user_permissions"> 55 <field to="auth.user" rel="ForeignKey" name="user">103</field> 56 <field to="auth.permissions" rel="ForeignKey" name="permission"> 57 <natural>delete_restrictedactionsmodel</natural> 58 <natural>admin_views</natural> 59 <natural>restrictedactionsmodel</natural> 60 </field> 61 </object> 62 <object pk="144" model="auth.user_user_permissions"> 63 <field to="auth.user" rel="ForeignKey" name="user">141</field> 64 <field to="auth.permissions" rel="ForeignKey" name="permission">141</field> 65 </object> 66 <object pk="145" model="auth.user_user_permissions"> 67 <field to="auth.user" rel="ForeignKey" name="user">142</field> 68 <field to="auth.permissions" rel="ForeignKey" name="permission">142</field> 69 </object> 70 </django-objects> -
tests/regressiontests/admin_views/models.py
diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py index 2b14300..8a70a64 100644
a b class Simple(models.Model): 656 656 """ 657 657 Simple model with nothing on it for use in testing 658 658 """ 659 660 661 class RestrictedActionsModel(models.Model): 662 """ 663 Just another simple model to use with RestrictedActionsAdmin. 664 """ -
tests/regressiontests/admin_views/tests.py
diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index 72dc6a3..d9d4e66 100644
a b from django.contrib.contenttypes.models import ContentType 28 28 from django.forms.util import ErrorList 29 29 from django.template.response import TemplateResponse 30 30 from django.test import TestCase 31 from django.test.client import RequestFactory 31 32 from django.utils import formats, translation, unittest 32 33 from django.utils.cache import get_max_age 33 34 from django.utils.encoding import iri_to_uri, force_bytes … … from .models import (Article, BarAccount, CustomArticle, EmptyModel, FooAccount, 46 47 OtherStory, ComplexSortedPerson, Parent, Child, AdminOrderedField, 47 48 AdminOrderedModelMethod, AdminOrderedAdminMethod, AdminOrderedCallable, 48 49 Report, MainPrepopulated, RelatedPrepopulated, UnorderedObject, 49 Simple, UndeletableObject) 50 Simple, UndeletableObject, RestrictedActionsModel) 51 52 from .admin import site 50 53 51 54 52 55 ERROR_MESSAGE = "Please enter the correct username and password \ … … class AdminViewLogoutTest(TestCase): 3693 3696 self.assertEqual(response.template_name, 'admin/login.html') 3694 3697 self.assertEqual(response.request['PATH_INFO'], '/test_admin/admin/') 3695 3698 self.assertContains(response, '<input type="hidden" name="next" value="/test_admin/admin/" />') 3699 3700 class AdminActionPermissionsTest(TestCase): 3701 """ Test permissions for admin action. 3702 3703 Users: 3704 - adduser - has admin_views.add_restrictedactionsmodel permission 3705 - deleteuser - has admin_views.delete_restrictedactionsmodel permission 3706 - addanotheruser - has admin_views.add_anothermodel permission 3707 - anotherdeleteuser - has admin_views.delete_anothermodel permission 3708 3709 Actions: 3710 - public action - visible for everyone (default) 3711 - shared_delete_action - visible for any delete_* permission 3712 - specific_add_action - visible for admin_views.add_restrictedactionsmodel 3713 3714 """ 3715 #urls = "regressiontests.admin_views.urls" 3716 fixtures = ['admin-views-users.xml', 'admin-users-permissions.xml'] 3717 3718 def setUp(self): 3719 self.factory = RequestFactory() 3720 self.modeladmin = site._registry[RestrictedActionsModel] 3721 self.dummy_request = self.factory.get('/') 3722 3723 def test_get_actions_as_superuser(self): 3724 """ 3725 Superuser should see all actions. 3726 """ 3727 request = self.dummy_request 3728 3729 request.user = User.objects.get(username='super') 3730 actions = ['delete_selected', 'public_action', 'shared_delete_action', 3731 'specific_add_action'] 3732 self.assertEqual(actions, self.modeladmin.get_actions(request).keys()) 3733 3734 def test_shared_actions(self): 3735 request = self.dummy_request 3736 3737 request.user = User.objects.get(username='deleteuser') 3738 actions = ['delete_selected', 'public_action', 'shared_delete_action'] 3739 self.assertEqual(actions, self.modeladmin.get_actions(request).keys()) 3740 3741 request.user = User.objects.get(username='anotherdeleteuser') 3742 self.assertEqual(actions, self.modeladmin.get_actions(request).keys()) 3743 3744 def test_specific_actions(self): 3745 request = self.dummy_request 3746 3747 request.user = User.objects.get(username='adduser') 3748 actions = ['public_action', 'specific_add_action'] 3749 self.assertEqual(actions, self.modeladmin.get_actions(request).keys()) 3750 3751 request.user = User.objects.get(username='anotheradduser') 3752 actions = ['public_action'] 3753 self.assertEqual(actions, self.modeladmin.get_actions(request).keys())