From 3a13027be0097015fb6f289b7432473df3d28537 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 28 Jan 2017 22:55:48 -0400 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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'), )