diff --git a/HISTORY.rst b/HISTORY.rst index b33984818b..6eba4f722d 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -16,6 +16,14 @@ the user links - Stop loading theme fonts from the web (GitLab #343). - Add support for attaching multiple tags (GitLab #307). +2.1.7 (2017-02-01) +================== +- Improved user management API endpoints. +- Improved permissions API endpoints. +- Improvements in the API tests of a few apps. +- Addition Content type list API view to the common app. +- Add API endpoints to the events app. +- Enable the parser and validation fields of the metadata serializer. 2.1.6 (2016-11-23) ================= diff --git a/docs/releases/2.1.7.rst b/docs/releases/2.1.7.rst new file mode 100644 index 0000000000..d4eba94951 --- /dev/null +++ b/docs/releases/2.1.7.rst @@ -0,0 +1,102 @@ +=============================== +Mayan EDMS v2.1.7 release notes +=============================== + +Released: February 2, 2017 + +What's new +========== + +This is a bug-fix release and all users are encouraged to upgrade. The focus +of this micro release was REST API improvement. + +Changes +------------- + +- Improved user management API endpoints (initial work by @lokeshmanmode): + + - Improved user creation API endpoint to allow specifying the group + membership. + - Improved user editing API endpoint to allow specifying the group + membership. + +- Improved permissions API endpoints (initial work by @lokeshmanmode): + + - Add permission list API endpoint. This API endpoint lists all possible + permissions in the system. + - Improved role creation API endpoint to allow specifying the role's group + membership and role's permissions. + - Improved role editing API endpoint to allow specifying the role's group + membership and role's permissions. + +- Improvements in the API tests of a few apps. +- Add content type list API view to the common app. Content type is required + when querying the events of an object, this view show list of content types + available. +- Add event type list api view. This API view shows all the possible events + that are registered in the system. +- Add event list API view. This view shows all the events that have taken + place in the system. +- Add object event list API view. This view show all the events for a specific + object (document, etc). The content type of the object whose events are being + requested must be specified. The list of available content types is provided + now by the common app API. +- The parser and validation fields of the metadata type model have been enable + in the metadata type API serializer. + +Removals +-------- +* None + +Upgrading from a previous version +--------------------------------- + +Using PIP +~~~~~~~~~ + +Type in the console:: + + $ pip install -U mayan-edms + +the requirements will also be updated automatically. + +Using Git +~~~~~~~~~ + +If you installed Mayan EDMS by cloning the Git repository issue the commands:: + + $ git reset --hard HEAD + $ git pull + +otherwise download the compressed archived and uncompress it overriding the +existing installation. + +Next upgrade/add the new requirements:: + + $ pip install --upgrade -r requirements.txt + +Common steps +~~~~~~~~~~~~ + +Migrate existing database schema with:: + + $ mayan-edms.py performupgrade + +Add new static media:: + + $ mayan-edms.py collectstatic --noinput + +The upgrade procedure is now complete. + + +Backward incompatible changes +============================= + +* None + +Bugs fixed or issues closed +=========================== + +* None + +.. _PyPI: https://pypi.python.org/pypi/mayan-edms/ diff --git a/docs/releases/index.rst b/docs/releases/index.rst index 4568e10261..65650cab3d 100644 --- a/docs/releases/index.rst +++ b/docs/releases/index.rst @@ -23,6 +23,7 @@ versions of the documentation contain the release notes for any later releases. :maxdepth: 1 2.2 + 2.1.7 2.1.6 2.1.5 2.1.4 diff --git a/mayan/__init__.py b/mayan/__init__.py index 86974477eb..43c74b02a1 100644 --- a/mayan/__init__.py +++ b/mayan/__init__.py @@ -1,8 +1,8 @@ from __future__ import unicode_literals __title__ = 'Mayan EDMS' -__version__ = '2.1.6' -__build__ = 0x020106 +__version__ = '2.1.7' +__build__ = 0x020107 __author__ = 'Roberto Rosario' __author_email__ = 'roberto.rosario@mayan-edms.com' __description__ = 'Free Open Source Electronic Document Management System' diff --git a/mayan/apps/acls/tests/test_models.py b/mayan/apps/acls/tests/test_models.py index f77028a0e9..4c03028ec5 100644 --- a/mayan/apps/acls/tests/test_models.py +++ b/mayan/apps/acls/tests/test_models.py @@ -11,7 +11,7 @@ from documents.permissions import permission_document_view from documents.tests import TEST_SMALL_DOCUMENT_PATH, TEST_DOCUMENT_TYPE from permissions.models import Role from permissions.tests.literals import TEST_ROLE_LABEL -from user_management.tests.literals import TEST_USER_USERNAME, TEST_GROUP +from user_management.tests.literals import TEST_USER_USERNAME, TEST_GROUP_NAME from ..models import AccessControlList @@ -48,7 +48,7 @@ class PermissionTestCase(BaseTestCase): self.user = get_user_model().objects.create( username=TEST_USER_USERNAME ) - self.group = Group.objects.create(name=TEST_GROUP) + self.group = Group.objects.create(name=TEST_GROUP_NAME) self.role = Role.objects.create(label=TEST_ROLE_LABEL) self.group.user_set.add(self.user) diff --git a/mayan/apps/common/api_views.py b/mayan/apps/common/api_views.py new file mode 100644 index 0000000000..1a1f274996 --- /dev/null +++ b/mayan/apps/common/api_views.py @@ -0,0 +1,16 @@ +from __future__ import unicode_literals + +from django.contrib.contenttypes.models import ContentType + +from rest_framework import generics + +from .serializers import ContentTypeSerializer + + +class APIContentTypeList(generics.ListAPIView): + """ + Returns a list of all the available content types. + """ + + serializer_class = ContentTypeSerializer + queryset = ContentType.objects.order_by('app_label', 'model') diff --git a/mayan/apps/common/apps.py b/mayan/apps/common/apps.py index 07c473f8c5..1c56cb4553 100644 --- a/mayan/apps/common/apps.py +++ b/mayan/apps/common/apps.py @@ -14,6 +14,7 @@ from django.utils.translation import ugettext_lazy as _ from mayan.celery import app from navigation.classes import Separator +from rest_api.classes import APIEndPoint from .handlers import ( user_locale_profile_session_config, user_locale_profile_create @@ -77,6 +78,8 @@ class CommonApp(MayanAppConfig): def ready(self): super(CommonApp, self).ready() + APIEndPoint(app=self, version_string='1') + app.conf.CELERYBEAT_SCHEDULE.update( { 'task_delete_stale_uploads': { diff --git a/mayan/apps/common/serializers.py b/mayan/apps/common/serializers.py new file mode 100644 index 0000000000..ab9520c89e --- /dev/null +++ b/mayan/apps/common/serializers.py @@ -0,0 +1,11 @@ +from __future__ import unicode_literals + +from django.contrib.contenttypes.models import ContentType + +from rest_framework import serializers + + +class ContentTypeSerializer(serializers.ModelSerializer): + class Meta: + fields = ('app_label', 'id', 'model') + model = ContentType diff --git a/mayan/apps/common/tests/test_api.py b/mayan/apps/common/tests/test_api.py new file mode 100644 index 0000000000..4b92bd8342 --- /dev/null +++ b/mayan/apps/common/tests/test_api.py @@ -0,0 +1,11 @@ +from __future__ import unicode_literals + +from django.core.urlresolvers import reverse + +from rest_framework.test import APITestCase + + +class CommonAPITestCase(APITestCase): + def test_content_type_list_view(self): + response = self.client.get(reverse('rest_api:content-type-list')) + self.assertEqual(response.status_code, 200) diff --git a/mayan/apps/common/tests/test_views.py b/mayan/apps/common/tests/test_views.py index ec56a1b9ea..1b4175ff9f 100644 --- a/mayan/apps/common/tests/test_views.py +++ b/mayan/apps/common/tests/test_views.py @@ -11,8 +11,8 @@ from permissions import Permission from permissions.models import Role from permissions.tests.literals import TEST_ROLE_LABEL from user_management.tests import ( - TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, TEST_ADMIN_EMAIL, TEST_GROUP, - TEST_USER_EMAIL, TEST_USER_USERNAME, TEST_USER_PASSWORD + TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, TEST_ADMIN_EMAIL, + TEST_GROUP_NAME, TEST_USER_EMAIL, TEST_USER_USERNAME, TEST_USER_PASSWORD ) from .base import BaseTestCase @@ -33,7 +33,7 @@ class GenericViewTestCase(BaseTestCase): password=TEST_USER_PASSWORD ) - self.group = Group.objects.create(name=TEST_GROUP) + self.group = Group.objects.create(name=TEST_GROUP_NAME) self.role = Role.objects.create(label=TEST_ROLE_LABEL) self.group.user_set.add(self.user) self.role.groups.add(self.group) diff --git a/mayan/apps/common/urls.py b/mayan/apps/common/urls.py index 1eef5af3f1..70141c1ceb 100644 --- a/mayan/apps/common/urls.py +++ b/mayan/apps/common/urls.py @@ -5,6 +5,7 @@ from django.contrib.staticfiles.templatetags.staticfiles import static from django.views.generic import RedirectView from django.views.i18n import javascript_catalog, set_language +from api_views import APIContentTypeList from .views import ( AboutView, CurrentUserDetailsView, CurrentUserEditView, CurrentUserLocaleProfileDetailsView, CurrentUserLocaleProfileEditView, @@ -67,3 +68,10 @@ urlpatterns += [ r'^set_language/$', set_language, name='set_language' ), ] + +api_urls = [ + url( + r'^content_types/$', APIContentTypeList.as_view(), + name='content-type-list' + ), +] diff --git a/mayan/apps/documents/apps.py b/mayan/apps/documents/apps.py index 4d274da0ec..25e6e96884 100644 --- a/mayan/apps/documents/apps.py +++ b/mayan/apps/documents/apps.py @@ -28,6 +28,7 @@ from events.permissions import permission_events_view from mayan.celery import app from navigation import SourceColumn from rest_api.classes import APIEndPoint +from rest_api.fields import DynamicSerializerField from statistics.classes import StatisticNamespace, CharJSLine from .handlers import create_default_document_type @@ -99,39 +100,9 @@ class DocumentsApp(MayanAppConfig): DocumentTypeFilename = self.get_model('DocumentTypeFilename') DocumentVersion = self.get_model('DocumentVersion') - DashboardWidget( - func=new_document_pages_this_month, icon='fa fa-calendar', - label=_('New pages this month'), - link=reverse_lazy( - 'statistics:statistic_detail', - args=('new-document-pages-per-month',) - ) - ) - DashboardWidget( - func=new_documents_this_month, icon='fa fa-calendar', - label=_('New documents this month'), - link=reverse_lazy( - 'statistics:statistic_detail', - args=('new-documents-per-month',) - ) - ) - - DashboardWidget( - icon='fa fa-file', queryset=Document.objects.all(), - label=_('Total documents'), - link=reverse_lazy('documents:document_list') - ) - - DashboardWidget( - icon='fa fa-book', queryset=DocumentType.objects.all(), - label=_('Document types'), - link=reverse_lazy('documents:document_type_list') - ) - - DashboardWidget( - icon='fa fa-trash', queryset=DeletedDocument.objects.all(), - label=_('Documents in trash'), - link=reverse_lazy('documents:document_list_deleted') + DynamicSerializerField.add_serializer( + klass=Document, + serializer_class='documents.serializers.DocumentSerializer' ) MissingItem( diff --git a/mayan/apps/documents/serializers.py b/mayan/apps/documents/serializers.py index 95fe32b76e..82b13cb8b4 100644 --- a/mayan/apps/documents/serializers.py +++ b/mayan/apps/documents/serializers.py @@ -131,6 +131,7 @@ class DocumentSerializer(serializers.HyperlinkedModelSerializer): 'latest_version', 'url', 'uuid', 'versions', ) model = Document + read_only_fields = ('document_type',) class NewDocumentSerializer(serializers.ModelSerializer): diff --git a/mayan/apps/documents/tests/literals.py b/mayan/apps/documents/tests/literals.py index 7af94441aa..36f860848f 100644 --- a/mayan/apps/documents/tests/literals.py +++ b/mayan/apps/documents/tests/literals.py @@ -16,15 +16,18 @@ __all__ = ( 'TEST_NON_ASCII_COMPRESSED_DOCUMENT_PATH', 'TEST_NON_ASCII_DOCUMENT_FILENAME', 'TEST_NON_ASCII_DOCUMENT_PATH', 'TEST_SMALL_DOCUMENT_FILENAME', 'TEST_SMALL_DOCUMENT_PATH', + 'TEST_DOCUMENT_VERSION_COMMENT_EDITED', ) # Filenames TEST_COMPRESSED_DOCUMENTS_FILENAME = 'compressed_documents.zip' TEST_DEU_DOCUMENT_FILENAME = 'deu_website.png' TEST_DOCUMENT_DESCRIPTION = 'test description' +TEST_DOCUMENT_DESCRIPTION_EDITED = 'test document description edited' TEST_DOCUMENT_FILENAME = 'mayan_11_1.pdf' TEST_DOCUMENT_TYPE = 'test_document_type' TEST_DOCUMENT_TYPE_2 = 'test document type 2' +TEST_DOCUMENT_VERSION_COMMENT_EDITED = 'test document version comment edited' TEST_HYBRID_DOCUMENT = 'hybrid_text_and_image.pdf' TEST_MULTI_PAGE_TIFF = 'multi_page.tiff' TEST_NON_ASCII_COMPRESSED_DOCUMENT_FILENAME = 'I18N_title_áéíóúüñÑ.png.zip' diff --git a/mayan/apps/documents/tests/test_api.py b/mayan/apps/documents/tests/test_api.py index 115b44c784..9abb7c561b 100644 --- a/mayan/apps/documents/tests/test_api.py +++ b/mayan/apps/documents/tests/test_api.py @@ -19,8 +19,10 @@ from user_management.tests.literals import ( ) from .literals import ( - TEST_DOCUMENT_FILENAME, TEST_DOCUMENT_PATH, TEST_DOCUMENT_TYPE, - TEST_SMALL_DOCUMENT_FILENAME, TEST_SMALL_DOCUMENT_PATH, + TEST_DOCUMENT_DESCRIPTION_EDITED, TEST_DOCUMENT_FILENAME, + TEST_DOCUMENT_PATH, TEST_DOCUMENT_TYPE, + TEST_DOCUMENT_VERSION_COMMENT_EDITED, TEST_SMALL_DOCUMENT_FILENAME, + TEST_SMALL_DOCUMENT_PATH ) from ..models import Document, DocumentType @@ -113,6 +115,12 @@ class DocumentAPITestCase(APITestCase): self.admin_user.delete() self.document_type.delete() + def _upload_document(self): + with open(TEST_SMALL_DOCUMENT_PATH) as file_object: + self.document = self.document_type.new_document( + file_object=file_object, + ) + def test_document_upload(self): with open(TEST_DOCUMENT_PATH) as file_descriptor: response = self.client.post( @@ -293,5 +301,71 @@ class DocumentAPITestCase(APITestCase): ), mime_type='application/octet-stream; charset=utf-8' ) + def test_document_version_edit_via_patch(self): + self._upload_document() + response = self.client.patch( + reverse( + 'rest_api:documentversion-detail', + args=(self.document.latest_version.pk,) + ), data={'comment': TEST_DOCUMENT_VERSION_COMMENT_EDITED} + ) + + self.assertEqual(response.status_code, 200) + self.document.latest_version.refresh_from_db() + self.assertEqual(self.document.versions.count(), 1) + self.assertEqual( + self.document.latest_version.comment, + TEST_DOCUMENT_VERSION_COMMENT_EDITED + ) + + def test_document_version_edit_via_put(self): + self._upload_document() + response = self.client.put( + reverse( + 'rest_api:documentversion-detail', + args=(self.document.latest_version.pk,) + ), data={'comment': TEST_DOCUMENT_VERSION_COMMENT_EDITED} + ) + + self.assertEqual(response.status_code, 200) + self.document.latest_version.refresh_from_db() + self.assertEqual(self.document.versions.count(), 1) + self.assertEqual( + self.document.latest_version.comment, + TEST_DOCUMENT_VERSION_COMMENT_EDITED + ) + + def test_document_comment_edit_via_patch(self): + self._upload_document() + response = self.client.patch( + reverse( + 'rest_api:document-detail', + args=(self.document.pk,) + ), data={'description': TEST_DOCUMENT_DESCRIPTION_EDITED} + ) + + self.assertEqual(response.status_code, 200) + self.document.refresh_from_db() + self.assertEqual( + self.document.description, + TEST_DOCUMENT_DESCRIPTION_EDITED + ) + + def test_document_comment_edit_via_put(self): + self._upload_document() + response = self.client.put( + reverse( + 'rest_api:document-detail', + args=(self.document.pk,) + ), data={'description': TEST_DOCUMENT_DESCRIPTION_EDITED} + ) + + self.assertEqual(response.status_code, 200) + self.document.refresh_from_db() + self.assertEqual( + self.document.description, + TEST_DOCUMENT_DESCRIPTION_EDITED + ) + # TODO: def test_document_set_document_type(self): # pass diff --git a/mayan/apps/events/api_views.py b/mayan/apps/events/api_views.py new file mode 100644 index 0000000000..37c56e6054 --- /dev/null +++ b/mayan/apps/events/api_views.py @@ -0,0 +1,72 @@ +from __future__ import absolute_import, unicode_literals + +from django.contrib.contenttypes.models import ContentType +from django.core.exceptions import PermissionDenied +from django.http import Http404 +from django.shortcuts import get_object_or_404 + +from actstream.models import Action, any_stream +from rest_framework import generics + +from acls.models import AccessControlList +from permissions import Permission +from rest_api.permissions import MayanPermission + +from .classes import Event +from .permissions import permission_events_view +from .serializers import EventSerializer, EventTypeSerializer + + +class APIObjectEventListView(generics.ListAPIView): + """ + Return a list of events for the specified object. + """ + + serializer_class = EventSerializer + + def get_object(self): + content_type = get_object_or_404( + ContentType, app_label=self.kwargs['app_label'], + model=self.kwargs['model'] + ) + + try: + return content_type.get_object_for_this_type( + pk=self.kwargs['object_id'] + ) + except content_type.model_class().DoesNotExist: + raise Http404 + + def get_queryset(self): + object = self.get_object() + + try: + Permission.check_permissions( + self.request.user, permissions=(permission_events_view,) + ) + except PermissionDenied: + AccessControlList.objects.check_access( + permission_events_view, self.request.user, object + ) + + return any_stream(object) + + +class APIEventTypeListView(generics.ListAPIView): + """ + Returns a list of all the available event types. + """ + + serializer_class = EventTypeSerializer + queryset = sorted(Event.all(), key=lambda event: event.name) + + +class APIEventListView(generics.ListAPIView): + """ + Returns a list of all the available events. + """ + + mayan_view_permissions = {'GET': (permission_events_view,)} + permission_classes = (MayanPermission,) + queryset = Action.objects.all() + serializer_class = EventSerializer diff --git a/mayan/apps/events/apps.py b/mayan/apps/events/apps.py index edd87731b0..620171d205 100644 --- a/mayan/apps/events/apps.py +++ b/mayan/apps/events/apps.py @@ -4,8 +4,8 @@ from django.apps import apps from django.utils.translation import ugettext_lazy as _ from common import MayanAppConfig, menu_tools - from navigation import SourceColumn +from rest_api.classes import APIEndPoint from .links import link_events_list from .licenses import * # NOQA @@ -21,6 +21,8 @@ class EventsApp(MayanAppConfig): super(EventsApp, self).ready() Action = apps.get_model(app_label='actstream', model_name='Action') + APIEndPoint(app=self, version_string='1') + SourceColumn( source=Action, label=_('Timestamp'), attribute='timestamp' ) diff --git a/mayan/apps/events/classes.py b/mayan/apps/events/classes.py index 07b76c9617..1cc0ad7df5 100644 --- a/mayan/apps/events/classes.py +++ b/mayan/apps/events/classes.py @@ -7,26 +7,48 @@ from actstream import action class Event(object): - _labels = {} + _registry = {} + + @classmethod + def all(cls): + return cls._registry.values() + + @classmethod + def get(cls, name): + try: + return cls._registry[name] + except KeyError: + raise KeyError( + _('Unknown or obsolete event type: {0}'.format(name)) + ) @classmethod def get_label(cls, name): try: - return cls._labels[name] - except KeyError: - return _('Unknown or obsolete event type: {0}'.format(name)) + return cls.get(name=name).label + except KeyError as exception: + return unicode(exception) def __init__(self, name, label): self.name = name self.label = label self.event_type = None - self.__class__._labels[name] = label + self.__class__._registry[name] = self + + def get_type(self): + if not self.event_type: + EventType = apps.get_model('events', 'EventType') + + self.event_type, created = EventType.objects.get_or_create( + name=self.name + ) + + return self.event_type def commit(self, actor=None, action_object=None, target=None): - model = apps.get_model('events', 'EventType') - if not self.event_type: - self.event_type, created = model.objects.get_or_create( + EventType = apps.get_model('events', 'EventType') + self.event_type, created = EventType.objects.get_or_create( name=self.name ) diff --git a/mayan/apps/events/models.py b/mayan/apps/events/models.py index bae47fe833..659236cfe8 100644 --- a/mayan/apps/events/models.py +++ b/mayan/apps/events/models.py @@ -13,9 +13,12 @@ class EventType(models.Model): max_length=64, unique=True, verbose_name=_('Name') ) - def __str__(self): - return unicode(Event.get_label(self.name)) - class Meta: verbose_name = _('Event type') verbose_name_plural = _('Event types') + + def __str__(self): + return self.get_class().label + + def get_class(self): + return Event.get_label(self.name) diff --git a/mayan/apps/events/serializers.py b/mayan/apps/events/serializers.py new file mode 100644 index 0000000000..aa3926eb5e --- /dev/null +++ b/mayan/apps/events/serializers.py @@ -0,0 +1,45 @@ +from __future__ import unicode_literals + +from django.utils.six import string_types + +from actstream.models import Action +from rest_framework import serializers + +from common.serializers import ContentTypeSerializer +from rest_api.fields import DynamicSerializerField + +from .classes import Event +from .models import EventType + + +class EventTypeSerializer(serializers.Serializer): + label = serializers.CharField() + name = serializers.CharField() + + def to_representation(self, instance): + if isinstance(instance, Event): + return super(EventTypeSerializer, self).to_representation( + instance + ) + elif isinstance(instance, EventType): + return super(EventTypeSerializer, self).to_representation( + instance.get_class() + ) + elif isinstance(instance, string_types): + return super(EventTypeSerializer, self).to_representation( + Event.get(name=instance) + ) + + +class EventSerializer(serializers.ModelSerializer): + actor = DynamicSerializerField(read_only=True) + target = DynamicSerializerField(read_only=True) + actor_content_type = ContentTypeSerializer(read_only=True) + target_content_type = ContentTypeSerializer(read_only=True) + verb = EventTypeSerializer(read_only=True) + + class Meta: + exclude = ( + 'action_object_content_type', 'action_object_object_id' + ) + model = Action diff --git a/mayan/apps/events/tests/test_api.py b/mayan/apps/events/tests/test_api.py new file mode 100644 index 0000000000..dadc0f531a --- /dev/null +++ b/mayan/apps/events/tests/test_api.py @@ -0,0 +1,11 @@ +from __future__ import unicode_literals + +from django.core.urlresolvers import reverse + +from rest_framework.test import APITestCase + + +class EventAPITestCase(APITestCase): + def test_evet_type_list_view(self): + response = self.client.get(reverse('rest_api:event-type-list')) + self.assertEqual(response.status_code, 200) diff --git a/mayan/apps/events/urls.py b/mayan/apps/events/urls.py index 9b0859a8ef..873a38a576 100644 --- a/mayan/apps/events/urls.py +++ b/mayan/apps/events/urls.py @@ -2,6 +2,9 @@ from __future__ import unicode_literals from django.conf.urls import url +from .api_views import ( + APIEventListView, APIEventTypeListView, APIObjectEventListView +) from .views import EventListView, ObjectEventListView, VerbEventListView urlpatterns = [ @@ -15,3 +18,12 @@ urlpatterns = [ name='events_by_verb' ), ] + +api_urls = [ + url(r'^types/$', APIEventTypeListView.as_view(), name='event-type-list'), + url(r'^events/$', APIEventListView.as_view(), name='event-list'), + url( + r'^object/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/events/$', + APIObjectEventListView.as_view(), name='object-event-list' + ), +] diff --git a/mayan/apps/metadata/serializers.py b/mayan/apps/metadata/serializers.py index 828a7792d5..12b70a50c7 100644 --- a/mayan/apps/metadata/serializers.py +++ b/mayan/apps/metadata/serializers.py @@ -9,7 +9,9 @@ from .models import DocumentMetadata, MetadataType, DocumentTypeMetadataType class MetadataTypeSerializer(serializers.ModelSerializer): class Meta: - fields = ('id', 'name', 'label', 'default', 'lookup') + fields = ( + 'id', 'name', 'label', 'default', 'lookup', 'parser', 'validation' + ) model = MetadataType diff --git a/mayan/apps/metadata/tests/literals.py b/mayan/apps/metadata/tests/literals.py index 2caa5937a5..bb8a2bebc2 100644 --- a/mayan/apps/metadata/tests/literals.py +++ b/mayan/apps/metadata/tests/literals.py @@ -12,5 +12,7 @@ TEST_METADATA_TYPE_LABEL = 'test metadata type' TEST_METADATA_TYPE_LABEL_2 = 'test metadata type label 2' TEST_METADATA_TYPE_NAME = 'test' TEST_METADATA_TYPE_NAME_2 = 'test metadata type name 2' +TEST_METADATA_VALUE = 'test value' +TEST_METADATA_VALUE_EDITED = 'test value edited' TEST_PARSED_VALID_DATE = '2001-01-01' TEST_VALID_DATE = '2001-1-1' diff --git a/mayan/apps/metadata/tests/test_api.py b/mayan/apps/metadata/tests/test_api.py index da70c338f2..79ccff4acb 100644 --- a/mayan/apps/metadata/tests/test_api.py +++ b/mayan/apps/metadata/tests/test_api.py @@ -16,12 +16,10 @@ from ..models import DocumentMetadata, DocumentTypeMetadataType, MetadataType from .literals import ( TEST_METADATA_TYPE_LABEL, TEST_METADATA_TYPE_LABEL_2, - TEST_METADATA_TYPE_NAME, TEST_METADATA_TYPE_NAME_2 + TEST_METADATA_TYPE_NAME, TEST_METADATA_TYPE_NAME_2, TEST_METADATA_VALUE, + TEST_METADATA_VALUE_EDITED ) -TEST_METADATA_VALUE = 'test value' -TEST_METADATA_VALUE_EDITED = 'test value edited' - class MetadataTypeAPITestCase(APITestCase): def setUp(self): @@ -34,8 +32,10 @@ class MetadataTypeAPITestCase(APITestCase): username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD ) - def tearDown(self): - self.admin_user.delete() + def _create_metadata_type(self): + self.metadata_type = MetadataType.objects.create( + label=TEST_METADATA_TYPE_LABEL, name=TEST_METADATA_TYPE_NAME + ) def test_metadata_type_create(self): response = self.client.post( @@ -56,26 +56,35 @@ class MetadataTypeAPITestCase(APITestCase): self.assertEqual(metadata_type.name, TEST_METADATA_TYPE_NAME) def test_metadata_type_delete(self): - metadata_type = MetadataType.objects.create( - label=TEST_METADATA_TYPE_LABEL, name=TEST_METADATA_TYPE_NAME - ) + self._create_metadata_type() response = self.client.delete( - reverse('rest_api:metadatatype-detail', args=(metadata_type.pk,)) + reverse('rest_api:metadatatype-detail', + args=(self.metadata_type.pk,)) ) self.assertEqual(response.status_code, 204) self.assertEqual(MetadataType.objects.count(), 0) - def test_metadata_type_edit(self): - metadata_type = MetadataType.objects.create( - label=TEST_METADATA_TYPE_LABEL, name=TEST_METADATA_TYPE_NAME + def test_metadata_type_detail_view(self): + self._create_metadata_type() + + response = self.client.get( + reverse('rest_api:metadatatype-detail', + args=(self.metadata_type.pk,)) + ) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.data['label'], TEST_METADATA_TYPE_LABEL ) - response = self.client.put( - reverse('rest_api:metadatatype-detail', args=(metadata_type.pk,)), - data={ + def test_metadata_type_edit_via_patch_view(self): + self._create_metadata_type() + + response = self.client.patch( + reverse('rest_api:metadatatype-detail', + args=(self.metadata_type.pk,)), data={ 'label': TEST_METADATA_TYPE_LABEL_2, 'name': TEST_METADATA_TYPE_NAME_2 } @@ -83,10 +92,37 @@ class MetadataTypeAPITestCase(APITestCase): self.assertEqual(response.status_code, 200) - metadata_type.refresh_from_db() + self.metadata_type.refresh_from_db() - self.assertEqual(metadata_type.label, TEST_METADATA_TYPE_LABEL_2) - self.assertEqual(metadata_type.name, TEST_METADATA_TYPE_NAME_2) + self.assertEqual(self.metadata_type.label, TEST_METADATA_TYPE_LABEL_2) + self.assertEqual(self.metadata_type.name, TEST_METADATA_TYPE_NAME_2) + + def test_metadata_type_edit_via_put_view(self): + self._create_metadata_type() + + response = self.client.put( + reverse('rest_api:metadatatype-detail', + args=(self.metadata_type.pk,)), data={ + 'label': TEST_METADATA_TYPE_LABEL_2, + 'name': TEST_METADATA_TYPE_NAME_2 + } + ) + + self.assertEqual(response.status_code, 200) + + self.metadata_type.refresh_from_db() + + self.assertEqual(self.metadata_type.label, TEST_METADATA_TYPE_LABEL_2) + self.assertEqual(self.metadata_type.name, TEST_METADATA_TYPE_NAME_2) + + def test_metadata_type_list_view(self): + self._create_metadata_type() + + response = self.client.get(reverse('rest_api:metadatatype-list')) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.data['results'][0]['label'], TEST_METADATA_TYPE_LABEL + ) class DocumentTypeMetadataTypeAPITestCase(APITestCase): diff --git a/mayan/apps/permissions/api_views.py b/mayan/apps/permissions/api_views.py index 8bc4999430..47f2ea7c48 100644 --- a/mayan/apps/permissions/api_views.py +++ b/mayan/apps/permissions/api_views.py @@ -17,8 +17,7 @@ from .permissions import ( permission_role_view ) from .serializers import ( - PermissionSerializer, RoleNewGroupListSerializer, - RoleNewPermissionSerializer, RoleSerializer, + PermissionSerializer, RoleSerializer, WritableRoleSerializer ) @@ -34,64 +33,12 @@ class APIPermissionList(generics.ListAPIView): return super(APIPermissionList, self).get(*args, **kwargs) -class APIRoleGroupList(generics.ListCreateAPIView): - """ - Returns a list of all the groups that belong to selected role. - """ - - mayan_object_permissions = { - 'GET': (permission_role_view,), - 'POST': (permission_role_edit,) - } - permission_classes = (MayanPermission,) - - def get_serializer_class(self): - if self.request.method == 'GET': - return GroupSerializer - elif self.request.method == 'POST': - return RoleNewGroupListSerializer - - def get_serializer_context(self): - """ - Extra context provided to the serializer class. - """ - return { - 'format': self.format_kwarg, - 'request': self.request, - 'role': self.get_role(), - 'view': self - } - - def get_queryset(self): - role = self.get_role() - - return AccessControlList.objects.filter_by_access( - permission_group_view, self.request.user, - queryset=role.groups.all() - ) - - def get_role(self): - return get_object_or_404(Role, pk=self.kwargs['pk']) - - def perform_create(self, serializer): - serializer.save(role=self.get_role()) - - def post(self, request, *args, **kwargs): - """ - Add a list of groups to the selected role. - """ - - return super(APIRoleGroupList, self).post(request, *args, **kwargs) - - class APIRoleListView(generics.ListCreateAPIView): - serializer_class = RoleSerializer - queryset = Role.objects.all() - - permission_classes = (MayanPermission,) filter_backends = (MayanObjectPermissionsFilter,) mayan_object_permissions = {'GET': (permission_role_view,)} mayan_view_permissions = {'POST': (permission_role_create,)} + permission_classes = (MayanPermission,) + queryset = Role.objects.all() def get(self, *args, **kwargs): """ @@ -100,6 +47,12 @@ class APIRoleListView(generics.ListCreateAPIView): return super(APIRoleListView, self).get(*args, **kwargs) + def get_serializer_class(self): + if self.request.method == 'GET': + return RoleSerializer + elif self.request.method == 'POST': + return WritableRoleSerializer + def post(self, *args, **kwargs): """ Create a new role. @@ -157,16 +110,14 @@ class APIRolePermissionList(generics.ListCreateAPIView): class APIRoleView(generics.RetrieveUpdateDestroyAPIView): - serializer_class = RoleSerializer - queryset = Role.objects.all() - - permission_classes = (MayanPermission,) mayan_object_permissions = { 'GET': (permission_role_view,), 'PUT': (permission_role_edit,), 'PATCH': (permission_role_edit,), 'DELETE': (permission_role_delete,) } + permission_classes = (MayanPermission,) + queryset = Role.objects.all() def delete(self, *args, **kwargs): """ @@ -182,6 +133,12 @@ class APIRoleView(generics.RetrieveUpdateDestroyAPIView): return super(APIRoleView, self).get(*args, **kwargs) + def get_serializer_class(self): + if self.request.method == 'GET': + return RoleSerializer + elif self.request.method in ('PATCH', 'PUT'): + return WritableRoleSerializer + def patch(self, *args, **kwargs): """ Edit the selected role. diff --git a/mayan/apps/permissions/models.py b/mayan/apps/permissions/models.py index a2154964e8..e7bec758fc 100644 --- a/mayan/apps/permissions/models.py +++ b/mayan/apps/permissions/models.py @@ -8,6 +8,7 @@ from django.db import models from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ +from .classes import Permission from .managers import RoleManager, StoredPermissionManager logger = logging.getLogger(__name__) @@ -21,8 +22,6 @@ class StoredPermission(models.Model): objects = StoredPermissionManager() def __init__(self, *args, **kwargs): - from .classes import Permission - super(StoredPermission, self).__init__(*args, **kwargs) try: self.volatile_permission = Permission.get( diff --git a/mayan/apps/permissions/serializers.py b/mayan/apps/permissions/serializers.py index 4ff79d4e7b..375818e93a 100644 --- a/mayan/apps/permissions/serializers.py +++ b/mayan/apps/permissions/serializers.py @@ -28,50 +28,8 @@ class PermissionSerializer(serializers.Serializer): ) -class RoleNewGroupListSerializer(serializers.Serializer): - group_pk_list = serializers.CharField( - help_text=_( - 'Comma separated list of group primary keys to assign to a ' - 'selected role.' - ) - ) - - def create(self, validated_data): - validated_data['role'].groups.clear() - - try: - pk_list = validated_data['group_pk_list'].split(',') - - for group in Group.objects.filter(pk__in=pk_list): - validated_data['role'].groups.add(group) - except Exception as exception: - raise ValidationError(exception) - - return {'group_pk_list': validated_data['group_pk_list']} - - -class RoleNewPermissionSerializer(serializers.Serializer): - permission_pk_list = serializers.CharField( - help_text=_( - 'Comma separated list of permission primary keys to grant to this ' - 'role.' - ) - ) - - def create(self, validated_data): - validated_data['role'].permissions.clear() - - try: - for pk in validated_data['permission_pk_list'].split(','): - stored_permission = Permission.get(pk=pk) - - validated_data['role'].permissions.add(stored_permission) - except KeyError as exception: - raise ValidationError(_('No such permission: %s') % pk) - except Exception as exception: - raise ValidationError(exception) - - return {'permission_pk_list': validated_data['permission_pk_list']} +class RoleSerializer(serializers.HyperlinkedModelSerializer): + groups = GroupSerializer(many=True) class RoleSerializer(serializers.ModelSerializer): @@ -81,3 +39,77 @@ class RoleSerializer(serializers.ModelSerializer): class Meta: fields = ('id', 'label', 'groups', 'permissions') model = Role + + +class WritableRoleSerializer(serializers.HyperlinkedModelSerializer): + groups_pk_list = serializers.CharField( + help_text=_( + 'Comma separated list of groups primary keys to add to, or replace' + ' in this role.' + ), required=False + ) + + permissions_pk_list = serializers.CharField( + help_text=_( + 'Comma separated list of permission primary keys to grant to this ' + 'role.' + ), required=False + ) + + class Meta: + fields = ('groups_pk_list', 'id', 'label', 'permissions_pk_list') + model = Role + + def create(self, validated_data): + result = validated_data.copy() + + self.groups_pk_list = validated_data.pop('groups_pk_list', '') + self.permissions_pk_list = validated_data.pop( + 'permissions_pk_list', '' + ) + + instance = super(WritableRoleSerializer, self).create(validated_data) + + if self.groups_pk_list: + self._add_groups(instance=instance) + + if self.permissions_pk_list: + self._add_permissions(instance=instance) + + return result + + def _add_groups(self, instance): + instance.groups.add( + *Group.objects.filter(pk__in=self.groups_pk_list.split(',')) + ) + + def _add_permissions(self, instance): + for pk in self.permissions_pk_list.split(','): + try: + stored_permission = Permission.get(pk=pk) + instance.permissions.add(stored_permission) + instance.save() + except KeyError: + raise ValidationError(_('No such permission: %s') % pk) + + def update(self, instance, validated_data): + result = validated_data.copy() + + self.groups_pk_list = validated_data.pop('groups_pk_list', '') + self.permissions_pk_list = validated_data.pop( + 'permissions_pk_list', '' + ) + + result = super(WritableRoleSerializer, self).update( + instance, validated_data + ) + + if self.groups_pk_list: + instance.groups.clear() + self._add_groups(instance=instance) + + if self.permissions_pk_list: + instance.permissions.clear() + self._add_permissions(instance=instance) + + return result diff --git a/mayan/apps/permissions/tests/test_api.py b/mayan/apps/permissions/tests/test_api.py new file mode 100644 index 0000000000..5a6d39ce12 --- /dev/null +++ b/mayan/apps/permissions/tests/test_api.py @@ -0,0 +1,166 @@ +from __future__ import unicode_literals + +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group +from django.core.urlresolvers import reverse + +from rest_framework.test import APITestCase + +from user_management.tests.literals import ( + TEST_ADMIN_EMAIL, TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, + TEST_GROUP_NAME +) + +from ..classes import Permission +from ..models import Role +from ..permissions import permission_role_view + +from .literals import TEST_ROLE_LABEL, TEST_ROLE_LABEL_EDITED + + +class PermissionAPITestCase(APITestCase): + def setUp(self): + super(PermissionAPITestCase, self).setUp() + self.admin_user = get_user_model().objects.create_superuser( + username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL, + password=TEST_ADMIN_PASSWORD + ) + + self.client.login( + username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD + ) + + Permission.invalidate_cache() + + def test_permissions_list_view(self): + response = self.client.get(reverse('rest_api:permission-list')) + self.assertEqual(response.status_code, 200) + + def _create_role(self): + self.role = Role.objects.create(label=TEST_ROLE_LABEL) + + def test_roles_list_view(self): + self._create_role() + + response = self.client.get(reverse('rest_api:role-list')) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['results'][0]['label'], TEST_ROLE_LABEL) + + def _role_create_request(self, extra_data=None): + data = { + 'label': TEST_ROLE_LABEL + } + + if extra_data: + data.update(extra_data) + + return self.client.post( + reverse('rest_api:role-list'), data=data + ) + + def test_role_create_view(self): + response = self._role_create_request() + self.assertEqual(response.status_code, 201) + self.assertEqual(Role.objects.count(), 1) + self.assertEqual(Role.objects.first().label, TEST_ROLE_LABEL) + + def _create_group(self): + self.group = Group.objects.create(name=TEST_GROUP_NAME) + + def test_role_create_complex_view(self): + self._create_group() + + response = self._role_create_request( + extra_data={ + 'groups_pk_list': '{}'.format(self.group.pk), + 'permissions_pk_list': '{}'.format(permission_role_view.pk) + } + ) + self.assertEqual(response.status_code, 201) + self.assertEqual(Role.objects.count(), 1) + self.assertEqual(Role.objects.first().label, TEST_ROLE_LABEL) + self.assertQuerysetEqual( + Role.objects.first().groups.all(), (repr(self.group),) + ) + self.assertQuerysetEqual( + Role.objects.first().permissions.all(), + (repr(permission_role_view.stored_permission),) + ) + + def _role_edit_request(self, extra_data=None, request_type='patch'): + data = { + 'label': TEST_ROLE_LABEL_EDITED + } + + if extra_data: + data.update(extra_data) + + return getattr(self.client, request_type)( + reverse('rest_api:role-detail', args=(self.role.pk,)), data=data + ) + + def test_role_edit_via_patch(self): + self._create_role() + response = self._role_edit_request() + self.assertEqual(response.status_code, 200) + self.assertEqual(Role.objects.first().label, TEST_ROLE_LABEL_EDITED) + + def test_role_edit_complex_via_patch(self): + Role.objects.all().delete() + Group.objects.all().delete() + + self._create_role() + self._create_group() + + response = self._role_edit_request( + extra_data={ + 'groups_pk_list': '{}'.format(self.group.pk), + 'permissions_pk_list': '{}'.format(permission_role_view.pk) + } + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(Role.objects.first().label, TEST_ROLE_LABEL_EDITED) + self.assertQuerysetEqual( + Role.objects.first().groups.all(), (repr(self.group),) + ) + self.assertQuerysetEqual( + Role.objects.first().permissions.all(), + (repr(permission_role_view.stored_permission),) + ) + + def test_role_edit_via_put(self): + self._create_role() + response = self._role_edit_request(request_type='put') + self.assertEqual(response.status_code, 200) + self.assertEqual(Role.objects.first().label, TEST_ROLE_LABEL_EDITED) + + def test_role_edit_complex_via_put(self): + Role.objects.all().delete() + Group.objects.all().delete() + + self._create_role() + self._create_group() + + response = self._role_edit_request( + extra_data={ + 'groups_pk_list': '{}'.format(self.group.pk), + 'permissions_pk_list': '{}'.format(permission_role_view.pk) + }, request_type='put' + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(Role.objects.first().label, TEST_ROLE_LABEL_EDITED) + self.assertQuerysetEqual( + Role.objects.first().groups.all(), (repr(self.group),) + ) + self.assertQuerysetEqual( + Role.objects.first().permissions.all(), + (repr(permission_role_view.stored_permission),) + ) + + def test_role_delete_view(self): + self._create_role() + response = self.client.delete( + reverse('rest_api:role-detail', args=(self.role.pk,)) + ) + self.assertEqual(response.status_code, 204) + self.assertEqual(Role.objects.count(), 0) diff --git a/mayan/apps/permissions/tests/test_models.py b/mayan/apps/permissions/tests/test_models.py index 6b8acba7a2..33a6c4fadc 100644 --- a/mayan/apps/permissions/tests/test_models.py +++ b/mayan/apps/permissions/tests/test_models.py @@ -5,7 +5,7 @@ from django.contrib.auth.models import Group from django.core.exceptions import PermissionDenied from common.tests import BaseTestCase -from user_management.tests import TEST_GROUP, TEST_USER_USERNAME +from user_management.tests import TEST_GROUP_NAME, TEST_USER_USERNAME from ..classes import Permission from ..models import Role @@ -20,7 +20,7 @@ class PermissionTestCase(BaseTestCase): self.user = get_user_model().objects.create( username=TEST_USER_USERNAME ) - self.group = Group.objects.create(name=TEST_GROUP) + self.group = Group.objects.create(name=TEST_GROUP_NAME) self.role = Role.objects.create(label=TEST_ROLE_LABEL) def test_no_permissions(self): diff --git a/mayan/apps/permissions/urls.py b/mayan/apps/permissions/urls.py index 4c157d85d4..6044f99aec 100644 --- a/mayan/apps/permissions/urls.py +++ b/mayan/apps/permissions/urls.py @@ -2,10 +2,7 @@ from __future__ import unicode_literals from django.conf.urls import url -from .api_views import ( - APIPermissionList, APIRoleGroupList, APIRoleListView, - APIRolePermissionList, APIRoleView, -) +from .api_views import APIPermissionList, APIRoleListView, APIRoleView from .views import ( RoleCreateView, RoleDeleteView, RoleEditView, RoleListView, SetupRoleMembersView, SetupRolePermissionsView @@ -30,16 +27,8 @@ urlpatterns = [ ] api_urls = [ + url(r'^permissions/$', APIPermissionList.as_view(), name='permission-list'), url(r'^roles/$', APIRoleListView.as_view(), name='role-list'), url(r'^roles/(?P[0-9]+)/$', APIRoleView.as_view(), name='role-detail'), - url( - r'^roles/(?P[0-9]+)/permissions/$', - APIRolePermissionList.as_view(), - name='role-permissions-list' - ), - url( - r'^roles/(?P[0-9]+)/groups/$', APIRoleGroupList.as_view(), - name='role-group-list' - ), url(r'^$', APIPermissionList.as_view(), name='permission-list'), ] diff --git a/mayan/apps/rest_api/fields.py b/mayan/apps/rest_api/fields.py new file mode 100644 index 0000000000..df2a5853d0 --- /dev/null +++ b/mayan/apps/rest_api/fields.py @@ -0,0 +1,30 @@ +from __future__ import unicode_literals + +from django.utils.module_loading import import_string +from django.utils.six import string_types +from django.utils.translation import ugettext_lazy as _ + +from rest_framework import serializers + + +class DynamicSerializerField(serializers.ReadOnlyField): + serializers = {} + + @classmethod + def add_serializer(cls, klass, serializer_class): + if isinstance(klass, string_types): + klass = import_string(klass) + + if isinstance(serializer_class, string_types): + serializer_class = import_string(serializer_class) + + cls.serializers[klass] = serializer_class + + def to_representation(self, value): + for klass, serializer_class in self.serializers.items(): + if isinstance(value, klass): + return serializer_class( + context={'request': self.context['request']} + ).to_representation(instance=value) + + return _('Unable to find serializer class for: %s') % value diff --git a/mayan/apps/user_management/apps.py b/mayan/apps/user_management/apps.py index 1fab8a91f0..08a7e0a5a3 100644 --- a/mayan/apps/user_management/apps.py +++ b/mayan/apps/user_management/apps.py @@ -10,6 +10,7 @@ from common.widgets import two_state_template from metadata import MetadataLookup from navigation import SourceColumn from rest_api.classes import APIEndPoint +from rest_api.fields import DynamicSerializerField from .links import ( link_group_add, link_group_delete, link_group_edit, link_group_list, @@ -48,6 +49,10 @@ class UserManagementApp(MayanAppConfig): User = get_user_model() APIEndPoint(app=self, version_string='1') + DynamicSerializerField.add_serializer( + klass=get_user_model(), + serializer_class='user_management.serializers.UserSerializer' + ) MetadataLookup( description=_('All the groups.'), name='groups', diff --git a/mayan/apps/user_management/serializers.py b/mayan/apps/user_management/serializers.py index b234b6f24d..0ff353f775 100644 --- a/mayan/apps/user_management/serializers.py +++ b/mayan/apps/user_management/serializers.py @@ -46,9 +46,13 @@ class UserGroupListSerializer(serializers.Serializer): class UserSerializer(serializers.HyperlinkedModelSerializer): groups = GroupSerializer(many=True, read_only=True) - + groups_pk_list = serializers.CharField( + help_text=_( + 'List of group primary keys to which to add the user.' + ), required=False + ) password = serializers.CharField( - required=False, style={'input_type': 'password'} + required=False, style={'input_type': 'password'}, write_only=True ) class Meta: @@ -56,28 +60,45 @@ class UserSerializer(serializers.HyperlinkedModelSerializer): 'url': {'view_name': 'rest_api:user-detail'} } fields = ( - 'first_name', 'date_joined', 'email', 'groups', 'id', 'is_active', - 'last_login', 'last_name', 'url', 'username', 'password' + 'first_name', 'date_joined', 'email', 'groups', 'groups_pk_list', + 'id', 'is_active', 'last_login', 'last_name', 'password', 'url', + 'username' ) model = get_user_model() - read_only_fields = ('last_login', 'date_joined') - write_only_fields = ('password',) + read_only_fields = ('groups', 'is_active', 'last_login', 'date_joined') + write_only_fields = ('password', 'group_pk_list') + + def _add_groups(self, instance): + instance.groups.add( + *Group.objects.filter(pk__in=self.groups_pk_list.split(',')) + ) def create(self, validated_data): - validated_data.pop('is_active') - user = get_user_model().objects.create_user(**validated_data) + self.groups_pk_list = validated_data.pop('groups_pk_list', '') + password = validated_data.pop('password', None) + instance = super(UserSerializer, self).create(validated_data) - return user + if password: + instance.set_password(password) + instance.save() + + if self.groups_pk_list: + self._add_groups(instance=instance) + + return instance def update(self, instance, validated_data): + self.groups_pk_list = validated_data.pop('groups_pk_list', '') + if 'password' in validated_data: instance.set_password(validated_data['password']) validated_data.pop('password') - for attr, value in validated_data.items(): - setattr(instance, attr, value) + instance = super(UserSerializer, self).update(instance, validated_data) - instance.save() + if self.groups_pk_list: + instance.groups.clear() + self._add_groups(instance=instance) return instance diff --git a/mayan/apps/user_management/tests/test_api.py b/mayan/apps/user_management/tests/test_api.py index 122902b3f1..d5cb0284f9 100644 --- a/mayan/apps/user_management/tests/test_api.py +++ b/mayan/apps/user_management/tests/test_api.py @@ -47,6 +47,89 @@ class UserManagementUserAPITestCase(APITestCase): user = get_user_model().objects.get(pk=response.data['id']) self.assertEqual(user.username, TEST_USER_USERNAME) + def test_user_create_with_group(self): + group_1 = Group.objects.create(name='test group 1') + + response = self.client.post( + reverse('rest_api:user-list'), data={ + 'email': TEST_USER_EMAIL, 'password': TEST_USER_PASSWORD, + 'username': TEST_USER_USERNAME, + 'groups_pk_list': '{}'.format(group_1.pk) + } + ) + + self.assertEqual(response.status_code, 201) + + user = get_user_model().objects.get(pk=response.data['id']) + self.assertEqual(user.username, TEST_USER_USERNAME) + self.assertQuerysetEqual(user.groups.all(), (repr(group_1),)) + + def test_user_create_with_groups(self): + group_1 = Group.objects.create(name='test group 1') + group_2 = Group.objects.create(name='test group 2') + + response = self.client.post( + reverse('rest_api:user-list'), data={ + 'email': TEST_USER_EMAIL, 'password': TEST_USER_PASSWORD, + 'username': TEST_USER_USERNAME, + 'groups_pk_list': '{},{}'.format(group_1.pk, group_2.pk) + } + ) + + self.assertEqual(response.status_code, 201) + + user = get_user_model().objects.get(pk=response.data['id']) + self.assertEqual(user.username, TEST_USER_USERNAME) + self.assertQuerysetEqual( + user.groups.all().order_by('name'), (repr(group_1), repr(group_2)) + ) + + def test_user_create_login(self): + response = self.client.post( + reverse('rest_api:user-list'), data={ + 'email': TEST_USER_EMAIL, 'password': TEST_USER_PASSWORD, + 'username': TEST_USER_USERNAME, + } + ) + + self.assertEqual(response.status_code, 201) + + self.assertTrue( + self.client.login( + username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD + ) + ) + + def test_user_create_login_password_change(self): + response = self.client.post( + reverse('rest_api:user-list'), data={ + 'email': TEST_USER_EMAIL, 'password': 'bad_password', + 'username': TEST_USER_USERNAME, + } + ) + + self.assertEqual(response.status_code, 201) + + self.assertFalse( + self.client.login( + username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD + ) + ) + + user = get_user_model().objects.get(pk=response.data['id']) + + response = self.client.patch( + reverse('rest_api:user-detail', args=(user.pk,)), data={ + 'password': TEST_USER_PASSWORD, + } + ) + + self.assertTrue( + self.client.login( + username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD + ) + ) + def test_user_edit_via_put(self): user = get_user_model().objects.create_user( email=TEST_USER_EMAIL, password=TEST_USER_PASSWORD, @@ -79,6 +162,44 @@ class UserManagementUserAPITestCase(APITestCase): user.refresh_from_db() self.assertEqual(user.username, TEST_USER_USERNAME_EDITED) + def test_user_edit_remove_groups_via_patch(self): + user = get_user_model().objects.create_user( + email=TEST_USER_EMAIL, password=TEST_USER_PASSWORD, + username=TEST_USER_USERNAME + ) + group_1 = Group.objects.create(name='test group 1') + user.groups.add(group_1) + + response = self.client.patch( + reverse('rest_api:user-detail', args=(user.pk,)), + ) + + self.assertEqual(response.status_code, 200) + + user.refresh_from_db() + self.assertQuerysetEqual( + user.groups.all().order_by('name'), (repr(group_1),) + ) + + def test_user_edit_add_groups_via_patch(self): + user = get_user_model().objects.create_user( + email=TEST_USER_EMAIL, password=TEST_USER_PASSWORD, + username=TEST_USER_USERNAME + ) + group_1 = Group.objects.create(name='test group 1') + + response = self.client.patch( + reverse('rest_api:user-detail', args=(user.pk,)), + data={'groups_pk_list': '{}'.format(group_1.pk)} + ) + + self.assertEqual(response.status_code, 200) + + user.refresh_from_db() + self.assertQuerysetEqual( + user.groups.all().order_by('name'), (repr(group_1),) + ) + def test_user_delete(self): user = get_user_model().objects.create_user( email=TEST_USER_EMAIL, password=TEST_USER_PASSWORD,