diff --git a/HISTORY.rst b/HISTORY.rst index f11298a934..174d6fe406 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -150,6 +150,14 @@ * Add AddRemoveView to replace AssignRemoveView * Update the group roles view to use the new AddRemoveView. * Add role create and edit events. +* Sort users by lastname, firstname. +* Switch user groups and group users views to AddRemoveView. +* Commit user edit event when an user is added or removed + from a group. +* Commit the group edit event when a group is added or remove + from an user. +* Require dual permissions when add or removing users to and + from group. Same with group to users. 3.1.11 (2019-04-XX) =================== diff --git a/docs/releases/3.2.rst b/docs/releases/3.2.rst index 882c4a02be..30dee2d5c5 100644 --- a/docs/releases/3.2.rst +++ b/docs/releases/3.2.rst @@ -182,6 +182,14 @@ Other changes * Add AddRemoveView to replace AssignRemoveView * Update the group roles view to use the new AddRemoveView. * Add role create and edit events. +* Sort users by lastname, firstname. +* Switch user groups and group users views to AddRemoveView. +* Commit user edit event when an user is added or removed + from a group. +* Commit the group edit event when a group is added or remove + from an user. +* Require dual permissions when add or removing users to and + from group. Same with group to users. Removals -------- diff --git a/mayan/apps/user_management/apps.py b/mayan/apps/user_management/apps.py index fe6dc1618c..3392df6bab 100644 --- a/mayan/apps/user_management/apps.py +++ b/mayan/apps/user_management/apps.py @@ -37,7 +37,9 @@ from .links import ( text_user_label ) from .methods import ( - get_method_group_save, get_method_user_save, method_user_get_absolute_url + get_method_group_save, get_method_user_save, method_user_get_absolute_url, + method_group_get_users, method_group_users_add, method_group_users_remove, + method_user_get_groups, method_user_groups_add, method_user_groups_remove ) from .permissions import ( @@ -89,6 +91,15 @@ class UserManagementApp(MayanAppConfig): Group._meta.verbose_name = _('Group') Group._meta.verbose_name_plural = _('Groups') + Group.add_to_class( + name='get_users', value=method_group_get_users + ) + Group.add_to_class( + name='users_add', value=method_group_users_add + ) + Group.add_to_class( + name='users_remove', value=method_group_users_remove + ) Group.add_to_class(name='save', value=get_method_group_save()) MetadataLookup( @@ -161,10 +172,20 @@ class UserManagementApp(MayanAppConfig): User._meta.ordering = ('pk',) User._meta.verbose_name = _('User') User._meta.verbose_name_plural = _('Users') + User._meta.ordering = ('last_name', 'first_name') User.add_to_class( name='get_absolute_url', value=method_user_get_absolute_url ) + User.add_to_class( + name='get_groups', value=method_user_get_groups + ) + User.add_to_class( + name='groups_add', value=method_user_groups_add + ) + User.add_to_class( + name='groups_remove', value=method_user_groups_remove + ) User.add_to_class(name='save', value=get_method_user_save()) menu_list_facet.bind_links( diff --git a/mayan/apps/user_management/methods.py b/mayan/apps/user_management/methods.py index 240457a5a2..e5d050fe1d 100644 --- a/mayan/apps/user_management/methods.py +++ b/mayan/apps/user_management/methods.py @@ -9,6 +9,8 @@ from .events import ( event_group_created, event_group_edited, event_user_created, event_user_edited ) +from .permissions import permission_group_view, permission_user_view +from .querysets import get_user_queryset def get_method_group_save(): @@ -33,12 +35,82 @@ def get_method_group_save(): return method_group_save +def method_group_get_users(self, user, permission=permission_user_view): + AccessControlList = apps.get_model( + app_label='acls', model_name='AccessControlList' + ) + + return AccessControlList.objects.filter_by_access( + permission=permission, queryset=get_user_queryset().filter( + id__in=self.user_set.all() + ), user=user + ) + + +def method_group_users_add(self, queryset, _user): + with transaction.atomic(): + event_group_edited.commit( + actor=_user, target=self + ) + for user in queryset: + self.user_set.add(user) + event_user_edited.commit( + actor=_user, target=user + ) + + +def method_group_users_remove(self, queryset, _user): + with transaction.atomic(): + event_group_edited.commit( + actor=_user, target=self + ) + for user in queryset: + self.user_set.remove(user) + event_user_edited.commit( + actor=_user, target=user + ) + + def method_user_get_absolute_url(self): return reverse( viewname='user_management:user_details', kwargs={'pk': self.pk} ) +def method_user_get_groups(self, user, permission=permission_group_view): + AccessControlList = apps.get_model( + app_label='acls', model_name='AccessControlList' + ) + + return AccessControlList.objects.filter_by_access( + permission=permission, queryset=self.groups.all(), user=user + ) + + +def method_user_groups_add(self, queryset, _user): + with transaction.atomic(): + event_user_edited.commit( + actor=_user, target=self + ) + for group in queryset: + self.groups.add(group) + event_group_edited.commit( + actor=_user, target=group + ) + + +def method_user_groups_remove(self, queryset, _user): + with transaction.atomic(): + event_user_edited.commit( + actor=_user, target=self + ) + for group in queryset: + self.groups.remove(group) + event_group_edited.commit( + actor=_user, target=group + ) + + def get_method_user_save(): user_save_original = get_user_model().save diff --git a/mayan/apps/user_management/tests/test_views.py b/mayan/apps/user_management/tests/test_views.py index 9bc476fef6..dd6eb6627c 100644 --- a/mayan/apps/user_management/tests/test_views.py +++ b/mayan/apps/user_management/tests/test_views.py @@ -115,7 +115,7 @@ class GroupViewsTestCase(GroupTestMixin, GroupViewTestMixin, UserTestMixin, Gene self.test_user.groups.add(self.test_group) response = self._request_test_group_members_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) def test_group_members_view_with_group_access(self): self._create_test_user() @@ -141,7 +141,7 @@ class GroupViewsTestCase(GroupTestMixin, GroupViewTestMixin, UserTestMixin, Gene response = self._request_test_group_members_view() self.assertNotContains( - response=response, text=self.test_group.name, status_code=403 + response=response, text=self.test_group.name, status_code=404 ) def test_group_members_view_with_full_access(self): @@ -311,7 +311,7 @@ class UserGroupViewTestCase(GroupTestMixin, UserTestMixin, UserViewTestMixin, Ge self.test_user.groups.add(self.test_group) response = self._request_test_user_groups_view() - self.assertEqual(response.status_code, 403) + self.assertEqual(response.status_code, 404) def test_user_groups_view_with_group_access(self): self._create_test_user() @@ -323,7 +323,7 @@ class UserGroupViewTestCase(GroupTestMixin, UserTestMixin, UserViewTestMixin, Ge response = self._request_test_user_groups_view() self.assertNotContains( - response=response, text=self.test_user.username, status_code=403 + response=response, text=self.test_user.username, status_code=404 ) def test_user_groups_view_with_user_access(self): diff --git a/mayan/apps/user_management/views.py b/mayan/apps/user_management/views.py index d3570a2080..1e950fc3ff 100644 --- a/mayan/apps/user_management/views.py +++ b/mayan/apps/user_management/views.py @@ -4,7 +4,6 @@ from django.contrib import messages from django.contrib.auth import get_user_model from django.contrib.auth.forms import SetPasswordForm from django.contrib.auth.models import Group -from django.contrib.contenttypes.models import ContentType from django.core.exceptions import PermissionDenied from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 @@ -12,12 +11,11 @@ from django.template import RequestContext from django.urls import reverse, reverse_lazy from django.utils.translation import ungettext, ugettext_lazy as _ -from mayan.apps.acls.models import AccessControlList +from mayan.apps.common.generics import AddRemoveView from mayan.apps.common.views import ( - AssignRemoveView, MultipleObjectConfirmActionView, - MultipleObjectFormActionView, SingleObjectCreateView, - SingleObjectDeleteView, SingleObjectDetailView, SingleObjectEditView, - SingleObjectListView + MultipleObjectConfirmActionView, MultipleObjectFormActionView, + SingleObjectCreateView, SingleObjectDeleteView, SingleObjectDetailView, + SingleObjectEditView, SingleObjectListView ) from .forms import UserForm @@ -28,6 +26,7 @@ from .permissions import ( permission_group_view, permission_user_create, permission_user_delete, permission_user_edit, permission_user_view ) +from .querysets import get_user_queryset class CurrentUserDetailsView(SingleObjectDetailView): @@ -126,62 +125,31 @@ class GroupDeleteView(SingleObjectDeleteView): } -class GroupUsersView(AssignRemoveView): - decode_content_type = True - left_list_title = _('Available users') - right_list_title = _('Users in group') - object_permission = permission_group_edit +class GroupUsersView(AddRemoveView): + action_add_method = 'users_add' + action_remove_method = 'users_remove' + main_object_model = Group + main_object_permission = permission_group_edit + main_object_pk_url_kwarg = 'pk' + secondary_object_permission = permission_user_edit + secondary_object_source_queryset = get_user_queryset() + list_available_title = _('Available users') + list_added_title = _('Group users') - @staticmethod - def generate_choices(choices): - results = [] - for choice in choices: - ct = ContentType.objects.get_for_model(choice) - label = choice.get_full_name() if choice.get_full_name() else choice - - results.append(('%s,%s' % (ct.model, choice.pk), '%s' % (label))) - - # Sort results by the label not the key value - return sorted(results, key=lambda x: x[1]) - - def add(self, item): - self.get_object().user_set.add(item) + def get_actions_extra_kwargs(self): + return {'_user': self.request.user} def get_extra_context(self): return { - 'object': self.get_object(), - 'title': _('Users of group: %s') % self.get_object() + 'object': self.main_object, + 'title': _('Users of group: %s') % self.main_object, } - def get_object(self): - return get_object_or_404(klass=Group, pk=self.kwargs['pk']) - - def get_choices_queryset(self): - return AccessControlList.objects.filter_by_access( - permission=permission_user_edit, - queryset=get_user_model().objects.exclude( - is_staff=True - ).exclude(is_superuser=True), - user=self.request.user + def get_list_added_queryset(self): + return self.main_object.get_users( + permission=permission_user_edit, user=self.request.user ) - def left_list(self): - return GroupUsersView.generate_choices( - self.get_choices_queryset().exclude( - groups=self.get_object() - ) - ) - - def right_list(self): - return GroupUsersView.generate_choices( - self.get_choices_queryset().filter( - pk__in=self.get_object().user_set.all() - ) - ) - - def remove(self, item): - self.get_object().user_set.remove(item) - class UserCreateView(SingleObjectCreateView): extra_context = { @@ -199,16 +167,13 @@ class UserCreateView(SingleObjectCreateView): ) ) - def get_save_extra_data(self): return {'_user': self.request.user} class UserDeleteView(MultipleObjectConfirmActionView): object_permission = permission_user_delete - queryset = get_user_model().objects.filter( - is_superuser=False, is_staff=False - ) + queryset = get_user_queryset() success_message = _('User delete request performed on %(count)d user') success_message_plural = _( 'User delete request performed on %(count)d users' @@ -267,9 +232,7 @@ class UserDetailsView(SingleObjectDetailView): ) object_permission = permission_user_view pk_url_kwarg = 'pk' - queryset = get_user_model().objects.filter( - is_superuser=False, is_staff=False - ) + queryset = get_user_queryset() def get_extra_context(self, **kwargs): return { @@ -284,9 +247,7 @@ class UserEditView(SingleObjectEditView): post_action_redirect = reverse_lazy( viewname='user_management:user_list' ) - queryset = get_user_model().objects.filter( - is_superuser=False, is_staff=False - ) + queryset = get_user_queryset() def get_extra_context(self): return { @@ -298,47 +259,31 @@ class UserEditView(SingleObjectEditView): return {'_user': self.request.user} -class UserGroupsView(AssignRemoveView): - decode_content_type = True - left_list_title = _('Available groups') - right_list_title = _('Groups joined') - object_permission = permission_user_edit +class UserGroupsView(AddRemoveView): + action_add_method = 'groups_add' + action_remove_method = 'groups_remove' + main_object_permission = permission_user_edit + main_object_source_queryset = get_user_queryset() + main_object_pk_url_kwarg = 'pk' + secondary_object_model = Group + secondary_object_permission = permission_group_edit + list_available_title = _('Available groups') + list_added_title = _('User groups') - def add(self, item): - item.user_set.add(self.get_object()) + def get_actions_extra_kwargs(self): + return {'_user': self.request.user} def get_extra_context(self): return { - 'object': self.get_object(), - 'title': _('Groups of user: %s') % self.get_object() + 'object': self.main_object, + 'title': _('Groups of user: %s') % self.main_object, } - def get_object(self): - return get_object_or_404( - klass=get_user_model().objects.filter( - is_superuser=False, is_staff=False - ), pk=self.kwargs['pk'] - ) - - def get_choices_queryset(self): - return AccessControlList.objects.filter_by_access( - queryset=Group.objects.filter(user=self.get_object()), + def get_list_added_queryset(self): + return self.main_object.get_groups( permission=permission_group_edit, user=self.request.user ) - def left_list(self): - return AssignRemoveView.generate_choices( - self.get_choices_queryset().exclude(user=self.get_object()) - ) - - def right_list(self): - return AssignRemoveView.generate_choices( - self.get_choices_queryset().filter(user=self.get_object()) - ) - - def remove(self, item): - item.user_set.remove(self.get_object()) - class UserListView(SingleObjectListView): object_permission = permission_user_view @@ -360,9 +305,7 @@ class UserListView(SingleObjectListView): } def get_object_list(self): - return get_user_model().objects.exclude( - is_superuser=True - ).exclude(is_staff=True).order_by('last_name', 'first_name') + return get_user_queryset() class UserOptionsEditView(SingleObjectEditView): @@ -385,9 +328,7 @@ class UserOptionsEditView(SingleObjectEditView): def get_user(self): return get_object_or_404( - klass=get_user_model().objects.filter( - is_superuser=False, is_staff=False - ), pk=self.kwargs['pk'] + klass=get_user_queryset(), pk=self.kwargs['pk'] )