From 059237e4b9fcec552185282c56493d0d520b4d5b Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 27 Jan 2017 02:06:09 -0400 Subject: [PATCH 01/16] Backport user creation and update API endpoints improvements. --- mayan/apps/user_management/serializers.py | 57 +++++++--- mayan/apps/user_management/tests/test_api.py | 111 ++++++++++++++++++- 2 files changed, 148 insertions(+), 20 deletions(-) diff --git a/mayan/apps/user_management/serializers.py b/mayan/apps/user_management/serializers.py index 2430629d35..83b4ca54c3 100644 --- a/mayan/apps/user_management/serializers.py +++ b/mayan/apps/user_management/serializers.py @@ -2,8 +2,10 @@ from __future__ import unicode_literals from django.contrib.auth import get_user_model from django.contrib.auth.models import Group +from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers +from rest_framework.exceptions import ValidationError class GroupSerializer(serializers.HyperlinkedModelSerializer): @@ -21,10 +23,16 @@ class GroupSerializer(serializers.HyperlinkedModelSerializer): class UserSerializer(serializers.HyperlinkedModelSerializer): - groups = GroupSerializer(many=True) + groups = GroupSerializer(read_only=True, many=True) + + groups_pk_list = serializers.CharField( + help_text=_( + 'List of group primary keys to which to the user.' + ), required=False + ) password = serializers.CharField( - required=False, style={'input_type': 'password'} + required=False, style={'input_type': 'password'}, write_only=True ) class Meta: @@ -32,30 +40,47 @@ 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 create(self, validated_data): - validated_data.pop('groups') - validated_data.pop('is_active') - user = get_user_model().objects.create_user(**validated_data) + 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 groups_pk_list: + try: + for group in Group.objects.filter(pk__in=groups_pk_list.split(',')): + instance.groups.add(group) + except Exception as exception: + raise ValidationError(exception) + + return instance def update(self, instance, validated_data): - validated_data.pop('groups') - 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) + result = super(UserSerializer, self).update(instance, validated_data) - instance.save() + groups_pk_list = validated_data.pop('groups_pk_list', '') - return instance + if groups_pk_list: + instance.groups.clear() + try: + for group in Group.objects.filter(pk__in=groups_pk_list.split(',')): + instance.groups.add(group) + except Exception as exception: + raise ValidationError(exception) + + return result diff --git a/mayan/apps/user_management/tests/test_api.py b/mayan/apps/user_management/tests/test_api.py index e5eb2b561c..8a952da42c 100644 --- a/mayan/apps/user_management/tests/test_api.py +++ b/mayan/apps/user_management/tests/test_api.py @@ -1,7 +1,7 @@ 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 @@ -47,6 +47,43 @@ class UserManagementAPITestCase(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={ @@ -57,7 +94,35 @@ class UserManagementAPITestCase(APITestCase): self.assertEqual(response.status_code, 201) - get_user_model().objects.get(pk=response.data['id']) + 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( @@ -81,7 +146,7 @@ class UserManagementAPITestCase(APITestCase): user.refresh_from_db() self.assertEqual(user.username, TEST_USER_USERNAME_EDITED) - def test_document_type_edit_via_patch(self): + def test_user_edit_via_patch(self): user = get_user_model().objects.create_user( email=TEST_USER_EMAIL, password=TEST_USER_PASSWORD, username=TEST_USER_USERNAME @@ -97,7 +162,45 @@ class UserManagementAPITestCase(APITestCase): user.refresh_from_db() self.assertEqual(user.username, TEST_USER_USERNAME_EDITED) - def test_document_type_delete(self): + 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, username=TEST_USER_USERNAME From b55b6dc117d23ef26c3ebd4912552455cb369040 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 28 Jan 2017 11:20:24 -0400 Subject: [PATCH 02/16] Add API test for document version comment editing via PUT and PATCH. GitLab issue #348. --- mayan/apps/documents/tests/literals.py | 2 ++ mayan/apps/documents/tests/test_api.py | 39 ++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/mayan/apps/documents/tests/literals.py b/mayan/apps/documents/tests/literals.py index 4956dbbee1..b2eceadfc5 100644 --- a/mayan/apps/documents/tests/literals.py +++ b/mayan/apps/documents/tests/literals.py @@ -16,6 +16,7 @@ __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 @@ -25,6 +26,7 @@ TEST_DOCUMENT_DESCRIPTION = 'test description' 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 579104e5a8..afc33471d6 100644 --- a/mayan/apps/documents/tests/test_api.py +++ b/mayan/apps/documents/tests/test_api.py @@ -22,6 +22,7 @@ from user_management.tests.literals import ( from .literals import ( TEST_DOCUMENT_FILENAME, TEST_DOCUMENT_PATH, TEST_DOCUMENT_TYPE, TEST_SMALL_DOCUMENT_CHECKSUM, TEST_SMALL_DOCUMENT_PATH, + TEST_DOCUMENT_VERSION_COMMENT_EDITED ) from ..models import Document, DocumentType, HASH_FUNCTION @@ -114,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 +300,37 @@ class DocumentAPITestCase(APITestCase): del(buf) + def _edit_document_version(self): + return self.client.patch( + reverse( + 'rest_api:documentversion-detail', + args=(self.document.latest_version.pk,) + ), data={'comment': TEST_DOCUMENT_VERSION_COMMENT_EDITED} + ) + + def test_document_version_edit_via_patch(self): + self._upload_document() + response = self._edit_document_version() + + 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._edit_document_version() + + 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 + ) + # TODO: def test_document_set_document_type(self): # pass From 288de26d2b357106c8026b246ff46032f21e6721 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 28 Jan 2017 18:13:27 -0400 Subject: [PATCH 03/16] Model.save() errors are being silently ignored by Django REST Framework. Force raising a ValidationError on Model.save() errors. GitLab issue #348 and #349. --- mayan/apps/documents/serializers.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/mayan/apps/documents/serializers.py b/mayan/apps/documents/serializers.py index 8a07cb12d4..4dbc21a21f 100644 --- a/mayan/apps/documents/serializers.py +++ b/mayan/apps/documents/serializers.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from rest_framework import serializers +from rest_framework.serializers import ValidationError from common.models import SharedUploadedFile @@ -81,6 +82,16 @@ class DocumentVersionSerializer(serializers.HyperlinkedModelSerializer): model = DocumentVersion read_only_fields = ('document', 'file') + def update(self, instance, validated_data): + try: + super(DocumentVersionSerializer, self).update( + instance, validated_data + ) + except Exception as exception: + raise ValidationError(unicode(exception)) + + return instance + class DocumentVersionRevertSerializer(DocumentVersionSerializer): class Meta(DocumentVersionSerializer.Meta): From 2f67f0cffa92891b99d0d1610119e82a34cefc11 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 28 Jan 2017 18:15:25 -0400 Subject: [PATCH 04/16] Improve document version comment API tests. Add document description update API tests. GitLab issues #348 and #349. --- mayan/apps/documents/tests/literals.py | 1 + mayan/apps/documents/tests/test_api.py | 54 +++++++++++++++++++++----- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/mayan/apps/documents/tests/literals.py b/mayan/apps/documents/tests/literals.py index b2eceadfc5..289b67d526 100644 --- a/mayan/apps/documents/tests/literals.py +++ b/mayan/apps/documents/tests/literals.py @@ -23,6 +23,7 @@ __all__ = ( 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' diff --git a/mayan/apps/documents/tests/test_api.py b/mayan/apps/documents/tests/test_api.py index afc33471d6..7195b01b15 100644 --- a/mayan/apps/documents/tests/test_api.py +++ b/mayan/apps/documents/tests/test_api.py @@ -20,9 +20,9 @@ from user_management.tests.literals import ( ) from .literals import ( - TEST_DOCUMENT_FILENAME, TEST_DOCUMENT_PATH, TEST_DOCUMENT_TYPE, - TEST_SMALL_DOCUMENT_CHECKSUM, TEST_SMALL_DOCUMENT_PATH, - TEST_DOCUMENT_VERSION_COMMENT_EDITED + TEST_DOCUMENT_FILENAME, TEST_DOCUMENT_DESCRIPTION_EDITED, + TEST_DOCUMENT_PATH, TEST_DOCUMENT_TYPE, TEST_SMALL_DOCUMENT_CHECKSUM, + TEST_SMALL_DOCUMENT_PATH, TEST_DOCUMENT_VERSION_COMMENT_EDITED ) from ..models import Document, DocumentType, HASH_FUNCTION @@ -300,18 +300,15 @@ class DocumentAPITestCase(APITestCase): del(buf) - def _edit_document_version(self): - return self.client.patch( + 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} ) - def test_document_version_edit_via_patch(self): - self._upload_document() - response = self._edit_document_version() - self.assertEqual(response.status_code, 200) self.document.latest_version.refresh_from_db() self.assertEqual(self.document.versions.count(), 1) @@ -322,7 +319,12 @@ class DocumentAPITestCase(APITestCase): def test_document_version_edit_via_put(self): self._upload_document() - response = self._edit_document_version() + 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() @@ -332,5 +334,37 @@ class DocumentAPITestCase(APITestCase): 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 From 7edb08a7cda4c32ed05ea2ecdeba38a7f19e45cf Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 28 Jan 2017 20:50:42 -0400 Subject: [PATCH 05/16] This is an issue of the DRF browseable interface. The exception is shown in the Swagger interface or via CURL. Revert "Model.save() errors are being silently ignored by Django REST Framework." This reverts commit 288de26d2b357106c8026b246ff46032f21e6721. --- mayan/apps/documents/serializers.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/mayan/apps/documents/serializers.py b/mayan/apps/documents/serializers.py index 4dbc21a21f..8a07cb12d4 100644 --- a/mayan/apps/documents/serializers.py +++ b/mayan/apps/documents/serializers.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals from rest_framework import serializers -from rest_framework.serializers import ValidationError from common.models import SharedUploadedFile @@ -82,16 +81,6 @@ class DocumentVersionSerializer(serializers.HyperlinkedModelSerializer): model = DocumentVersion read_only_fields = ('document', 'file') - def update(self, instance, validated_data): - try: - super(DocumentVersionSerializer, self).update( - instance, validated_data - ) - except Exception as exception: - raise ValidationError(unicode(exception)) - - return instance - class DocumentVersionRevertSerializer(DocumentVersionSerializer): class Meta(DocumentVersionSerializer.Meta): From 3a13027be0097015fb6f289b7432473df3d28537 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 28 Jan 2017 22:55:48 -0400 Subject: [PATCH 06/16] Add content type list API view to the common app. --- mayan/apps/common/api_views.py | 16 ++++++++++++++++ mayan/apps/common/apps.py | 3 +++ mayan/apps/common/serializers.py | 11 +++++++++++ mayan/apps/common/tests/test_api.py | 11 +++++++++++ mayan/apps/common/urls.py | 5 +++++ 5 files changed, 46 insertions(+) create mode 100644 mayan/apps/common/api_views.py create mode 100644 mayan/apps/common/serializers.py create mode 100644 mayan/apps/common/tests/test_api.py 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 457ecc2073..ace2edd1c0 100644 --- a/mayan/apps/common/apps.py +++ b/mayan/apps/common/apps.py @@ -15,6 +15,7 @@ from django.db.models.signals import post_save from django.utils.translation import ugettext_lazy as _ from mayan.celery import app +from rest_api.classes import APIEndPoint from .classes import Package from .handlers import ( @@ -74,6 +75,8 @@ class CommonApp(MayanAppConfig): def ready(self): super(CommonApp, self).ready() + APIEndPoint(app=self, version_string='1') + Package(label='Django', license_text=''' Copyright (c) Django Software Foundation and individual contributors. All rights reserved. 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/urls.py b/mayan/apps/common/urls.py index 11b6e1acf8..2db113ce26 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 +from api_views import APIContentTypeList from .views import ( AboutView, CurrentUserDetailsView, CurrentUserEditView, CurrentUserLocaleProfileDetailsView, CurrentUserLocaleProfileEditView, @@ -66,3 +67,7 @@ urlpatterns += patterns( name='set_language' ), ) + +api_urls = [ + url(r'^content_types/$', APIContentTypeList.as_view(), name='content-type-list'), +] From 562a9dea704d04138df50d301081108687a7636b Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 28 Jan 2017 22:56:22 -0400 Subject: [PATCH 07/16] Add event type list api view. --- mayan/apps/events/api_views.py | 15 +++++++++++++++ mayan/apps/events/apps.py | 4 +++- mayan/apps/events/classes.py | 23 ++++++++++++++++++----- mayan/apps/events/serializers.py | 8 ++++++++ mayan/apps/events/tests/test_api.py | 11 +++++++++++ mayan/apps/events/urls.py | 5 +++++ 6 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 mayan/apps/events/api_views.py create mode 100644 mayan/apps/events/serializers.py create mode 100644 mayan/apps/events/tests/test_api.py diff --git a/mayan/apps/events/api_views.py b/mayan/apps/events/api_views.py new file mode 100644 index 0000000000..a317c64c15 --- /dev/null +++ b/mayan/apps/events/api_views.py @@ -0,0 +1,15 @@ +from __future__ import unicode_literals + +from rest_framework import generics + +from .classes import Event +from .serializers import EventSerializer + + +class APIEventTypeList(generics.ListAPIView): + """ + Returns a list of all the available event types. + """ + + serializer_class = EventSerializer + queryset = sorted(Event.all(), key=lambda event: event.name) diff --git a/mayan/apps/events/apps.py b/mayan/apps/events/apps.py index 74a6ddc661..190382c352 100644 --- a/mayan/apps/events/apps.py +++ b/mayan/apps/events/apps.py @@ -6,8 +6,8 @@ from actstream.models import Action from common import MayanAppConfig, menu_tools from common.classes import Package - from navigation import SourceColumn +from rest_api.classes import APIEndPoint from .links import link_events_list from .widgets import event_type_link @@ -21,6 +21,8 @@ class EventsApp(MayanAppConfig): def ready(self): super(EventsApp, self).ready() + APIEndPoint(app=self, version_string='1') + Package(label='django-activity-stream', license_text=''' Copyright (c) 2010-2015, Justin Quick All rights reserved. diff --git a/mayan/apps/events/classes.py b/mayan/apps/events/classes.py index 07b76c9617..d986bfb52b 100644 --- a/mayan/apps/events/classes.py +++ b/mayan/apps/events/classes.py @@ -7,20 +7,33 @@ 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 commit(self, actor=None, action_object=None, target=None): model = apps.get_model('events', 'EventType') diff --git a/mayan/apps/events/serializers.py b/mayan/apps/events/serializers.py new file mode 100644 index 0000000000..6223363413 --- /dev/null +++ b/mayan/apps/events/serializers.py @@ -0,0 +1,8 @@ +from __future__ import unicode_literals + +from rest_framework import serializers + + +class EventSerializer(serializers.Serializer): + label = serializers.CharField() + name = serializers.CharField() 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 3f87a57e58..b256e1585e 100644 --- a/mayan/apps/events/urls.py +++ b/mayan/apps/events/urls.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from django.conf.urls import patterns, url +from .api_views import APIEventTypeList from .views import EventListView, ObjectEventListView, VerbEventListView urlpatterns = patterns( @@ -16,3 +17,7 @@ urlpatterns = patterns( name='events_by_verb' ), ) + +api_urls = [ + url(r'^$', APIEventTypeList.as_view(), name='event-type-list'), +] From 914c5e7146be5cd2c83933eeea57f53fd4dee67f Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 28 Jan 2017 22:56:52 -0400 Subject: [PATCH 08/16] Code cleanups. --- mayan/apps/common/tests/decorators.py | 8 ++++---- mayan/apps/common/tests/mixins.py | 2 +- mayan/apps/common/tests/test_views.py | 1 - mayan/apps/events/tests/test_views.py | 1 - 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/mayan/apps/common/tests/decorators.py b/mayan/apps/common/tests/decorators.py index 9c1b5b8186..264e5cc7cf 100644 --- a/mayan/apps/common/tests/decorators.py +++ b/mayan/apps/common/tests/decorators.py @@ -1,5 +1,5 @@ def skip_file_descriptor_check(func): - def func_wrapper(item): - item._skip_file_descriptor_test = True - return func(item) - return func_wrapper + def func_wrapper(item): + item._skip_file_descriptor_test = True + return func(item) + return func_wrapper diff --git a/mayan/apps/common/tests/mixins.py b/mayan/apps/common/tests/mixins.py index 9140288b49..d5bfaf2e31 100644 --- a/mayan/apps/common/tests/mixins.py +++ b/mayan/apps/common/tests/mixins.py @@ -66,7 +66,7 @@ class TempfileCheckMixin(object): msg='Orphan temporary file. The number of temporary files and/or ' 'directories at the start and at the end of the test are not the ' 'same. Orphan entries: {}'.format( - ','.join(final_temporary_items-self._temporary_items) + ','.join(final_temporary_items - self._temporary_items) ) ) super(TempfileCheckMixin, self).tearDown() diff --git a/mayan/apps/common/tests/test_views.py b/mayan/apps/common/tests/test_views.py index 9541d06436..6fa95a8721 100644 --- a/mayan/apps/common/tests/test_views.py +++ b/mayan/apps/common/tests/test_views.py @@ -6,7 +6,6 @@ from django.contrib.auth.models import Group from django.core.urlresolvers import clear_url_caches, reverse from django.http import HttpResponse from django.template import Context, Template -from django.test import TestCase from permissions import Permission from permissions.models import Role diff --git a/mayan/apps/events/tests/test_views.py b/mayan/apps/events/tests/test_views.py index e136607b75..7be4003b80 100644 --- a/mayan/apps/events/tests/test_views.py +++ b/mayan/apps/events/tests/test_views.py @@ -2,7 +2,6 @@ from __future__ import absolute_import, unicode_literals from django.contrib.contenttypes.models import ContentType -from acls.models import AccessControlList from documents.tests.test_views import GenericDocumentViewTestCase from user_management.tests import ( TEST_USER_USERNAME, TEST_USER_PASSWORD From 6fbd166f3c2211a5a3e7746c3639da0dd4f32cf0 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 29 Jan 2017 01:25:59 -0400 Subject: [PATCH 09/16] Add event list API view --- mayan/apps/documents/apps.py | 5 ++++ mayan/apps/events/api_views.py | 21 +++++++++++++--- mayan/apps/events/classes.py | 17 ++++++++++--- mayan/apps/events/models.py | 9 ++++--- mayan/apps/events/serializers.py | 39 +++++++++++++++++++++++++++++- mayan/apps/events/urls.py | 5 ++-- mayan/apps/rest_api/fields.py | 30 +++++++++++++++++++++++ mayan/apps/user_management/apps.py | 5 ++++ 8 files changed, 118 insertions(+), 13 deletions(-) create mode 100644 mayan/apps/rest_api/fields.py diff --git a/mayan/apps/documents/apps.py b/mayan/apps/documents/apps.py index 1abfd005e5..9c7ff6c36d 100644 --- a/mayan/apps/documents/apps.py +++ b/mayan/apps/documents/apps.py @@ -29,6 +29,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 @@ -94,6 +95,10 @@ class DocumentsApp(MayanAppConfig): DocumentTypeFilename = self.get_model('DocumentTypeFilename') DocumentVersion = self.get_model('DocumentVersion') + DynamicSerializerField.add_serializer( + klass=Document, + serializer_class='documents.serializers.DocumentSerializer' + ) MissingItem( label=_('Create a document type'), description=_( diff --git a/mayan/apps/events/api_views.py b/mayan/apps/events/api_views.py index a317c64c15..9882cc3ce6 100644 --- a/mayan/apps/events/api_views.py +++ b/mayan/apps/events/api_views.py @@ -1,15 +1,30 @@ from __future__ import unicode_literals +from actstream.models import Action from rest_framework import generics +from rest_api.permissions import MayanPermission + from .classes import Event -from .serializers import EventSerializer +from .permissions import permission_events_view +from .serializers import EventSerializer, EventTypeSerializer -class APIEventTypeList(generics.ListAPIView): +class APIEventTypeListView(generics.ListAPIView): """ Returns a list of all the available event types. """ - serializer_class = EventSerializer + 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/classes.py b/mayan/apps/events/classes.py index d986bfb52b..1cc0ad7df5 100644 --- a/mayan/apps/events/classes.py +++ b/mayan/apps/events/classes.py @@ -35,11 +35,20 @@ class Event(object): self.event_type = None self.__class__._registry[name] = self - def commit(self, actor=None, action_object=None, target=None): - model = apps.get_model('events', 'EventType') - + def get_type(self): 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 + ) + + return self.event_type + + def commit(self, actor=None, action_object=None, target=None): + if not self.event_type: + 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 index 6223363413..aa3926eb5e 100644 --- a/mayan/apps/events/serializers.py +++ b/mayan/apps/events/serializers.py @@ -1,8 +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 -class EventSerializer(serializers.Serializer): +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/urls.py b/mayan/apps/events/urls.py index b256e1585e..0641d17c31 100644 --- a/mayan/apps/events/urls.py +++ b/mayan/apps/events/urls.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.conf.urls import patterns, url -from .api_views import APIEventTypeList +from .api_views import APIEventListView, APIEventTypeListView from .views import EventListView, ObjectEventListView, VerbEventListView urlpatterns = patterns( @@ -19,5 +19,6 @@ urlpatterns = patterns( ) api_urls = [ - url(r'^$', APIEventTypeList.as_view(), name='event-type-list'), + url(r'^types/$', APIEventTypeListView.as_view(), name='event-type-list'), + url(r'^events/$', APIEventListView.as_view(), name='event-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 992b11dc9c..3bf3f437ab 100644 --- a/mayan/apps/user_management/apps.py +++ b/mayan/apps/user_management/apps.py @@ -12,6 +12,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, @@ -42,6 +43,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', From 8a1f426715c33cea76d25aeb7b721100590ffe44 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 29 Jan 2017 02:41:51 -0400 Subject: [PATCH 10/16] Add object event list API view. --- mayan/apps/events/api_views.py | 46 ++++++++++++++++++++++++++++++++-- mayan/apps/events/urls.py | 8 +++++- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/mayan/apps/events/api_views.py b/mayan/apps/events/api_views.py index 9882cc3ce6..37c56e6054 100644 --- a/mayan/apps/events/api_views.py +++ b/mayan/apps/events/api_views.py @@ -1,8 +1,15 @@ -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals -from actstream.models import Action +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 @@ -10,6 +17,41 @@ 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. diff --git a/mayan/apps/events/urls.py b/mayan/apps/events/urls.py index 0641d17c31..0fb5673b0a 100644 --- a/mayan/apps/events/urls.py +++ b/mayan/apps/events/urls.py @@ -2,7 +2,9 @@ from __future__ import unicode_literals from django.conf.urls import patterns, url -from .api_views import APIEventListView, APIEventTypeListView +from .api_views import ( + APIEventListView, APIEventTypeListView, APIObjectEventListView +) from .views import EventListView, ObjectEventListView, VerbEventListView urlpatterns = patterns( @@ -21,4 +23,8 @@ urlpatterns = patterns( 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' + ), ] From c81a15f4f0ee9caffad51a43c59640bf68424fa2 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 30 Jan 2017 00:21:57 -0400 Subject: [PATCH 11/16] Add permission list API endpoint. --- mayan/apps/permissions/api_views.py | 15 +++++++++- mayan/apps/permissions/classes.py | 43 ++++++++++++++------------- mayan/apps/permissions/serializers.py | 18 ++++++++++- mayan/apps/permissions/urls.py | 3 +- 4 files changed, 56 insertions(+), 23 deletions(-) diff --git a/mayan/apps/permissions/api_views.py b/mayan/apps/permissions/api_views.py index b63ba07a20..3e0b1db34c 100644 --- a/mayan/apps/permissions/api_views.py +++ b/mayan/apps/permissions/api_views.py @@ -5,12 +5,25 @@ from rest_framework import generics from rest_api.filters import MayanObjectPermissionsFilter from rest_api.permissions import MayanPermission +from .classes import Permission from .models import Role from .permissions import ( permission_role_create, permission_role_delete, permission_role_edit, permission_role_view ) -from .serializers import RoleSerializer +from .serializers import PermissionSerializer, RoleSerializer + + +class APIPermissionList(generics.ListAPIView): + serializer_class = PermissionSerializer + queryset = Permission.all() + + def get(self, *args, **kwargs): + """ + Returns a list of all the available permissions. + """ + + return super(APIPermissionList, self).get(*args, **kwargs) class APIRoleListView(generics.ListCreateAPIView): diff --git a/mayan/apps/permissions/classes.py b/mayan/apps/permissions/classes.py index a5b6dcb093..0de231d8b0 100644 --- a/mayan/apps/permissions/classes.py +++ b/mayan/apps/permissions/classes.py @@ -45,12 +45,15 @@ class PermissionNamespace(object): class Permission(object): - _stored_permissions_cache = {} _permissions = {} + _stored_permissions_cache = {} @classmethod - def invalidate_cache(cls): - cls._stored_permissions_cache = {} + def all(cls): + # Return sorted permisions by namespace.name + return sorted( + cls._permissions.values(), key=lambda x: x.namespace.name + ) @classmethod def check_permissions(cls, requester, permissions): @@ -62,6 +65,14 @@ class Permission(object): raise PermissionDenied(_('Insufficient permissions.')) + @classmethod + def get(cls, get_dict, proxy_only=False): + if 'pk' in get_dict: + if proxy_only: + return cls._permissions[get_dict['pk']] + else: + return cls._permissions[get_dict['pk']].stored_permission + @classmethod def get_for_holder(cls, holder): StoredPermission = apps.get_model( @@ -71,19 +82,8 @@ class Permission(object): return StoredPermission.get_for_holder(holder) @classmethod - def all(cls): - # Return sorted permisions by namespace.name - return sorted( - cls._permissions.values(), key=lambda x: x.namespace.name - ) - - @classmethod - def get(cls, get_dict, proxy_only=False): - if 'pk' in get_dict: - if proxy_only: - return cls._permissions[get_dict['pk']] - else: - return cls._permissions[get_dict['pk']].stored_permission + def invalidate_cache(cls): + cls._stored_permissions_cache = {} def __init__(self, namespace, name, label): self.namespace = namespace @@ -92,16 +92,15 @@ class Permission(object): self.pk = self.uuid self.__class__._permissions[self.uuid] = self + def __repr__(self): + return self.pk + def __unicode__(self): return unicode(self.label) def __str__(self): return str(self.__unicode__()) - @property - def uuid(self): - return '%s.%s' % (self.namespace.name, self.name) - @property def stored_permission(self): StoredPermission = apps.get_model( @@ -120,3 +119,7 @@ class Permission(object): self.uuid ] = stored_permission return stored_permission + + @property + def uuid(self): + return '%s.%s' % (self.namespace.name, self.name) diff --git a/mayan/apps/permissions/serializers.py b/mayan/apps/permissions/serializers.py index 9e39fa5160..8850b5db45 100644 --- a/mayan/apps/permissions/serializers.py +++ b/mayan/apps/permissions/serializers.py @@ -2,7 +2,23 @@ from __future__ import unicode_literals from rest_framework import serializers -from .models import Role +from .models import Role, StoredPermission + + +class PermissionSerializer(serializers.Serializer): + namespace = serializers.CharField() + pk = serializers.CharField() + label = serializers.CharField() + + def to_representation(self, instance): + if isinstance(instance, StoredPermission): + return super(PermissionSerializer, self).to_representation( + instance.volatile_permission + ) + else: + return super(PermissionSerializer, self).to_representation( + instance + ) class RoleSerializer(serializers.ModelSerializer): diff --git a/mayan/apps/permissions/urls.py b/mayan/apps/permissions/urls.py index bb18ef10a3..17923c3d63 100644 --- a/mayan/apps/permissions/urls.py +++ b/mayan/apps/permissions/urls.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.conf.urls import patterns, url -from .api_views import APIRoleListView, APIRoleView +from .api_views import APIPermissionList, APIRoleListView, APIRoleView from .views import ( RoleCreateView, RoleDeleteView, RoleEditView, RoleListView, SetupRoleMembersView, SetupRolePermissionsView @@ -31,4 +31,5 @@ api_urls = patterns( '', url(r'^roles/$', APIRoleListView.as_view(), name='role-list'), url(r'^roles/(?P[0-9]+)/$', APIRoleView.as_view(), name='role-detail'), + url(r'^permissions/$', APIPermissionList.as_view(), name='permission-list'), ) From 3634284c5c8203e2b08b8520152cd8843905155e Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 30 Jan 2017 00:46:56 -0400 Subject: [PATCH 12/16] Set the document type in the document serializer to read only. This change improves PUT requests and changing the document type via the API is not supported anyways. --- mayan/apps/documents/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mayan/apps/documents/serializers.py b/mayan/apps/documents/serializers.py index 4dbc21a21f..6ca01dab70 100644 --- a/mayan/apps/documents/serializers.py +++ b/mayan/apps/documents/serializers.py @@ -161,6 +161,7 @@ class DocumentSerializer(serializers.HyperlinkedModelSerializer): 'latest_version', 'url', 'uuid', 'versions', ) model = Document + read_only_fields = ('document_type',) class NewDocumentSerializer(serializers.ModelSerializer): From e2c8f7b3fca5db2cb51244a7316d2e8b8ce09985 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 30 Jan 2017 18:33:06 -0400 Subject: [PATCH 13/16] Add complete role API endpoints (role create, patch, put, delete, add/update role groups, add/update role permissions). --- mayan/apps/permissions/api_views.py | 28 ++-- mayan/apps/permissions/models.py | 3 +- mayan/apps/permissions/serializers.py | 89 +++++++++++- mayan/apps/permissions/tests/test_api.py | 165 +++++++++++++++++++++++ mayan/apps/permissions/urls.py | 2 +- 5 files changed, 272 insertions(+), 15 deletions(-) create mode 100644 mayan/apps/permissions/tests/test_api.py diff --git a/mayan/apps/permissions/api_views.py b/mayan/apps/permissions/api_views.py index 3e0b1db34c..c6777bc76c 100644 --- a/mayan/apps/permissions/api_views.py +++ b/mayan/apps/permissions/api_views.py @@ -11,7 +11,9 @@ from .permissions import ( permission_role_create, permission_role_delete, permission_role_edit, permission_role_view ) -from .serializers import PermissionSerializer, RoleSerializer +from .serializers import ( + PermissionSerializer, RoleSerializer, WritableRoleSerializer +) class APIPermissionList(generics.ListAPIView): @@ -27,13 +29,11 @@ class APIPermissionList(generics.ListAPIView): 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): """ @@ -42,6 +42,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. @@ -51,16 +57,14 @@ class APIRoleListView(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): """ @@ -76,6 +80,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 35b55cf0b9..af35e599ea 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 8850b5db45..f311070999 100644 --- a/mayan/apps/permissions/serializers.py +++ b/mayan/apps/permissions/serializers.py @@ -1,7 +1,14 @@ from __future__ import unicode_literals -from rest_framework import serializers +from django.contrib.auth.models import Group +from django.utils.translation import ugettext_lazy as _ +from rest_framework import serializers +from rest_framework.exceptions import ValidationError + +from user_management.serializers import GroupSerializer + +from .classes import Permission from .models import Role, StoredPermission @@ -21,7 +28,83 @@ class PermissionSerializer(serializers.Serializer): ) -class RoleSerializer(serializers.ModelSerializer): +class RoleSerializer(serializers.HyperlinkedModelSerializer): + groups = GroupSerializer(many=True) + class Meta: - fields = ('id', 'label') + fields = ('groups', 'id', 'label') 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(get_dict={'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..c00399d072 --- /dev/null +++ b/mayan/apps/permissions/tests/test_api.py @@ -0,0 +1,165 @@ +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 +) + +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) + + 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/urls.py b/mayan/apps/permissions/urls.py index 17923c3d63..04670cf0a9 100644 --- a/mayan/apps/permissions/urls.py +++ b/mayan/apps/permissions/urls.py @@ -29,7 +29,7 @@ urlpatterns = patterns( api_urls = patterns( '', + 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'^permissions/$', APIPermissionList.as_view(), name='permission-list'), ) From be39120e3f10965befd325c4a9d9fd93c0ee2ab2 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 2 Feb 2017 01:06:28 -0400 Subject: [PATCH 14/16] Enable the parser and validation fields of the metadata type serializer. --- mayan/apps/metadata/serializers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 From 1030db134b3cbd10ffb95fc0f0251abb8b083d89 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 2 Feb 2017 01:08:07 -0400 Subject: [PATCH 15/16] Add metadata type list, patch and detail API view tests. --- mayan/apps/metadata/tests/literals.py | 2 + mayan/apps/metadata/tests/test_api.py | 74 ++++++++++++++++++++------- 2 files changed, 57 insertions(+), 19 deletions(-) diff --git a/mayan/apps/metadata/tests/literals.py b/mayan/apps/metadata/tests/literals.py index 8d0bde2696..951207e495 100644 --- a/mayan/apps/metadata/tests/literals.py +++ b/mayan/apps/metadata/tests/literals.py @@ -12,5 +12,7 @@ TEST_METADATA_TYPE_LABEL = 'test' 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): From 8c3a19cf56aff2af68dfc2ef06e19390e09ee09a Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 2 Feb 2017 01:42:27 -0400 Subject: [PATCH 16/16] Bump version to 2.1.7. Add release note and changelog. --- HISTORY.rst | 9 ++++ docs/releases/2.1.7.rst | 102 ++++++++++++++++++++++++++++++++++++++++ docs/releases/index.rst | 1 + mayan/__init__.py | 4 +- 4 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 docs/releases/2.1.7.rst diff --git a/HISTORY.rst b/HISTORY.rst index a9f77c3c2e..3573e665ac 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,3 +1,12 @@ +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) ================= - Fix variable name typo in the rotation transformation class. 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 144af6e26f..de40317d59 100644 --- a/docs/releases/index.rst +++ b/docs/releases/index.rst @@ -22,6 +22,7 @@ versions of the documentation contain the release notes for any later releases. .. toctree:: :maxdepth: 1 + 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'