From 31456e366ded80d777ca4e18a22ea2661b6f6135 Mon Sep 17 00:00:00 2001 From: powderflask Date: Wed, 20 Sep 2017 13:55:47 -0700 Subject: [PATCH 1/5] Add permission labels (permission.name) when permissions are created. See: https://github.com/vintasoftware/django-role-permissions/pull/71 --- rolepermissions/permissions.py | 6 ++---- rolepermissions/roles.py | 19 +++++++++++++------ rolepermissions/tests/test_roles.py | 17 +++++++++++++++++ rolepermissions/utils.py | 7 +++++++ 4 files changed, 39 insertions(+), 10 deletions(-) diff --git a/rolepermissions/permissions.py b/rolepermissions/permissions.py index 8a8acb7..40c61f5 100644 --- a/rolepermissions/permissions.py +++ b/rolepermissions/permissions.py @@ -6,7 +6,7 @@ from rolepermissions.exceptions import ( RolePermissionScopeException, CheckerNotRegistered) -from rolepermissions.roles import get_user_roles +from rolepermissions.roles import get_user_roles, get_or_create_permission class PermissionsManager(object): @@ -37,9 +37,7 @@ def fuction_decorator(func): def get_permission(permission_name): """Get a Permission object from a permission name.""" - user_ct = ContentType.objects.get_for_model(get_user_model()) - permission, _created = Permission.objects.get_or_create( - content_type=user_ct, codename=permission_name) + permission, created = get_or_create_permission(permission_name) return permission diff --git a/rolepermissions/roles.py b/rolepermissions/roles.py index b669f9c..f2384d5 100644 --- a/rolepermissions/roles.py +++ b/rolepermissions/roles.py @@ -8,7 +8,7 @@ from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType -from rolepermissions.utils import camelToSnake +from rolepermissions.utils import camelToSnake, camel_or_snake_to_title from rolepermissions.exceptions import RoleDoesNotExist @@ -158,11 +158,11 @@ def get_or_create_permissions(cls, permission_names): permissions = list(Permission.objects.filter( content_type=user_ct, codename__in=permission_names).all()) - if len(permissions) != len(permission_names): - for permission_name in permission_names: - permission, created = Permission.objects.get_or_create( - content_type=user_ct, codename=permission_name) - if created: + missing_permissions = set(permission_names) - set((p.codename for p in permissions)) + if len(missing_permissions) > 0: + for permission_name in missing_permissions: + permission, created = get_or_create_permission(permission_name) + if created: # assert created is True permissions.append(permission) return permissions @@ -172,6 +172,13 @@ def get_default(cls, permission_name): return cls.available_permissions[permission_name] +def get_or_create_permission(permission_name): + """Get a Permission object from a permission name.""" + user_ct = ContentType.objects.get_for_model(get_user_model()) + return Permission.objects.get_or_create( + content_type=user_ct, codename=permission_name, name=camel_or_snake_to_title(permission_name)) + + def retrieve_role(role_name): """Get a Role object from a role name.""" return RolesManager.retrieve_role(role_name) diff --git a/rolepermissions/tests/test_roles.py b/rolepermissions/tests/test_roles.py index 10d8453..3d00663 100644 --- a/rolepermissions/tests/test_roles.py +++ b/rolepermissions/tests/test_roles.py @@ -29,6 +29,11 @@ class RolRole3(AbstractUserRole): 'permission6': False, } +class RolRole4(AbstractUserRole): + available_permissions = { + 'permission_number_7': True, + 'PermissionNumber8': True, + } class AbstractUserRoleTests(TestCase): @@ -130,6 +135,18 @@ def test_permission_names_list(self): self.assertIn('permission3', RolRole2.permission_names_list()) self.assertIn('permission4', RolRole2.permission_names_list()) + def test_permission_labels(self): + user = mommy.make(get_user_model()) + + RolRole4.assign_role_to_user(user) + permissions = user.user_permissions.all() + + permission_labels = [perm.name for perm in permissions] + + self.assertIn('Permission Number 7', permission_labels) + self.assertIn('Permission Number8', permission_labels) + self.assertEquals(len(permissions), 2) + class RolesManagerTests(TestCase): diff --git a/rolepermissions/utils.py b/rolepermissions/utils.py index dde3bd4..eebfa48 100644 --- a/rolepermissions/utils.py +++ b/rolepermissions/utils.py @@ -24,3 +24,10 @@ def camelToSnake(s): subbed = _underscorer1.sub(r'\1_\2', s) return _underscorer2.sub(r'\1_\2', subbed).lower() + + +def snake_to_title(s) : + return ' '.join(x.capitalize() for x in s.split('_')) + +def camel_or_snake_to_title(s): + return snake_to_title(camelToSnake(s)) From 89131027421fe81b87243b3d97cb625ca8764a8b Mon Sep 17 00:00:00 2001 From: powderflask Date: Thu, 21 Sep 2017 01:04:27 -0700 Subject: [PATCH 2/5] Add custom UserAdmin mixin to sync user's roles to user.groups Add sync_roles management command to sync auth.Group to UserRoles --- rolepermissions/admin.py | 31 ++++++- rolepermissions/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../management/commands/sync_roles.py | 27 ++++++ rolepermissions/roles.py | 4 + rolepermissions/tests/test_admin.py | 91 +++++++++++++++++++ 6 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 rolepermissions/management/__init__.py create mode 100644 rolepermissions/management/commands/__init__.py create mode 100644 rolepermissions/management/commands/sync_roles.py create mode 100644 rolepermissions/tests/test_admin.py diff --git a/rolepermissions/admin.py b/rolepermissions/admin.py index 23abb18..87206f4 100644 --- a/rolepermissions/admin.py +++ b/rolepermissions/admin.py @@ -1,2 +1,31 @@ +from django.conf import settings +from django.contrib import admin, auth +from django.contrib.auth.admin import UserAdmin +from rolepermissions import roles -from django.contrib import admin +ROLEPERMISSIONS_REGISTER_ADMIN = getattr(settings, 'ROLEPERMISSIONS_REGISTER_ADMIN', False) +UserModel = auth.get_user_model() + + +class RolePermissionsUserAdminMixin(object): + """ Must be mixed in with an UserAdmin class""" + def save_related(self, request, form, formsets, change): + super(RolePermissionsUserAdminMixin, self).save_related(request, form, formsets, change) + # re-load and take a copy of user's newly saved roles + user = UserModel.objects.get(pk=form.instance.pk) + groups = list(user.groups.all()) + # reset user's roles to match the list of groups just saved + roles.clear_roles(user) + for g in groups : + try : + roles.assign_role(user, g.name) + except roles.RoleDoesNotExist : + pass + + +class RolePermissionsUserAdmin(RolePermissionsUserAdminMixin, UserAdmin): + pass + +if ROLEPERMISSIONS_REGISTER_ADMIN: + admin.site.unregister(UserModel) + admin.site.register(UserModel, RolePermissionsUserAdmin) diff --git a/rolepermissions/management/__init__.py b/rolepermissions/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rolepermissions/management/commands/__init__.py b/rolepermissions/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rolepermissions/management/commands/sync_roles.py b/rolepermissions/management/commands/sync_roles.py new file mode 100644 index 0000000..d97e132 --- /dev/null +++ b/rolepermissions/management/commands/sync_roles.py @@ -0,0 +1,27 @@ +from django.conf import settings +from django.core.management.base import BaseCommand +from django.contrib.auth.models import Group +from django.contrib.auth import get_user_model +from rolepermissions import roles + +UserModel = get_user_model() + +class Command(BaseCommand): + ROLEPERMISSIONS_MODULE = getattr(settings, 'ROLEPERMISSIONS_MODULE', 'roles.py') + help = "Synchronize auth Groups and Permissions with UserRoles defined in %s."%ROLEPERMISSIONS_MODULE + + def handle(self, *args, **options): + # Sync auth.Group with current registered roles (leaving existing groups intact!) + for role in roles.RolesManager.get_roles() : + group, created = Group.objects.get_or_create(name=role.get_name()) + if created: + self.stdout.write("Created Group: %s from Role: %s"%(group.name, role.get_name())) + # Sync auth.Permission with permissions for this role + role.get_default_true_permissions() + + # Push any permission changes made to roles and remove any unregistered roles from all auth.Users + for user in UserModel.objects.all(): + user_roles = roles.get_user_roles(user) + roles.clear_roles(user) + for role in user_roles: + roles.assign_role(user, role) diff --git a/rolepermissions/roles.py b/rolepermissions/roles.py index f2384d5..20dbd98 100644 --- a/rolepermissions/roles.py +++ b/rolepermissions/roles.py @@ -29,6 +29,10 @@ def retrieve_role(cls, role_name): def get_roles_names(cls): return registered_roles.keys() + @classmethod + def get_roles(cls): + return registered_roles.values() + class RolesClassRegister(type): diff --git a/rolepermissions/tests/test_admin.py b/rolepermissions/tests/test_admin.py new file mode 100644 index 0000000..d871dcd --- /dev/null +++ b/rolepermissions/tests/test_admin.py @@ -0,0 +1,91 @@ +from collections import namedtuple +from django.core.management import call_command +from django.test import TestCase +from django.utils.six import StringIO +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group, Permission + +from model_mommy import mommy + +from rolepermissions.roles import AbstractUserRole, get_user_roles +from rolepermissions.admin import RolePermissionsUserAdminMixin + + +class AdminRole1(AbstractUserRole): + available_permissions = { + 'admin_perm1': True, + 'admin_perm2': False, + } + + +class UserAdminMixinTest(TestCase): + class UserAdminMock: + def save_related(self, request, form, formsets, change) : + pass + class CustomUserAdminMock(RolePermissionsUserAdminMixin, UserAdminMock): + pass + + FormMock = namedtuple('FormMock', ['instance', ]) + + def setup(self): + pass + + def test_admin_save_related_syncs_roles(self): + user = mommy.make(get_user_model()) + grp1 = mommy.make(Group) + grp2 = mommy.make(Group, name=AdminRole1.get_name()) + user.groups.add(grp1) + user.groups.add(grp2) + form = self.FormMock(instance=user) + self.CustomUserAdminMock().save_related(None, form, None, None) + user_roles = get_user_roles(user) + self.assertNotIn(grp1.name, (role.get_name() for role in user_roles)) + self.assertIn(AdminRole1, user_roles) + + +class SyncRolesTest(TestCase): + + def setup(self): + pass + + def test_sync_group(self): + out = StringIO() + call_command('sync_roles', stdout=out) + self.assertIn('Created Group: %s'%AdminRole1.get_name(), out.getvalue()) + group_names = [group['name'] for group in Group.objects.all().values('name')] + self.assertIn(AdminRole1.get_name(), group_names) + + def test_sync_permissions(self): + out = StringIO() + call_command('sync_roles', stdout=out) + permissions = [perm['codename'] for perm in Permission.objects.all().values('codename')] + self.assertIn('admin_perm1', permissions) + self.assertNotIn('admin_perm2', permissions) + + def test_sync_user_role_permissions(self): + user = mommy.make(get_user_model()) + grp1 = mommy.make(Group) + grp2 = mommy.make(Group, name=AdminRole1.get_name()) + user.groups.add(grp1) + user.groups.add(grp2) + out = StringIO() + call_command('sync_roles', stdout=out) + + user_group_names = [group['name'] for group in user.groups.all().values('name')] + self.assertIn(grp1.name, user_group_names) + self.assertIn(grp2.name, user_group_names) + + user_permission_names = [perm['codename'] for perm in user.user_permissions.all().values('codename')] + self.assertIn('admin_perm1', user_permission_names) + self.assertNotIn('admin_perm2', user_permission_names) + + def test_sync_preserves_groups(self): + grp1 = mommy.make(Group) + grp2 = mommy.make(Group, name=AdminRole1.get_name()) + out = StringIO() + call_command('sync_roles', stdout=out) + group_names = [group['name'] for group in Group.objects.all().values('name')] + self.assertIn(grp1.name, group_names) + self.assertIn(grp2.name, group_names) + + From e2a783cbe9bf8f32fd0fa6f1c4cb1fb021f5d37d Mon Sep 17 00:00:00 2001 From: powderflask Date: Tue, 17 Oct 2017 21:57:18 -0700 Subject: [PATCH 3/5] PR #72 - improve django-admin integration, suggestion from @filipeximenes --- rolepermissions/admin.py | 27 +++++++++----- .../management/commands/sync_roles.py | 30 ++++++++++++---- rolepermissions/roles.py | 16 ++++++--- rolepermissions/tests/test_admin.py | 2 +- rolepermissions/tests/test_roles.py | 35 ++++++++++++++++++- rolepermissions/tests/test_utils.py | 22 ++++++++++++ 6 files changed, 110 insertions(+), 22 deletions(-) create mode 100644 rolepermissions/tests/test_utils.py diff --git a/rolepermissions/admin.py b/rolepermissions/admin.py index 87206f4..f9cf2ce 100644 --- a/rolepermissions/admin.py +++ b/rolepermissions/admin.py @@ -1,5 +1,6 @@ from django.conf import settings from django.contrib import admin, auth +from django.contrib.auth.models import Group from django.contrib.auth.admin import UserAdmin from rolepermissions import roles @@ -10,16 +11,24 @@ class RolePermissionsUserAdminMixin(object): """ Must be mixed in with an UserAdmin class""" def save_related(self, request, form, formsets, change): - super(RolePermissionsUserAdminMixin, self).save_related(request, form, formsets, change) - # re-load and take a copy of user's newly saved roles user = UserModel.objects.get(pk=form.instance.pk) - groups = list(user.groups.all()) - # reset user's roles to match the list of groups just saved - roles.clear_roles(user) - for g in groups : - try : - roles.assign_role(user, g.name) - except roles.RoleDoesNotExist : + old_user_roles = set(r.get_name() for r in roles.get_user_roles(user)) + super(RolePermissionsUserAdminMixin, self).save_related(request, form, formsets, change) + + new_user_groups = set(g.name for g in user.groups.all()) + + for role_name in (old_user_roles - new_user_groups): # roles removed from User's groups + try: # put the recently removed group back, let rolepermissions remove it... + group = Group.objects.get(name=role_name) + user.groups.add(group) + except Group.DoesNotExist: + pass + roles.remove_role(user, role_name) + + for group_name in (new_user_groups - old_user_roles): # groups potentially added to User's roles + try: + roles.assign_role(user, group_name) + except roles.RoleDoesNotExist: pass diff --git a/rolepermissions/management/commands/sync_roles.py b/rolepermissions/management/commands/sync_roles.py index d97e132..db5eecb 100644 --- a/rolepermissions/management/commands/sync_roles.py +++ b/rolepermissions/management/commands/sync_roles.py @@ -4,11 +4,24 @@ from django.contrib.auth import get_user_model from rolepermissions import roles -UserModel = get_user_model() class Command(BaseCommand): ROLEPERMISSIONS_MODULE = getattr(settings, 'ROLEPERMISSIONS_MODULE', 'roles.py') help = "Synchronize auth Groups and Permissions with UserRoles defined in %s."%ROLEPERMISSIONS_MODULE + version = "1.0.0" + + def get_version(self): + return self.version + + def add_arguments(self, parser): + # Optional argument + parser.add_argument( + '--reset_user_permissions', + action='store_true', + dest='reset_user_permissions', + default=False, + help='Re-assign all User roles -- resets user Permissions to defaults defined by role(s) !! CAUTION !!', + ) def handle(self, *args, **options): # Sync auth.Group with current registered roles (leaving existing groups intact!) @@ -19,9 +32,12 @@ def handle(self, *args, **options): # Sync auth.Permission with permissions for this role role.get_default_true_permissions() - # Push any permission changes made to roles and remove any unregistered roles from all auth.Users - for user in UserModel.objects.all(): - user_roles = roles.get_user_roles(user) - roles.clear_roles(user) - for role in user_roles: - roles.assign_role(user, role) + if options.get('reset_user_permissions', False): # dj1.7 compat + # Push any permission changes made to roles and remove any unregistered roles from all auth.Users + self.stdout.write("Resetting permissions for ALL Users to defaults defined by roles.") + + for user in get_user_model().objects.all(): + user_roles = roles.get_user_roles(user=user) + roles.clear_roles(user=user) + for role in user_roles: + roles.assign_role(user=user, role=role) diff --git a/rolepermissions/roles.py b/rolepermissions/roles.py index 20dbd98..09aa0bd 100644 --- a/rolepermissions/roles.py +++ b/rolepermissions/roles.py @@ -176,11 +176,19 @@ def get_default(cls, permission_name): return cls.available_permissions[permission_name] -def get_or_create_permission(permission_name): - """Get a Permission object from a permission name.""" +def get_or_create_permission(codename, name=camel_or_snake_to_title): + """ + Get a Permission object from a permission name. + @:param codename: permission code name + @:param name: human-readable permissions name (str) or callable that takes codename as argument and returns str + """ user_ct = ContentType.objects.get_for_model(get_user_model()) - return Permission.objects.get_or_create( - content_type=user_ct, codename=permission_name, name=camel_or_snake_to_title(permission_name)) + # Careful here - don't use name to lookup existing permissions + try: + return Permission.objects.get(content_type=user_ct, codename=codename), False + except Permission.DoesNotExist: + perm_name = name(codename) if callable(name) else name + return Permission.objects.create(content_type=user_ct, codename=codename, name=perm_name), True def retrieve_role(role_name): diff --git a/rolepermissions/tests/test_admin.py b/rolepermissions/tests/test_admin.py index d871dcd..8d74a78 100644 --- a/rolepermissions/tests/test_admin.py +++ b/rolepermissions/tests/test_admin.py @@ -69,7 +69,7 @@ def test_sync_user_role_permissions(self): user.groups.add(grp1) user.groups.add(grp2) out = StringIO() - call_command('sync_roles', stdout=out) + call_command('sync_roles', reset_user_permissions=True, stdout=out) user_group_names = [group['name'] for group in user.groups.all().values('name')] self.assertIn(grp1.name, user_group_names) diff --git a/rolepermissions/tests/test_roles.py b/rolepermissions/tests/test_roles.py index 3d00663..ba30fc6 100644 --- a/rolepermissions/tests/test_roles.py +++ b/rolepermissions/tests/test_roles.py @@ -5,7 +5,7 @@ from model_mommy import mommy -from rolepermissions.roles import RolesManager, AbstractUserRole +from rolepermissions.roles import RolesManager, AbstractUserRole, get_or_create_permission class RolRole1(AbstractUserRole): @@ -156,3 +156,36 @@ def setUp(self): def test_retrieve_role(self): self.assertEquals(RolesManager.retrieve_role('rol_role1'), RolRole1) self.assertEquals(RolesManager.retrieve_role('rol_role2'), RolRole2) + + +class GetOrCreatePermissionsTests(TestCase): + + def setUp(self): + pass + + def test_create_default_named_permission(self): + perm_snake, _created = get_or_create_permission("my_perm_name1") + self.assertEqual(perm_snake.name, "My Perm Name1") + + perm_camel, _created = get_or_create_permission("myPermName2") + self.assertEqual(perm_camel.name, "My Perm Name2") + + def test_create_and_get_named_permission(self) : + perm1, _created = get_or_create_permission("my_perm_name", name="My Custom Name") + self.assertEqual(perm1.name, "My Custom Name") + + perm2, _created = get_or_create_permission("my_perm_name", name="My Custom Name") + self.assertEqual(perm1, perm2) + + def test_create_and_get_specialty_named_permission(self) : + def name_perm(codename): + return "Custom-"+codename + perm, _created = get_or_create_permission("my_perm_name", name_perm) + self.assertEqual(perm.name, "Custom-my_perm_name") + + def test_backwards_compat_with_unnamed_permission(self) : + unnamed_perm, _created = get_or_create_permission("my_perm_name", name="") + self.assertEqual(unnamed_perm.name, "") + + perm, _created = get_or_create_permission("my_perm_name") + self.assertEqual(unnamed_perm, perm) diff --git a/rolepermissions/tests/test_utils.py b/rolepermissions/tests/test_utils.py new file mode 100644 index 0000000..197e5bf --- /dev/null +++ b/rolepermissions/tests/test_utils.py @@ -0,0 +1,22 @@ + +from django.test import TestCase +from rolepermissions.utils import camelToSnake, snake_to_title, camel_or_snake_to_title + + +class UtilTests(TestCase): + + def setUp(self): + pass + + def test_camel_to_snake(self): + self.assertEqual(camelToSnake('camelCaseString'), 'camel_case_string') + self.assertEqual(camelToSnake('Snake_Camel_String'), 'snake__camel__string') + + def test_snake_to_title(self): + self.assertEqual(snake_to_title('snake_case_string'), 'Snake Case String') + self.assertEqual(snake_to_title('Even_if__itsFunky'), 'Even If Itsfunky') + + def test_camel_or_snake_to_title(self): + self.assertEqual(camel_or_snake_to_title('snake_case_string'), 'Snake Case String') + self.assertEqual(camel_or_snake_to_title('camelCaseString'), 'Camel Case String') + self.assertEqual(camel_or_snake_to_title('mix_itUp_WhyNot'), 'Mix It Up Why Not') From 27ecf5397c8fd48478f1c539f56a9d1c7b5ef0a9 Mon Sep 17 00:00:00 2001 From: powderflask Date: Sat, 28 Oct 2017 11:31:31 -0700 Subject: [PATCH 4/5] DRY group and permission get_or_create --- rolepermissions/management/commands/sync_roles.py | 3 +-- rolepermissions/roles.py | 15 +++++++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/rolepermissions/management/commands/sync_roles.py b/rolepermissions/management/commands/sync_roles.py index db5eecb..d62aed2 100644 --- a/rolepermissions/management/commands/sync_roles.py +++ b/rolepermissions/management/commands/sync_roles.py @@ -1,6 +1,5 @@ from django.conf import settings from django.core.management.base import BaseCommand -from django.contrib.auth.models import Group from django.contrib.auth import get_user_model from rolepermissions import roles @@ -26,7 +25,7 @@ def add_arguments(self, parser): def handle(self, *args, **options): # Sync auth.Group with current registered roles (leaving existing groups intact!) for role in roles.RolesManager.get_roles() : - group, created = Group.objects.get_or_create(name=role.get_name()) + group, created = role.get_or_create_group() if created: self.stdout.write("Created Group: %s from Role: %s"%(group.name, role.get_name())) # Sync auth.Permission with permissions for this role diff --git a/rolepermissions/roles.py b/rolepermissions/roles.py index 09aa0bd..9511053 100644 --- a/rolepermissions/roles.py +++ b/rolepermissions/roles.py @@ -126,7 +126,7 @@ def remove_role_from_user(cls, user): # Grab the adjusted true permissions before the removal current_adjusted_true_permissions = cls._get_adjusted_true_permissions(user) - group, _created = Group.objects.get_or_create(name=cls.get_name()) + group, _created = cls.get_or_create_group() user.groups.remove(group) # Grab the adjusted true permissions after the removal @@ -175,6 +175,10 @@ def get_or_create_permissions(cls, permission_names): def get_default(cls, permission_name): return cls.available_permissions[permission_name] + @classmethod + def get_or_create_group(cls): + return Group.objects.get_or_create(name=cls.get_name()) + def get_or_create_permission(codename, name=camel_or_snake_to_title): """ @@ -183,13 +187,8 @@ def get_or_create_permission(codename, name=camel_or_snake_to_title): @:param name: human-readable permissions name (str) or callable that takes codename as argument and returns str """ user_ct = ContentType.objects.get_for_model(get_user_model()) - # Careful here - don't use name to lookup existing permissions - try: - return Permission.objects.get(content_type=user_ct, codename=codename), False - except Permission.DoesNotExist: - perm_name = name(codename) if callable(name) else name - return Permission.objects.create(content_type=user_ct, codename=codename, name=perm_name), True - + return Permission.objects.get_or_create(content_type=user_ct, codename=codename, + defaults={'name':name(codename) if callable(name) else name}) def retrieve_role(role_name): """Get a Role object from a role name.""" From 845819eb36ded95053a557d0c5d3aebc2f779746 Mon Sep 17 00:00:00 2001 From: powderflask Date: Sat, 28 Oct 2017 13:38:43 -0700 Subject: [PATCH 5/5] Add docs for admin integration --- docs/admin.rst | 80 +++++++++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + docs/settings.rst | 17 ++++++++++ docs/utils.rst | 2 ++ 4 files changed, 100 insertions(+) create mode 100644 docs/admin.rst diff --git a/docs/admin.rst b/docs/admin.rst new file mode 100644 index 0000000..87cdb91 --- /dev/null +++ b/docs/admin.rst @@ -0,0 +1,80 @@ +================= +Admin Integration +================= + +Use Django User Admin Site to manage roles and permissions interactively. + + +Permission Names +================ + +Permissions defined in ``roles.py`` are given 'human-friendly' names. + +All such permissions are assigned to the ``auth | user`` Content Type. + +Permission names are a Title Case version of the snake_case or camelCase permission codename, so... + +* ``create_medical_record`` is named ``auth | user | Create Medical Record`` +* ``enterSurgery`` is named ``auth | user | Enter Surgery`` + + +.. _rolepermissions-useradmin: + +RolePermissions User Admin +========================== + +Assign / remove roles when editing Users in the Django User Admin Site. + +.. function:: RolePermissionsUserAdmin + + Custom ``django.contrib.auth.admin.UserAdmin`` that essentially adds the following logic: + + * ``remove_role(user, group)`` is called for each Group, removed via the Admin, that represents a role. + * ``assign_role(user, group)`` is called for each Group, added via the Admin, that represents a role. + + Opt-in with ``setting``: :ref:`ROLEPERMISSIONS_REGISTER_ADMIN ` = True + +.. function:: RolePermissionsUserAdminMixin + + Mixin the functionality of ``RolePermissionsUserAdmin`` to your own custom ``UserAdmin`` class + + + .. code-block:: python + + class MyCustomUserAdmin(RolePermissionsUserAdminMixin, django.contrib.auth.admin.UserAdmin): + ... + +.. warning:: ``remove_role`` removes every permission associated with a removed ``Group``, + regardless of how those permissions were originally assigned. + See :ref:`remove_role() ` + + +Management Commands +=================== + +.. code-block:: shell + + django-admin sync_roles + +Ensures that ``django.contrib.auth.models`` ``Group`` and ``Permission`` objects exist +for each role defined in ``roles.py`` + +This makes the roles and permissions defined in code immediately acccessible via the Django User Admin + +.. note:: ``sync_roles`` never deletes a ``Group`` or ``Permission``. + + If you remove a role or permission from ``roles.py``, the corresponding ``Group`` / ``Persission`` + continues to exist until it is manually removed. + +.. code-block:: shell + + django-admin sync_roles --reset_user_permissions + +Additionally, update every User's permissions to ensure they include all those defined by their current roles. + +.. warning:: ``--reset_user_permissions`` is primarily intended for development, not production! + + Changing which permissions are associated with a role in ``roles.py`` does NOT change any User's actual permissions! + ``--reset_user_permissions`` simply clears each User's roles and then re-assign them. + This guarantees that Users will have all permissions defined by their role(s) in ``roles.py``, + but in no way does this imply that any permissions previously granted to the User have been revoked! diff --git a/docs/index.rst b/docs/index.rst index f5cbee9..40458cf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -17,6 +17,7 @@ Contents: object_permissions utils views_utils + admin settings diff --git a/docs/settings.rst b/docs/settings.rst index 1b4ac16..7f901e6 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -14,3 +14,20 @@ Add the following variable to your django ``settings.py``: .. code-block:: python ROLEPERMISSIONS_REDIRECT_TO_LOGIN = True + + +.. _register-user-admin-setting: + +Regiser User Admin +================== + +Replace the default ``django.contrib.auth.admin.UserAdmin`` with :ref:`RolePermissionsUserAdmin ` +so you can manange roles interactively via the Django User Admin Site. + +Add the following variable to your django ``settings.py``: + +``settings.py`` + +.. code-block:: python + + ROLEPERMISSIONS_REGISTER_ADMIN = True diff --git a/docs/utils.rst b/docs/utils.rst index 5716d73..efd1432 100644 --- a/docs/utils.rst +++ b/docs/utils.rst @@ -25,6 +25,8 @@ Assigns a role to the user. Role parameter can be passed as string or role class assign_role(user, 'doctor') +.. _remove-role: + .. function:: remove_role(user, role) Removes a role from a user. Role parameter can be passed as string or role class object.