From cae7b8f8c5e5046b8bf793b4fdac89829c41e890 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 15 Feb 2019 04:20:41 -0400 Subject: [PATCH] Add document tags API views These views allow accesing the tags list of a document as well as attaching or removing tags in bulk. The URLs for tag list, attach and remove are added to the DocumentSerializer. Signed-off-by: Roberto Rosario --- mayan/apps/tags/api_views.py | 333 ++++++--------------------------- mayan/apps/tags/apps.py | 42 ++++- mayan/apps/tags/serializers.py | 137 +++++--------- mayan/apps/tags/urls.py | 16 +- 4 files changed, 150 insertions(+), 378 deletions(-) diff --git a/mayan/apps/tags/api_views.py b/mayan/apps/tags/api_views.py index 32ccc9a576..2516c66ee9 100644 --- a/mayan/apps/tags/api_views.py +++ b/mayan/apps/tags/api_views.py @@ -3,7 +3,7 @@ from __future__ import absolute_import, unicode_literals from drf_yasg.utils import swagger_auto_schema from rest_framework import generics, serializers, status, routers, viewsets -from rest_framework.decorators import action, detail_route +from rest_framework.decorators import action from rest_framework.exceptions import ValidationError from rest_framework.response import Response @@ -12,7 +12,9 @@ from mayan.apps.documents.api_views import DocumentViewSet from mayan.apps.documents.models import Document from mayan.apps.documents.permissions import permission_document_view from mayan.apps.documents.serializers import DocumentSerializer -from mayan.apps.rest_api.viewsets import MayanAPIModelViewSet +from mayan.apps.rest_api.viewsets import ( + MayanAPIGenericViewSet, MayanAPIModelViewSet +) from .models import Tag from .permissions import ( @@ -20,14 +22,12 @@ from .permissions import ( permission_tag_edit, permission_tag_remove, permission_tag_view ) from .serializers import ( - DocumentTagAttachSerializer, DocumentTagSerializer, - TagDocumentAttachRemoveSerializer, + DocumentTagAttachRemoveSerializer, TagDocumentAttachRemoveSerializer, TagSerializer, - ) -class TagViewSet(MayanAPIModelViewSet): +class TagAPIViewSet(MayanAPIModelViewSet): lookup_url_kwarg='tag_id' object_permission_map = { 'destroy': permission_tag_delete, @@ -53,7 +53,7 @@ class TagViewSet(MayanAPIModelViewSet): instance = self.get_object() serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - serializer.attach(instance=instance) + serializer.documents_attach(instance=instance) headers = self.get_success_headers(data=serializer.data) return Response( serializer.data, status=status.HTTP_200_OK, headers=headers @@ -86,284 +86,69 @@ class TagViewSet(MayanAPIModelViewSet): instance = self.get_object() serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - serializer.attach(instance=instance) + serializer.documents_remove(instance=instance) headers = self.get_success_headers(data=serializer.data) return Response( serializer.data, status=status.HTTP_200_OK, headers=headers ) -class DocumentTagViewSet(ExternalObjectMixin, viewsets.ReadOnlyModelViewSet): - external_object_class = Document - external_object_pk_url_kwarg = 'document_id' - external_object_permission = permission_tag_view - #lookup_field = 'pk' - object_permission = { - 'list': permission_document_view, - 'retrieve': permission_document_view + +class DocumentTagAPIViewSet(MayanAPIGenericViewSet): + lookup_url_kwarg='document_id' + object_permission_map = { + 'tag_attach': permission_tag_attach, + 'tag_list': permission_tag_view, + 'tag_remove': permission_tag_remove, } - serializer_class = DocumentTagSerializer + queryset = Document.objects.all() @action( - #detail=True, lookup_field='pk', lookup_url_kwarg='document_id', - detail=True, lookup_url_kwarg='document_id', - methods=('post',), serializer_class=DocumentTagAttachSerializer, - url_name='tag-attach', url_path='attach' + detail=False, lookup_url_kwarg='document_id', methods=('post',), + serializer_class=DocumentTagAttachRemoveSerializer, + url_name='tag-attach', url_path='tags/attach' ) - def attach(self, request, *args, **kwargs): - return Response({}) - - - ''' - serializer = DocumentSerializer( - instance=self.get_object().documents.all(), many=True, - context={'request': request} + def tag_attach(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.tags_attach(instance=instance) + headers = self.get_success_headers(data=serializer.data) + return Response( + serializer.data, status=status.HTTP_200_OK, headers=headers ) + + @action( + detail=False, lookup_url_kwarg='document_id', + serializer_class=TagSerializer, url_name='tag-list', + url_path='tags' + ) + def tag_list(self, request, *args, **kwargs): + queryset = self.get_object().get_tags( + permission=permission_tag_view, user=self.request.user + ) + page = self.paginate_queryset(queryset) + + serializer = self.get_serializer( + queryset, many=True, context={'request': request} + ) + + if page is not None: + return self.get_paginated_response(serializer.data) + return Response(serializer.data) - ''' - def get_document(self): - return self.get_external_object() - - def get_queryset(self): - #return self.get_document().get_tags(user=self.request.user).all() - return self.get_document().tags.all() - - #@detail_route(lookup_url_kwarg='tag_id') - #def document_list(self, request, *args, **kwargs): - # serializer = DocumentSerializer( - ## instance=self.get_object().documents.all(), many=True, - # context={'request': request} - # ) - # return Response(serializer.data) - -''' - -class APITagListView(ListCreateAPIView): - """ - get: Returns a list of all the tags. - post: Create a new tag. - """ - object_permission = {'GET': permission_tag_view} - queryset = Tag.objects.all() - serializer_class = TagSerializer - view_permission = {'POST': permission_tag_create} - - -class APITagView(RetrieveUpdateDestroyAPIView): - """ - delete: Delete the selected tag. - get: Return the details of the selected tag. - patch: Edit the selected tag. - put: Edit the selected tag. - """ - lookup_url_kwarg = 'tag_pk' - object_permission = { - 'DELETE': permission_tag_delete, - 'GET': permission_tag_view, - 'PATCH': permission_tag_edit, - 'PUT': permission_tag_edit - } - queryset = Tag.objects.all() - serializer_class = TagSerializer - -## - -class APITagDocumentListView(ExternalObjectMixin, ListCreateAPIView): - """ - get: Returns a list of all the documents tagged by a particular tag. - """ - external_object_class = Tag - external_object_pk_url_kwarg = 'tag_pk' - external_object_permission = permission_tag_view - object_permission = {'GET': permission_document_view} - serializer_class = TagDocumentSerializer - - def get_queryset(self): - return self.get_tag().get_documents(user=self.request.user).all() - - def get_tag(self): - return self.get_external_object() - -## -''' - - -''' -class APITagView(RetrieveDestroyAPIView): - """ - delete: Delete the selected tag document. - get: Return the details of the selected tag document. - """ - lookup_url_kwarg = 'tag_pk' - object_permission = { - 'DELETE': permission_tag_delete, - 'GET': permission_tag_view, - 'PATCH': permission_tag_edit, - 'PUT': permission_tag_edit - } - queryset = Tag.objects.all() - serializer_class = TagSerializer -''' -## -''' -class DocumentSourceMixin(ExternalObjectMixin): - external_object_class = Document - external_object_pk_url_kwarg = 'document_pk' - serializer_class = DocumentTagSerializer - - def get_document(self): - return self.get_external_object() - - def get_external_object_permission(self): - permission_dictionary = { - 'DELETE': permission_tag_remove, - 'GET': permission_tag_view, - 'POST': permission_tag_attach - } - - return permission_dictionary.get(self.request.method) - - def get_queryset(self): - return self.get_document().tags.all() - - def get_serializer_context(self): - context = super(DocumentSourceMixin, self).get_serializer_context() - if self.kwargs: - context.update( - { - 'document': self.get_document(), - } - ) - - return context - - -class APIDocumentTagListView(DocumentSourceMixin, ListCreateAPIView): - """ - get: Returns a list of all the tags attached to a document. - post: Attach a tag to a document. - """ - #external_object_class = Document - #external_object_pk_url_kwarg = 'document_pk' - object_permission = { - 'GET': permission_tag_view, - #'POST': permission_tag_attach - } - #serializer_class = DocumentTagSerializer - - #def get_document(self): - # return self.get_external_object() - - #def get_external_object_permission(self): - # if self.request.method == 'POST': - # return permission_tag_attach - # else: - # return permission_tag_view - - #def get_queryset(self): - # #if self.request.method == 'POST': - # # permission = permission_tag_attach - # #else: - # # permission = permission_tag_view - # - # return self.get_document().tags().all() - # #return self.get_document().get_tags( - ## # permission=permission, user=self.request.user - # #).all() - - """ - def get_serializer_context(self): - """ - Extra context provided to the serializer class. - """ - context = super(APIDocumentTagListView, self).get_serializer_context() - if self.kwargs: - context.update( - { - 'document': self.get_document(), - } - ) - - return context - """ - -class APIDocumentTagView(DocumentSourceMixin, RetrieveDestroyAPIView): - """ - delete: Remove a tag from the selected document. - get: Returns the details of the selected document tag. - """ - #external_object_class = Document - #external_object_pk_url_kwarg = 'document_pk' - lookup_url_kwarg = 'tag_pk' - mayan_object_permission = { - 'GET': permission_tag_view, - 'DELETE': permission_tag_remove - } - #serializer_class = DocumentTagSerializer - - #def get_document(self): - # return self.get_external_object() - - #def get_external_object_permission(self): - # if self.request.method == 'DELETE': - # return permission_tag_remove - # else: - # return permission_tag_view - - """ - def get_queryset(self): - if self.request.method == 'DELETE': - permission = permission_tag_remove - else: - permission = permission_tag_view - - return self.get_document().get_tags( - permission=permission, user=self.request.user - ).all() - """ - - """ - def get_serializer_context(self): - """ - Extra context provided to the serializer class. - """ - context = super(APIDocumentTagView, self).get_serializer_context() - if self.kwargs: - context.update( - { - 'document': self.get_document(), - } - ) - - return context - """ - - def perform_destroy(self, instance): - # try: - from mayan.apps.acls.models import AccessControlList - from rest_framework.generics import get_object_or_404 - - queryset = AccessControlList.objects.restrict_queryset( - permission=permission_tag_remove, queryset=Tag.objects.all(), - user=self.request.user + @action( + detail=False, lookup_field='pk', lookup_url_kwarg='document_id', + methods=('post',), serializer_class=DocumentTagAttachRemoveSerializer, + url_name='tag-remove', url_path='tags/remove' + ) + def tag_remove(self, request, *args, **kwargs): + instance = self.get_object() + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.tags_remove(instance=instance) + headers = self.get_success_headers(data=serializer.data) + return Response( + serializer.data, status=status.HTTP_200_OK, headers=headers ) - instance = get_object_or_404(queryset=queryset, pk=instance.pk) - #instance.attach_to( - # document=self.context['document'], - # user=self.context['request'].user - #) - - instance.remove_from( - document=self.get_document(), user=self.request.user - ) - #instance.documents.remove(self.get_document()) - # except Exception as exception: - # raise ValidationError(exception) - - #def retrieve(self, request, *args, **kwargs): - # instance = self.get_object() - - # serializer = self.get_serializer(instance) - # return Response(serializer.data) -''' diff --git a/mayan/apps/tags/apps.py b/mayan/apps/tags/apps.py index 75209a5517..b38f451f0b 100644 --- a/mayan/apps/tags/apps.py +++ b/mayan/apps/tags/apps.py @@ -9,7 +9,7 @@ from mayan.apps.acls.links import link_acl_list from mayan.apps.acls.permissions import permission_acl_edit, permission_acl_view from mayan.apps.common import ( MayanAppConfig, menu_facet, menu_list_facet, menu_object, menu_main, - menu_multi_item, menu_sidebar + menu_multi_item, menu_secondary ) from mayan.apps.common.classes import ModelAttribute, ModelField from mayan.apps.documents.search import document_page_search, document_search @@ -19,6 +19,8 @@ from mayan.apps.events.links import ( ) from mayan.apps.events.permissions import permission_events_view from mayan.apps.navigation import SourceColumn +from mayan.apps.rest_api.fields import HyperlinkField +from mayan.apps.rest_api.serializers import LazyExtraFieldsSerializerMixin from .events import ( event_tag_attach, event_tag_created, event_tag_edited, event_tag_remove @@ -33,7 +35,10 @@ from .links import ( link_tag_list, link_tag_multiple_delete ) from .menus import menu_tags -from .methods import method_get_tags +from .methods import ( + method_document_tags_attach, method_document_get_tags, + method_document_tags_remove +) from .permissions import ( permission_tag_attach, permission_tag_delete, permission_tag_edit, permission_tag_remove, permission_tag_view @@ -66,7 +71,9 @@ class TagsApp(MayanAppConfig): DocumentTag = self.get_model(model_name='DocumentTag') Tag = self.get_model(model_name='Tag') - Document.add_to_class(name='get_tags', value=method_get_tags) + Document.add_to_class(name='get_tags', value=method_document_get_tags) + Document.add_to_class(name='tags_attach', value=method_document_tags_attach) + Document.add_to_class(name='tags_remove', value=method_document_tags_remove) ModelEventType.register( model=Tag, event_types=( @@ -167,7 +174,7 @@ class TagsApp(MayanAppConfig): link_tag_edit, link_tag_delete ), sources=(Tag,) ) - menu_sidebar.bind_links( + menu_secondary.bind_links( links=( link_document_tag_multiple_attach, link_document_tag_multiple_remove ), sources=( @@ -194,3 +201,30 @@ class TagsApp(MayanAppConfig): dispatch_uid='tags_handler_tag_pre_delete', receiver=handler_tag_pre_delete, sender=Tag ) + + LazyExtraFieldsSerializerMixin.add_field( + dotted_path='mayan.apps.documents.serializers.DocumentSerializer', + field_name='tag_attach_url', + field=HyperlinkField( + lookup_url_kwarg='document_id', + view_name='rest_api:document-tag-attach' + ) + ) + + LazyExtraFieldsSerializerMixin.add_field( + dotted_path='mayan.apps.documents.serializers.DocumentSerializer', + field_name='tag_list_url', + field=HyperlinkField( + lookup_url_kwarg='document_id', + view_name='rest_api:document-tag-list' + ) + ) + + LazyExtraFieldsSerializerMixin.add_field( + dotted_path='mayan.apps.documents.serializers.DocumentSerializer', + field_name='tag_remove_url', + field=HyperlinkField( + lookup_url_kwarg='document_id', + view_name='rest_api:document-tag-remove' + ) + ) diff --git a/mayan/apps/tags/serializers.py b/mayan/apps/tags/serializers.py index fe4c62a0d0..75b58f0684 100644 --- a/mayan/apps/tags/serializers.py +++ b/mayan/apps/tags/serializers.py @@ -16,16 +16,42 @@ from .models import Tag from .permissions import permission_tag_attach +class DocumentTagAttachRemoveSerializer(ExternalObjectListSerializerMixin, serializers.Serializer): + tag_id_list = serializers.CharField( + help_text=_( + 'Comma separated list of tag primary keys to be attached or ' + 'removed.' + ), required=False, write_only=True + ) + + class Meta: + external_object_list_model = Tag + external_object_list_permission = permission_tag_attach + external_object_list_pk_list_field = 'tag_id_list' + + def tags_attach(self, instance): + instance.tags_attach( + queryset=self.get_external_object_list(), + _user=self.context['request'].user + ) + + def tags_remove(self, instance): + instance.tags_remove( + queryset=self.get_external_object_list(), + _user=self.context['request'].user + ) + + class TagSerializer(serializers.HyperlinkedModelSerializer): - attach_url = serializers.HyperlinkedIdentityField( + document_attach_url = serializers.HyperlinkedIdentityField( lookup_url_kwarg='tag_id', view_name='rest_api:tag-document-attach' ) - documents_url = serializers.HyperlinkedIdentityField( + document_list_url = serializers.HyperlinkedIdentityField( lookup_url_kwarg='tag_id', view_name='rest_api:tag-document-list' ) - remove_url = serializers.HyperlinkedIdentityField( + document_remove_url = serializers.HyperlinkedIdentityField( lookup_url_kwarg='tag_id', view_name='rest_api:tag-document-remove' ) @@ -37,94 +63,14 @@ class TagSerializer(serializers.HyperlinkedModelSerializer): }, } fields = ( - 'attach_url', 'color', 'documents_url', 'label', 'id', - 'remove_url', 'url' + 'color', 'document_attach_url', 'document_list_url', + 'document_remove_url','label', 'id', 'url' ) model = Tag -class DocumentTagSerializer(TagSerializer): - #document_attach_url = serializers.HyperlinkedIdentityField( - # lookup_url_kwarg='document_id', view_name='rest_api:document-tag-attach' - #) - - class Meta(TagSerializer.Meta): - #fields = TagSerializer.Meta.fields + ('document_attach_url',) - fields = TagSerializer.Meta.fields - #fields = TagSerializer.Meta.fields + ('document_tag_url', 'tag_pk') - #fields = TagSerializer.Meta.fields + ('tag_pk',) - #read_only_fields = TagSerializer.Meta.fields + ('document_attach_url',) - read_only_fields = TagSerializer.Meta.fields - #read_only_fields = TagSerializer.Meta.fields - - #related_models = ('tags',) - #related_models_kwargs = { - # 'documents': { - ## #'pk_list': 'tags_pk_list', 'model': Tag, - # #'model': Tag, - # 'object_permission': {'create': permission_tag_attach}, - # #'add_method': 'add', 'add_method_kwargs': 'document' - # } - #} - - ''' - def create(self, validated_data): - """ - queryset = Tag.objects.filter(pk__in=validated_data['tags_pk_list'].split(',')) - - #permission = self.object_permission.get('create') - - #if permission: - queryset = AccessControlList.objects.restrict_queryset( - permission=permission_tag_attach, queryset=queryset, - user=self.context['request'].user - ) - - for tag in queryset.all(): - tag.attach_to( - document=self.context['document'], - user=self.context['request'].user - ) - """ - queryset = AccessControlList.objects.restrict_queryset( - permission=permission_tag_attach, queryset=Tag.objects.all(), - user=self.context['request'].user - ) - tag = get_object_or_404(queryset=queryset, pk=validated_data['tag_pk']) - tag.attach_to( - document=self.context['document'], - user=self.context['request'].user - ) - return tag - #return None - ''' - - def get_document_tag_url(self, instance): - return reverse( - viewname='rest_api:document-tag-detail', kwargs={ - 'document_id': self.context['document'].pk, - 'tag_id': instance.pk - }, request=self.context['request'], format=self.context['format'] - ) - - -class DocumentTagAttachSerializer(serializers.Serializer): - tags_pk_list = serializers.CharField( - help_text=_( - 'Comma separated list of tag primary keys that will be attached ' - 'to this document.' - ), write_only=True - ) - - class TagDocumentAttachRemoveSerializer(ExternalObjectListSerializerMixin, serializers.Serializer): - document_id = serializers.CharField( - help_text=_( - 'Primary key of document to which this tag will be attached or ' - 'removed.' - ), required=False, write_only=True - ) - documents_id_list = serializers.CharField( + document_id_list = serializers.CharField( help_text=_( 'Comma separated list of document primary keys to which this ' 'tag will be attached or removed.' @@ -134,15 +80,16 @@ class TagDocumentAttachRemoveSerializer(ExternalObjectListSerializerMixin, seria class Meta: external_object_list_model = Document external_object_list_permission = permission_tag_attach - external_object_list_pk_field = 'document_id' external_object_list_pk_list_field = 'document_id_list' - def attach(self, instance): - queryset = self.get_external_object_list() - for document in queryset: - instance.attach_to(document=document, user=self.context['request'].user) + def document_attach(self, instance): + instance.documents_attach( + queryset=self.get_external_object_list(), + _user=self.context['request'].user + ) - def remove(self, instance): - queryset = self.get_external_object_list() - for document in queryset: - instance.attach_from(document=document, user=self.context['request'].user) + def document_remove(self, instance): + instance.documents_remove( + queryset=self.get_external_object_list(), + _user=self.context['request'].user + ) diff --git a/mayan/apps/tags/urls.py b/mayan/apps/tags/urls.py index 89df42fb9e..13b5b7e434 100644 --- a/mayan/apps/tags/urls.py +++ b/mayan/apps/tags/urls.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.conf.urls import url -from .api_views import DocumentTagViewSet, TagViewSet +from .api_views import DocumentTagAPIViewSet, TagAPIViewSet from .views import ( DocumentTagListView, TagAttachActionView, TagCreateView, @@ -58,9 +58,15 @@ urlpatterns = [ ] api_router_entries = ( - {'prefix': r'tags', 'viewset': TagViewSet, 'basename': 'tag'}, + {'prefix': r'tags', 'viewset': TagAPIViewSet, 'basename': 'tag'}, { - 'prefix': r'documents/(?P\d+)/tags', - 'viewset': DocumentTagViewSet, 'basename': 'document-tag' - }, + 'prefix': r'documents/(?P\d+)', + 'viewset': DocumentTagAPIViewSet, 'basename': 'document' + } ) + + +# { +# 'prefix': r'permission_namespaces/(?P[^/.]+)/permissions', +# 'viewset': PermissionViewSet, 'basename': 'permission' +# },