Refactor the user management app API

Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
This commit is contained in:
Roberto Rosario
2019-02-07 20:12:55 -04:00
parent ee2637dddc
commit e4af406d5f
7 changed files with 651 additions and 523 deletions

View File

@@ -1,15 +1,13 @@
from __future__ import unicode_literals
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.shortcuts import get_object_or_404
from rest_framework import generics
from rest_framework import generics, status
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.mixins import ExternalObjectMixin
from mayan.apps.rest_api.filters import MayanObjectPermissionsFilter
from mayan.apps.rest_api.permissions import MayanPermission
from mayan.apps.rest_api.viewsets import MayanAPIModelViewSet
from .permissions import (
permission_group_create, permission_group_delete, permission_group_edit,
@@ -17,141 +15,147 @@ from .permissions import (
permission_user_edit, permission_user_view
)
from .serializers import (
GroupSerializer, UserSerializer#, UserGroupListSerializer
CurrentUserSerializer, GroupUserAddRemoveSerializer, GroupSerializer,
UserGroupAddRemoveSerializer, UserSerializer
)
from .querysets import get_user_queryset
class APICurrentUserView(generics.RetrieveUpdateDestroyAPIView):
"""
delete: Delete the current user.
get: Return the details of the current user.
patch: Partially edit the current user.
put: Edit the current user.
"""
serializer_class = UserSerializer
class CurrentUserAPIView(generics.RetrieveUpdateAPIView):
permission_classes = (IsAuthenticated,)
serializer_class = CurrentUserSerializer
def get_object(self):
return self.request.user
class APIGroupListView(generics.ListCreateAPIView):
"""
get: Returns a list of all the groups.
post: Create a new group.
"""
filter_backends = (MayanObjectPermissionsFilter,)
mayan_object_permissions = {'GET': (permission_group_view,)}
mayan_view_permissions = {'POST': (permission_group_create,)}
permission_classes = (MayanPermission,)
queryset = Group.objects.order_by('id')
class GroupAPIViewSet(MayanAPIModelViewSet):
lookup_url_kwarg = 'group_id'
object_permission_map = {
'destroy': permission_group_delete,
'list': permission_group_view,
'partial_update': permission_group_edit,
'retrieve': permission_group_view,
'update': permission_group_edit,
'user_add': permission_group_edit,
'user_list': permission_group_view,
'user_remove': permission_group_edit
}
queryset = Group.objects.all()
serializer_class = GroupSerializer
class APIGroupView(generics.RetrieveUpdateDestroyAPIView):
"""
delete: Delete the selected group.
get: Return the details of the selected group.
patch: Partially edit the selected group.
put: Edit the selected group.
"""
lookup_url_kwarg = 'group_pk'
mayan_object_permissions = {
'GET': (permission_group_view,),
'PUT': (permission_group_edit,),
'PATCH': (permission_group_edit,),
'DELETE': (permission_group_delete,)
}
permission_classes = (MayanPermission,)
queryset = Group.objects.order_by('id')
serializer_class = GroupSerializer
class APIUserListView(generics.ListCreateAPIView):
"""
get: Returns a list of all the users.
post: Create a new user.
"""
filter_backends = (MayanObjectPermissionsFilter,)
mayan_object_permissions = {'GET': (permission_user_view,)}
mayan_view_permissions = {'POST': (permission_user_create,)}
permission_classes = (MayanPermission,)
queryset = get_user_model().objects.all()
serializer_class = UserSerializer
class APIUserView(generics.RetrieveUpdateDestroyAPIView):
"""
delete: Delete the selected user.
get: Return the details of the selected user.
patch: Partially edit the selected user.
put: Edit the selected user.
"""
lookup_url_kwarg = 'user_pk'
mayan_object_permissions = {
'GET': (permission_user_view,),
'PUT': (permission_user_edit,),
'PATCH': (permission_user_edit,),
'DELETE': (permission_user_delete,)
}
permission_classes = (MayanPermission,)
queryset = get_user_model().objects.all()
serializer_class = UserSerializer
class APIUserGroupList(ExternalObjectMixin, generics.ListCreateAPIView):
"""
get: Returns a list of all the groups to which an user belongs.
post: Add a user to a list of groups.
"""
external_object_pk_url_kwarg = 'user_pk'
filter_backends = (MayanObjectPermissionsFilter,)
mayan_object_permissions = {
'GET': (permission_group_view,),
'POST': (permission_group_edit,)
view_permission_map = {
'create': permission_group_create
}
def get_external_object_permission(self):
if self.request.method == 'POST':
return permission_user_edit
else:
return permission_user_view
def get_external_object_queryset(self):
return get_user_model().objects.exclude(is_staff=True).exclude(
is_superuser=True
@action(
detail=True, lookup_url_kwarg='group_id', methods=('post',),
serializer_class=GroupUserAddRemoveSerializer,
url_name='user-add', url_path='users/add'
)
def user_add(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.user_add(instance=instance)
headers = self.get_success_headers(data=serializer.data)
return Response(
serializer.data, status=status.HTTP_200_OK, headers=headers
)
def get_serializer(self, *args, **kwargs):
if not self.request:
return None
@action(
detail=True, lookup_url_kwarg='group_id',
serializer_class=UserSerializer, url_name='user-list',
url_path='users'
)
def user_list(self, request, *args, **kwargs):
queryset = self.get_object().get_users(_user=self.request.user)
page = self.paginate_queryset(queryset)
return super(APIUserGroupList, self).get_serializer(*args, **kwargs)
serializer = self.get_serializer(
queryset, many=True, context={'request': request}
)
def get_serializer_class(self):
if self.request.method == 'POST':
return UserSerializer
else:
return GroupSerializer
if page is not None:
return self.get_paginated_response(serializer.data)
def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
context = super(APIUserGroupList, self).get_serializer_context()
if self.kwargs:
context.update(
{
'user': self.get_user(),
}
)
return Response(serializer.data)
return context
@action(
detail=True, lookup_url_kwarg='group_id',
methods=('post',), serializer_class=GroupUserAddRemoveSerializer,
url_name='user-remove', url_path='users/remove'
)
def user_remove(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.user_remove(instance=instance)
headers = self.get_success_headers(data=serializer.data)
return Response(
serializer.data, status=status.HTTP_200_OK, headers=headers
)
def get_queryset(self):
return self.get_user().groups.order_by('id')
def get_user(self):
return self.get_external_object()
class UserAPIViewSet(MayanAPIModelViewSet):
lookup_url_kwarg = 'user_id'
object_permission_map = {
'destroy': permission_user_delete,
'group-list': permission_user_view,
'list': permission_user_view,
'partial_update': permission_user_edit,
'retrieve': permission_user_view,
'update': permission_user_edit,
}
queryset = get_user_queryset()
serializer_class = UserSerializer
view_permission_map = {
'create': permission_user_create
}
def perform_create(self, serializer):
return serializer.save(user=self.get_object(), _user=self.request.user)
@action(
detail=True, lookup_url_kwarg='user_id', methods=('post',),
serializer_class=UserGroupAddRemoveSerializer,
url_name='group-add', url_path='group/add'
)
def group_add(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.group_add(instance=instance)
headers = self.get_success_headers(data=serializer.data)
return Response(
serializer.data, status=status.HTTP_200_OK, headers=headers
)
@action(
detail=True, lookup_url_kwarg='user_id',
serializer_class=GroupSerializer, url_name='group-list',
url_path='groups'
)
def group_list(self, request, *args, **kwargs):
queryset = self.get_object().get_groups(_user=self.request.user)
page = self.paginate_queryset(queryset)
serializer = self.get_serializer(
queryset, many=True, context={'request': request}
)
if page is not None:
return self.get_paginated_response(serializer.data)
return Response(serializer.data)
@action(
detail=True, lookup_url_kwarg='user_id',
methods=('post',), serializer_class=UserGroupAddRemoveSerializer,
url_name='group-remove', url_path='groups/remove'
)
def group_remove(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.group_remove(instance=instance)
headers = self.get_success_headers(data=serializer.data)
return Response(
serializer.data, status=status.HTTP_200_OK, headers=headers
)

View File

@@ -33,7 +33,10 @@ from .links import (
link_user_set_password, link_user_setup, text_user_label,
separator_user_label
)
from .methods import method_get_absolute_url
from .methods import (
method_group_get_users, method_group_user_add, method_group_user_remove,
method_user_get_absolute_url, method_user_get_groups
)
from .permissions import (
permission_group_delete, permission_group_edit,
permission_group_view, permission_user_delete, permission_user_edit,
@@ -63,6 +66,14 @@ class UserManagementApp(MayanAppConfig):
serializer_class='mayan.apps.user_management.serializers.UserSerializer'
)
# Silence UnorderedObjectListWarning
# "Pagination may yield inconsistent result"
# Remove on Django 2.x
Group._meta.ordering = ('name',)
Group.add_to_class(name='get_users', value=method_group_get_users)
Group.add_to_class(name='user_add', value=method_group_user_add)
Group.add_to_class(name='user_remove', value=method_group_user_remove)
MetadataLookup(
description=_('All the groups.'), name='groups',
value=lookup_get_groups
@@ -124,7 +135,10 @@ class UserManagementApp(MayanAppConfig):
)
User.add_to_class(
name='get_absolute_url', value=method_get_absolute_url
name='get_absolute_url', value=method_user_get_absolute_url
)
User.add_to_class(
name='get_groups', value=method_user_get_groups
)
menu_list_facet.bind_links(

View File

@@ -1,9 +1,59 @@
from __future__ import unicode_literals
from django.apps import apps
from django.db import transaction
from django.shortcuts import reverse
from .events import event_group_edited, event_user_edited
from .permissions import permission_user_view
from .querysets import get_user_queryset
def method_get_absolute_url(self):
def method_group_get_users(self, _user):
AccessControlList = apps.get_model(
app_label='acls', model_name='AccessControlList'
)
return AccessControlList.objects.restrict_queryset(
permission=permission_user_view, queryset=get_user_queryset(),
user=_user
)
def method_group_user_add(self, user, _user):
with transaction.atomic():
self.user_set.add(user)
event_group_edited.commit(
actor=_user, target=self
)
event_user_edited.commit(
actor=_user, target=user
)
def method_group_user_remove(self, user, _user):
with transaction.atomic():
self.user_set.remove(user)
event_group_edited.commit(
actor=_user, target=self
)
event_user_edited.commit(
actor=_user, target=user
)
def method_user_get_absolute_url(self):
return reverse(
viewname='user_management:user_details', kwargs={'user_id': self.pk}
)
def method_user_get_groups(self, _user):
AccessControlList = apps.get_model(
app_label='acls', model_name='AccessControlList'
)
return AccessControlList.objects.restrict_queryset(
permission=permission_user_view, queryset=self.groups.all(),
user=_user
)

View File

@@ -0,0 +1,7 @@
from __future__ import absolute_import, unicode_literals
from django.contrib.auth import get_user_model
def get_user_queryset():
return get_user_model().objects.filter(is_superuser=False, is_staff=False)

View File

@@ -3,41 +3,112 @@ from __future__ import unicode_literals
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.contrib.auth.password_validation import validate_password
from django.core.exceptions import PermissionDenied
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from mayan.apps.acls.models import AccessControlList
from mayan.apps.rest_api.mixins import ExternalObjectListSerializerMixin
from .permissions import permission_group_edit, permission_group_view
from .permissions import permission_group_edit, permission_user_edit
from .querysets import get_user_queryset
class GroupSerializer(serializers.HyperlinkedModelSerializer):
users_count = serializers.SerializerMethodField()
user_add_url = serializers.HyperlinkedIdentityField(
lookup_url_kwarg='group_id', view_name='rest_api:group-user-add'
)
user_list_url = serializers.HyperlinkedIdentityField(
lookup_url_kwarg='group_id', view_name='rest_api:group-user-list'
)
user_remove_url = serializers.HyperlinkedIdentityField(
lookup_url_kwarg='group_id', view_name='rest_api:group-user-remove'
)
class Meta:
extra_kwargs = {
'url': {
'lookup_field': 'pk', 'lookup_url_kwarg': 'group_pk',
'lookup_url_kwarg': 'group_id',
'view_name': 'rest_api:group-detail'
}
}
fields = ('id', 'name', 'url', 'users_count')
fields = (
'id', 'name', 'url', 'user_add_url', 'user_list_url',
'user_remove_url'
)
model = Group
def get_users_count(self, instance):
return instance.user_set.count()
class GroupUserAddRemoveSerializer(ExternalObjectListSerializerMixin, serializers.Serializer):
user_id = serializers.CharField(
help_text=_(
'Primary key of the user that will be added or removed.'
), required=False, write_only=True
)
users_id_list = serializers.CharField(
help_text=_(
'Comma separated list of user primary keys that will be added or '
'removed.'
), required=False, write_only=True
)
class Meta:
external_object_list_queryset = get_user_queryset()
external_object_list_permission = permission_user_edit
external_object_list_pk_field = 'user_id'
external_object_list_pk_list_field = 'user_id_list'
def user_add(self, instance):
queryset = self.get_external_object_list()
for user in queryset:
instance.user_add(user=user, _user=self.context['request'].user)
def user_remove(self, instance):
queryset = self.get_external_object_list()
for user in queryset:
instance.user_remove(user=user, _user=self.context['request'].user)
class UserGroupAddRemoveSerializer(ExternalObjectListSerializerMixin, serializers.Serializer):
group_id = serializers.CharField(
help_text=_(
'Primary key of the group that will be added or removed.'
), required=False, write_only=True
)
groups_id_list = serializers.CharField(
help_text=_(
'Comma separated list of group primary keys that will be added or '
'removed.'
), required=False, write_only=True
)
class Meta:
external_object_list_queryset = Group.objects.all()
external_object_list_permission = permission_group_edit
external_object_list_pk_field = 'group_id'
external_object_list_pk_list_field = 'group_id_list'
def group_add(self, instance):
queryset = self.get_external_object_list()
for group in queryset:
instance.group_add(group=group, _group=self.context['request'].group)
def group_remove(self, instance):
queryset = self.get_external_object_list()
for group in queryset:
instance.group_remove(group=group, _group=self.context['request'].group)
class UserSerializer(serializers.HyperlinkedModelSerializer):
groups = GroupSerializer(many=True, read_only=True, required=False)
groups_pk_list = serializers.CharField(
help_text=_(
'Comma separated list of group primary keys to assign this '
'user to.'
), required=False, write_only=True
group_add_url = serializers.HyperlinkedIdentityField(
lookup_url_kwarg='user_id', view_name='rest_api:user-group-add'
)
group_list_url = serializers.HyperlinkedIdentityField(
lookup_url_kwarg='user_id', view_name='rest_api:user-group-list'
)
group_remove_url = serializers.HyperlinkedIdentityField(
lookup_url_kwarg='user_id', view_name='rest_api:user-group-remove'
)
password = serializers.CharField(
required=False, style={'input_type': 'password'}, write_only=True
@@ -46,32 +117,19 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
extra_kwargs = {
'url': {
'lookup_field': 'pk', 'lookup_url_kwarg': 'user_pk',
'lookup_url_kwarg': 'user_id',
'view_name': 'rest_api:user-detail'
}
}
fields = (
'first_name', 'date_joined', 'email', 'groups', 'groups_pk_list',
'id', 'is_active', 'last_login', 'last_name', 'password', 'url',
'username'
'first_name', 'date_joined', 'email', 'group_add_url',
'group_list_url', 'group_remove_url', 'id', 'is_active',
'last_login', 'last_name', 'password', 'url', 'username'
)
model = get_user_model()
read_only_fields = ('groups', 'is_active', 'last_login', 'date_joined')
write_only_fields = ('password', 'group_pk_list')
def _add_groups(self, instance, groups_pk_list):
instance.groups.clear()
queryset = AccessControlList.objects.restrict_queryset(
permission=permission_group_edit,
queryset=Group.objects.filter(pk__in=groups_pk_list.split(',')),
user=self.context['request'].user
)
instance.groups.add(*queryset)
read_only_fields = ('is_active', 'last_login', 'date_joined')
def create(self, validated_data):
groups_pk_list = validated_data.pop('groups_pk_list', '')
password = validated_data.pop('password', None)
instance = super(UserSerializer, self).create(validated_data)
@@ -79,23 +137,15 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
instance.set_password(password)
instance.save()
if groups_pk_list:
self._add_groups(instance=instance, groups_pk_list=groups_pk_list)
return instance
def update(self, instance, validated_data):
groups_pk_list = validated_data.pop('groups_pk_list', '')
if 'password' in validated_data:
instance.set_password(validated_data['password'])
validated_data.pop('password')
instance = super(UserSerializer, self).update(instance, validated_data)
if groups_pk_list:
self._add_groups(instance=instance, groups_pk_list=groups_pk_list)
return instance
def validate(self, data):
@@ -103,3 +153,14 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
validate_password(data['password'], self.instance)
return data
class CurrentUserSerializer(UserSerializer):
class Meta(UserSerializer.Meta):
# Remove some fields that don't apply to the current user
_field_list = (
'group_add_url', 'group_list_url', 'group_remove_url', 'url'
)
fields = [
field for field in UserSerializer.Meta.fields if field not in _field_list
]

File diff suppressed because it is too large Load Diff

View File

@@ -2,10 +2,7 @@ from __future__ import unicode_literals
from django.conf.urls import url
from .api_views import (
APICurrentUserView, APIGroupListView, APIGroupView, APIUserGroupList,
APIUserListView, APIUserView
)
from .api_views import CurrentUserAPIView, GroupAPIViewSet, UserAPIViewSet
from .views import (
CurrentUserDetailsView, CurrentUserEditView, GroupCreateView,
GroupDeleteView, GroupEditView, GroupListView, GroupMembersView,
@@ -82,25 +79,13 @@ urlpatterns = [
)
]
api_urls = [
api_urlpatterns = [
url(
regex=r'^groups/$', name='group-list', view=APIGroupListView.as_view()
),
url(
regex=r'^groups/(?P<group_id>\d+)/$', name='group-detail',
view=APIGroupView.as_view(),
),
url(
regex=r'^user/$', name='user-current',
view=APICurrentUserView.as_view()
),
url(regex=r'^users/$', name='user-list', view=APIUserListView.as_view()),
url(
regex=r'^users/(?P<user_id>\d+)/$', name='user-detail',
view=APIUserView.as_view()
),
url(
regex=r'^users/(?P<user_id>\d+)/groups/$', name='users-group-list',
view=APIUserGroupList.as_view()
),
regex=r'^user/$', name='user-current', view=CurrentUserAPIView.as_view()
)
]
api_router_entries = (
{'prefix': r'groups', 'viewset': GroupAPIViewSet, 'basename': 'group'},
{'prefix': r'users', 'viewset': UserAPIViewSet, 'basename': 'user'},
)