-
Notifications
You must be signed in to change notification settings - Fork 114
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Manage roles and permissions via the django admin UserAdmin Groups and Permissions. #72
Changes from 4 commits
31456e3
8913102
1009050
e2a783c
27ecf53
845819e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,40 @@ | ||
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 | ||
|
||
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): | ||
user = UserModel.objects.get(pk=form.instance.pk) | ||
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 | ||
|
||
|
||
class RolePermissionsUserAdmin(RolePermissionsUserAdminMixin, UserAdmin): | ||
pass | ||
|
||
if ROLEPERMISSIONS_REGISTER_ADMIN: | ||
admin.site.unregister(UserModel) | ||
admin.site.register(UserModel, RolePermissionsUserAdmin) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
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 | ||
|
||
|
||
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 !!', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool! |
||
) | ||
|
||
def handle(self, *args, **options): | ||
# Sync auth.Group with current registered roles (leaving existing groups intact!) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool! |
||
for role in roles.RolesManager.get_roles() : | ||
group, created = Group.objects.get_or_create(name=role.get_name()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's create a method in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea. Will do. |
||
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() | ||
|
||
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) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
||
|
||
|
@@ -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): | ||
|
||
|
@@ -158,11 +162,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 +176,21 @@ def get_default(cls, permission_name): | |
return cls.available_permissions[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()) | ||
# Careful here - don't use name to lookup existing permissions | ||
try: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we use a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Like...
? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @powderflask
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ha ha - new to me, go figure !! :-P so simple, duh...
Thanks. |
||
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): | ||
"""Get a Role object from a role name.""" | ||
return RolesManager.retrieve_role(role_name) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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', 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) | ||
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) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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') |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you provide some tests for those functions? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes. Good catch. |
||
return snake_to_title(camelToSnake(s)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool approach, liked it!