diff --git a/docs/releases/2.2.rst b/docs/releases/2.2.rst index 9c01310a9a..c26d6b3b25 100644 --- a/docs/releases/2.2.rst +++ b/docs/releases/2.2.rst @@ -7,6 +7,36 @@ Released: XX, 2017 What's new ========== +API changes +----------- +Document API URLs updated to use the resource/sub resource paradigm. +Before: + +/api/documents/document_version +/api/documents/document_pages + +After: +/api/documents//version/ +/api/documents//version//pages/ + +Fields that reference a resource by URL now have the suffix '_url' to differentiate +then from fields include the resource. + +Before: + +'document': '/api/documents/10' + +After: + +'document_url': '/api/documents/10' + +Removal of the document version revert API endpoint. To revert a document to a +previous version using the API, use the DELETE verb to delete the most recent +document version to be discarded. + +Pages data is no longer included as part of the version data. Instead a link to +the document version's pages has been added by the name 'pages_url'. This +resolved to '/api/documents//pages//pages'. Other changes ------------- diff --git a/mayan/apps/documents/api_views.py b/mayan/apps/documents/api_views.py index 215b5f7a3c..e2430e4854 100644 --- a/mayan/apps/documents/api_views.py +++ b/mayan/apps/documents/api_views.py @@ -15,25 +15,24 @@ from rest_api.permissions import MayanPermission from .literals import DOCUMENT_IMAGE_TASK_TIMEOUT from .models import ( - Document, DocumentPage, DocumentType, DocumentVersion, RecentDocument + Document, DocumentType, RecentDocument ) from .permissions import ( permission_document_create, permission_document_delete, permission_document_download, permission_document_edit, permission_document_new_version, permission_document_properties_edit, permission_document_restore, permission_document_trash, - permission_document_version_revert, permission_document_view, - permission_document_type_create, permission_document_type_delete, - permission_document_type_edit, permission_document_type_view + permission_document_view, permission_document_type_create, + permission_document_type_delete, permission_document_type_edit, + permission_document_type_view ) from .runtime import cache_storage_backend from .serializers import ( DeletedDocumentSerializer, DocumentPageSerializer, DocumentSerializer, DocumentTypeSerializer, DocumentVersionSerializer, - DocumentVersionRevertSerializer, NewDocumentSerializer, - NewDocumentVersionSerializer, RecentDocumentSerializer, - WritableDocumentSerializer, WritableDocumentTypeSerializer, - WritableDocumentVersionSerializer + NewDocumentSerializer, NewDocumentVersionSerializer, + RecentDocumentSerializer, WritableDocumentSerializer, + WritableDocumentTypeSerializer, WritableDocumentVersionSerializer ) from .tasks import task_generate_document_page_image @@ -158,12 +157,15 @@ class APIDocumentVersionDownloadView(DownloadMixin, generics.RetrieveAPIView): paramType: path type: number """ + lookup_url_kwarg = 'version_pk' - mayan_object_permissions = { - 'GET': (permission_document_download,) - } - permission_classes = (MayanPermission,) - queryset = DocumentVersion.objects.all() + def get_document(self): + document = get_object_or_404(Document, pk=self.kwargs['pk']) + + AccessControlList.objects.check_access( + permission_document_view, self.request.user, document + ) + return document def get_file(self): instance = self.get_object() @@ -172,6 +174,9 @@ class APIDocumentVersionDownloadView(DownloadMixin, generics.RetrieveAPIView): def get_serializer_class(self): return None + def get_queryset(self): + return self.get_document().versions.all() + def retrieve(self, request, *args, **kwargs): return self.render_to_response() @@ -204,6 +209,13 @@ class APIDocumentView(generics.RetrieveUpdateDestroyAPIView): return super(APIDocumentView, self).get(*args, **kwargs) + def get_serializer_context(self): + return { + 'format': self.format_kwarg, + 'request': self.request, + 'view': self + } + def get_serializer_class(self): if self.request.method == 'GET': return DocumentSerializer @@ -242,12 +254,28 @@ class APIDocumentPageImageView(generics.RetrieveAPIView): type: number """ - mayan_object_permissions = { - 'GET': (permission_document_view,), - } - mayan_permission_attribute_check = 'document' - permission_classes = (MayanPermission,) - queryset = DocumentPage.objects.all() + lookup_url_kwarg = 'page_pk' + + def get_document(self): + if self.request.method == 'GET': + permission_required = permission_document_view + else: + permission_required = permission_document_edit + + document = get_object_or_404(Document, pk=self.kwargs['pk']) + + AccessControlList.objects.check_access( + permission_required, self.request.user, document + ) + return document + + def get_document_version(self): + return get_object_or_404( + self.get_document().versions.all(), pk=self.kwargs['version_pk'] + ) + + def get_queryset(self): + return self.get_document_version().pages.all() def get_serializer_class(self): return None @@ -281,14 +309,7 @@ class APIDocumentPageView(generics.RetrieveUpdateAPIView): Returns the selected document page details. """ - mayan_object_permissions = { - 'GET': (permission_document_view,), - 'PUT': (permission_document_edit,), - 'PATCH': (permission_document_edit,) - } - mayan_permission_attribute_check = 'document' - permission_classes = (MayanPermission,) - queryset = DocumentPage.objects.all() + lookup_url_kwarg = 'page_pk' serializer_class = DocumentPageSerializer def get(self, *args, **kwargs): @@ -298,6 +319,27 @@ class APIDocumentPageView(generics.RetrieveUpdateAPIView): return super(APIDocumentPageView, self).get(*args, **kwargs) + def get_document(self): + if self.request.method == 'GET': + permission_required = permission_document_view + else: + permission_required = permission_document_edit + + document = get_object_or_404(Document, pk=self.kwargs['pk']) + + AccessControlList.objects.check_access( + permission_required, self.request.user, document + ) + return document + + def get_document_version(self): + return get_object_or_404( + self.get_document().versions.all(), pk=self.kwargs['version_pk'] + ) + + def get_queryset(self): + return self.get_document_version().pages.all() + def patch(self, *args, **kwargs): """ Edit the selected document page. @@ -424,6 +466,33 @@ class APIRecentDocumentListView(generics.ListAPIView): return super(APIRecentDocumentListView, self).get(*args, **kwargs) +class APIDocumentVersionPageListView(generics.ListAPIView): + serializer_class = DocumentPageSerializer + + def get_document(self): + document = get_object_or_404(Document, pk=self.kwargs['pk']) + + AccessControlList.objects.check_access( + permission_document_view, self.request.user, document + ) + return document + + def get_document_version(self): + return get_object_or_404( + self.get_document().versions.all(), pk=self.kwargs['version_pk'] + ) + + def get_queryset(self): + return self.get_document_version().pages.all() + + def get_serializer_context(self): + return { + 'format': self.format_kwarg, + 'request': self.request, + 'view': self + } + + class APIDocumentVersionsListView(generics.ListCreateAPIView): """ Return a list of the selected document's versions. @@ -468,19 +537,35 @@ class APIDocumentVersionsListView(generics.ListCreateAPIView): return Response(status=status.HTTP_202_ACCEPTED, headers=headers) -class APIDocumentVersionView(generics.RetrieveUpdateAPIView): +class APIDocumentVersionView(generics.RetrieveUpdateDestroyAPIView): """ Returns the selected document version details. """ - mayan_object_permissions = { - 'GET': (permission_document_view,), - 'PATCH': (permission_document_edit,), - 'PUT': (permission_document_edit,), - } - mayan_permission_attribute_check = 'document' - permission_classes = (MayanPermission,) - queryset = DocumentVersion.objects.all() + lookup_url_kwarg = 'version_pk' + + def delete(self, *args, **kwargs): + """ + Delete the selected document version. + """ + + return super(APIDocumentVersionView, self).delete(*args, **kwargs) + + def get_document(self): + if self.request.method == 'GET': + permission_required = permission_document_view + else: + permission_required = permission_document_edit + + document = get_object_or_404(Document, pk=self.kwargs['pk']) + + AccessControlList.objects.check_access( + permission_required, self.request.user, document + ) + return document + + def get_queryset(self): + return self.get_document().versions.all() def get_serializer_class(self): if self.request.method == 'GET': @@ -488,6 +573,13 @@ class APIDocumentVersionView(generics.RetrieveUpdateAPIView): else: return WritableDocumentVersionSerializer + def get_serializer_context(self): + return { + 'format': self.format_kwarg, + 'request': self.request, + 'view': self + } + def patch(self, *args, **kwargs): """ Edit the selected document version. @@ -501,21 +593,3 @@ class APIDocumentVersionView(generics.RetrieveUpdateAPIView): """ return super(APIDocumentVersionView, self).put(*args, **kwargs) - - -class APIDocumentVersionRevertView(generics.GenericAPIView): - """ - Revert to an earlier document version. - """ - - mayan_object_permissions = { - 'POST': (permission_document_version_revert,) - } - mayan_permission_attribute_check = 'document' - permission_classes = (MayanPermission,) - queryset = DocumentVersion.objects.all() - serializer_class = DocumentVersionRevertSerializer - - def post(self, *args, **kwargs): - self.get_object().revert(_user=self.request.user) - return Response(status=status.HTTP_200_OK) diff --git a/mayan/apps/documents/serializers.py b/mayan/apps/documents/serializers.py index 726fcb42f1..5520700b7d 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.reverse import reverse from common.models import SharedUploadedFile @@ -12,19 +13,37 @@ from .tasks import task_upload_new_version class DocumentPageSerializer(serializers.HyperlinkedModelSerializer): - image = serializers.HyperlinkedIdentityField( - view_name='rest_api:documentpage-image' - ) + document_version_url = serializers.SerializerMethodField() + image_url = serializers.SerializerMethodField() + url = serializers.SerializerMethodField() class Meta: - extra_kwargs = { - 'url': {'view_name': 'rest_api:documentpage-detail'}, - 'document_version': { - 'view_name': 'rest_api:documentversion-detail' - } - } + fields = ('document_version_url', 'image_url', 'page_number', 'url') model = DocumentPage + def get_document_version_url(self, instance): + return reverse( + 'rest_api:documentversion-detail', args=( + instance.document.pk, instance.document_version.pk, + ), request=self.context['request'], format=self.context['format'] + ) + + def get_image_url(self, instance): + return reverse( + 'rest_api:documentpage-image', args=( + instance.document.pk, instance.document_version.pk, + instance.pk, + ), request=self.context['request'], format=self.context['format'] + ) + + def get_url(self, instance): + return reverse( + 'rest_api:documentpage-detail', args=( + instance.document.pk, instance.document_version.pk, + instance.pk, + ), request=self.context['request'], format=self.context['format'] + ) + class DocumentTypeSerializer(serializers.HyperlinkedModelSerializer): documents_url = serializers.HyperlinkedIdentityField( @@ -69,44 +88,96 @@ class WritableDocumentTypeSerializer(serializers.ModelSerializer): class DocumentVersionSerializer(serializers.HyperlinkedModelSerializer): - pages = DocumentPageSerializer(many=True, required=False, read_only=True) - revert = serializers.HyperlinkedIdentityField( - view_name='rest_api:documentversion-revert' - ) + document_url = serializers.SerializerMethodField() + download_url = serializers.SerializerMethodField() + pages_url = serializers.SerializerMethodField() + url = serializers.SerializerMethodField() class Meta: extra_kwargs = { 'document': {'view_name': 'rest_api:document-detail'}, 'file': {'use_url': False}, - 'url': {'view_name': 'rest_api:documentversion-detail'}, } + fields = ( + 'checksum', 'comment', 'document_url', 'download_url', 'encoding', + 'file', 'mimetype', 'pages_url', 'timestamp', 'url' + ) model = DocumentVersion read_only_fields = ('document', 'file') + def get_document_url(self, instance): + return reverse( + 'rest_api:document-detail', args=( + instance.document.pk, + ), request=self.context['request'], format=self.context['format'] + ) + + def get_download_url(self, instance): + return reverse( + 'rest_api:documentversion-download', args=( + instance.document.pk, instance.pk, + ), request=self.context['request'], format=self.context['format'] + ) + + def get_pages_url(self, instance): + return reverse( + 'rest_api:documentversion-page-list', args=( + instance.document.pk, instance.pk, + ), request=self.context['request'], format=self.context['format'] + ) + + def get_url(self, instance): + return reverse( + 'rest_api:documentversion-detail', args=( + instance.document.pk, instance.pk, + ), request=self.context['request'], format=self.context['format'] + ) + class WritableDocumentVersionSerializer(serializers.ModelSerializer): - document = serializers.HyperlinkedIdentityField( - view_name='rest_api:document-detail' - ) - pages = DocumentPageSerializer(many=True, required=False, read_only=True) - revert = serializers.HyperlinkedIdentityField( - view_name='rest_api:documentversion-revert' - ) - url = serializers.HyperlinkedIdentityField( - view_name='rest_api:documentversion-detail' - ) + document_url = serializers.SerializerMethodField() + download_url = serializers.SerializerMethodField() + pages_url = serializers.SerializerMethodField() + url = serializers.SerializerMethodField() class Meta: extra_kwargs = { 'file': {'use_url': False}, } + fields = ( + 'checksum', 'comment', 'document_url', 'download_url', 'encoding', + 'file', 'mimetype', 'pages_url', 'timestamp', 'url' + ) model = DocumentVersion read_only_fields = ('document', 'file') + def get_document_url(self, instance): + return reverse( + 'rest_api:document-detail', args=( + instance.document.pk, + ), request=self.context['request'], format=self.context['format'] + ) -class DocumentVersionRevertSerializer(DocumentVersionSerializer): - class Meta(DocumentVersionSerializer.Meta): - read_only_fields = ('comment', 'document',) + def get_download_url(self, instance): + return reverse( + 'rest_api:documentversion-download', args=( + instance.document.pk, instance.pk, + ), request=self.context['request'], format=self.context['format'] + ) + + def get_pages_url(self, instance): + return reverse( + 'rest_api:documentversion-page-list', args=( + instance.document.pk, instance.pk, + ), request=self.context['request'], format=self.context['format'] + ) + + def get_url(self, instance): + return reverse( + 'rest_api:documentversion-detail', args=( + instance.document.pk, instance.pk, + ), request=self.context['request'], format=self.context['format'] + ) class NewDocumentVersionSerializer(serializers.Serializer): @@ -152,9 +223,9 @@ class DeletedDocumentSerializer(serializers.HyperlinkedModelSerializer): class DocumentSerializer(serializers.HyperlinkedModelSerializer): - document_type_label = serializers.SerializerMethodField() + document_type = DocumentTypeSerializer() latest_version = DocumentVersionSerializer(many=False, read_only=True) - versions = serializers.HyperlinkedIdentityField( + versions_url = serializers.HyperlinkedIdentityField( view_name='rest_api:document-version-list', ) @@ -164,19 +235,15 @@ class DocumentSerializer(serializers.HyperlinkedModelSerializer): 'url': {'view_name': 'rest_api:document-detail'} } fields = ( - 'date_added', 'description', 'document_type', - 'document_type_label', 'id', 'label', 'language', - 'latest_version', 'url', 'uuid', 'versions', + 'date_added', 'description', 'document_type', 'id', 'label', + 'language', 'latest_version', 'url', 'uuid', 'versions_url', ) model = Document read_only_fields = ('document_type',) - def get_document_type_label(self, instance): - return instance.document_type.label - class WritableDocumentSerializer(serializers.ModelSerializer): - document_type_label = serializers.SerializerMethodField() + document_type = DocumentTypeSerializer(read_only=True) latest_version = DocumentVersionSerializer(many=False, read_only=True) versions = serializers.HyperlinkedIdentityField( view_name='rest_api:document-version-list', @@ -187,16 +254,12 @@ class WritableDocumentSerializer(serializers.ModelSerializer): class Meta: fields = ( - 'date_added', 'description', 'document_type', - 'document_type_label', 'id', 'label', 'language', - 'latest_version', 'url', 'uuid', 'versions', + 'date_added', 'description', 'document_type', 'id', 'label', + 'language', 'latest_version', 'url', 'uuid', 'versions', ) model = Document read_only_fields = ('document_type',) - def get_document_type_label(self, instance): - return instance.document_type.label - class NewDocumentSerializer(serializers.ModelSerializer): file = serializers.FileField(write_only=True) diff --git a/mayan/apps/documents/tests/test_api.py b/mayan/apps/documents/tests/test_api.py index 501ceed1de..637def9413 100644 --- a/mayan/apps/documents/tests/test_api.py +++ b/mayan/apps/documents/tests/test_api.py @@ -10,7 +10,6 @@ from django.contrib.auth import get_user_model from django.core.urlresolvers import reverse from django.test import override_settings from django.utils.encoding import force_text -from django.utils.six import BytesIO from django_downloadview import assert_download_response from rest_framework import status @@ -201,17 +200,18 @@ class DocumentAPITestCase(APITestCase): self.assertEqual(document.versions.count(), 2) - document_version = document.versions.first() + last_version = document.versions.last() - self.client.post( + self.client.delete( reverse( - 'rest_api:documentversion-revert', args=(document_version.pk,) + 'rest_api:documentversion-detail', + args=(document.pk, last_version.pk,) ) ) self.assertEqual(document.versions.count(), 1) - self.assertEqual(document_version, document.latest_version) + self.assertEqual(document.versions.first(), document.latest_version) def test_document_download(self): with open(TEST_SMALL_DOCUMENT_PATH) as file_object: @@ -242,7 +242,7 @@ class DocumentAPITestCase(APITestCase): response = self.client.get( reverse( 'rest_api:documentversion-download', - args=(latest_version.pk,) + args=(document.pk, latest_version.pk,) ) ) @@ -260,7 +260,7 @@ class DocumentAPITestCase(APITestCase): response = self.client.patch( reverse( 'rest_api:documentversion-detail', - args=(self.document.latest_version.pk,) + args=(self.document.pk, self.document.latest_version.pk,) ), data={'comment': TEST_DOCUMENT_VERSION_COMMENT_EDITED} ) @@ -277,7 +277,7 @@ class DocumentAPITestCase(APITestCase): response = self.client.put( reverse( 'rest_api:documentversion-detail', - args=(self.document.latest_version.pk,) + args=(self.document.pk, self.document.latest_version.pk,) ), data={'comment': TEST_DOCUMENT_VERSION_COMMENT_EDITED} ) @@ -313,7 +313,6 @@ class DocumentAPITestCase(APITestCase): args=(self.document.pk,) ), data={'description': TEST_DOCUMENT_DESCRIPTION_EDITED} ) - self.assertEqual(response.status_code, 200) self.document.refresh_from_db() self.assertEqual( diff --git a/mayan/apps/documents/tests/test_views.py b/mayan/apps/documents/tests/test_views.py index 98c47c47d5..a40ea03bf4 100644 --- a/mayan/apps/documents/tests/test_views.py +++ b/mayan/apps/documents/tests/test_views.py @@ -84,7 +84,7 @@ class DocumentsViewsTestCase(GenericDocumentViewTestCase): def test_document_list_view_with_permissions(self): self.grant(permission=permission_document_view) response = self.get('documents:document_list') - self.assertContains(response, 'Total: 1', status_code=200) + self.assertContains(response, self.document.label, status_code=200) def _edit_document_type(self, document_type): return self.post( diff --git a/mayan/apps/documents/urls.py b/mayan/apps/documents/urls.py index cc507b9b2d..c81fb12461 100644 --- a/mayan/apps/documents/urls.py +++ b/mayan/apps/documents/urls.py @@ -9,7 +9,7 @@ from .api_views import ( APIDocumentPageImageView, APIDocumentPageView, APIDocumentTypeDocumentListView, APIDocumentTypeListView, APIDocumentTypeView, APIDocumentVersionsListView, - APIDocumentVersionRevertView, APIDocumentVersionView, + APIDocumentVersionPageListView, APIDocumentVersionView, APIRecentDocumentListView ) from .views import ( @@ -258,18 +258,6 @@ urlpatterns = [ ] api_urls = [ - url( - r'^trashed_documents/$', APIDeletedDocumentListView.as_view(), - name='trasheddocument-list' - ), - url( - r'^trashed_documents/(?P[0-9]+)/$', - APIDeletedDocumentView.as_view(), name='trasheddocument-detail' - ), - url( - r'^trashed_documents/(?P[0-9]+)/restore/$', - APIDeletedDocumentRestoreView.as_view(), name='trasheddocument-restore' - ), url(r'^documents/$', APIDocumentListView.as_view(), name='document-list'), url( r'^documents/recent/$', APIRecentDocumentListView.as_view(), @@ -279,33 +267,33 @@ api_urls = [ r'^documents/(?P[0-9]+)/$', APIDocumentView.as_view(), name='document-detail' ), - url( - r'^documents/(?P[0-9]+)/versions/$', - APIDocumentVersionsListView.as_view(), name='document-version-list' - ), url( r'^documents/(?P[0-9]+)/download/$', APIDocumentDownloadView.as_view(), name='document-download' ), url( - r'^document_version/(?P[0-9]+)/$', + r'^documents/(?P[0-9]+)/versions/$', + APIDocumentVersionsListView.as_view(), name='document-version-list' + ), + url( + r'^documents/(?P[0-9]+)/versions/(?P[0-9]+)/$', APIDocumentVersionView.as_view(), name='documentversion-detail' ), url( - r'^document_version/(?P[0-9]+)/revert/$', - APIDocumentVersionRevertView.as_view(), name='documentversion-revert' + r'^documents/(?P[0-9]+)/versions/(?P[0-9]+)/pages/$', + APIDocumentVersionPageListView.as_view(), name='documentversion-page-list' ), url( - r'^document_version/(?P[0-9]+)/download/$', + r'^documents/(?P[0-9]+)/versions/(?P[0-9]+)/download/$', APIDocumentVersionDownloadView.as_view(), name='documentversion-download' ), url( - r'^document_page/(?P[0-9]+)/$', APIDocumentPageView.as_view(), - name='documentpage-detail' + r'^documents/(?P[0-9]+)/versions/(?P[0-9]+)/pages/(?P[0-9]+)$', + APIDocumentPageView.as_view(), name='documentpage-detail' ), url( - r'^document_page/(?P[0-9]+)/image/$', + r'^documents/(?P[0-9]+)/versions/(?P[0-9]+)/pages/(?P[0-9]+)/image/$', APIDocumentPageImageView.as_view(), name='documentpage-image' ), url( @@ -321,4 +309,16 @@ api_urls = [ r'^document_types/$', APIDocumentTypeListView.as_view(), name='documenttype-list' ), + url( + r'^trashed_documents/$', APIDeletedDocumentListView.as_view(), + name='trasheddocument-list' + ), + url( + r'^trashed_documents/(?P[0-9]+)/$', + APIDeletedDocumentView.as_view(), name='trasheddocument-detail' + ), + url( + r'^trashed_documents/(?P[0-9]+)/restore/$', + APIDeletedDocumentRestoreView.as_view(), name='trasheddocument-restore' + ), ] diff --git a/mayan/apps/documents/widgets.py b/mayan/apps/documents/widgets.py index 31a9dd0eef..ef5900ec41 100644 --- a/mayan/apps/documents/widgets.py +++ b/mayan/apps/documents/widgets.py @@ -102,7 +102,9 @@ class InstanceImageWidget(object): # Click view def get_click_view_kwargs(self, instance): return { - 'pk': instance.pk + 'pk': instance.document.pk, + 'version_pk': instance.document_version.pk, + 'page_pk': instance.pk } def get_click_view_query_dict(self, instance): @@ -141,7 +143,9 @@ class InstanceImageWidget(object): # Preview view def get_preview_view_kwargs(self, instance): return { - 'pk': instance.pk + 'pk': instance.document.pk, + 'version_pk': instance.document_version.pk, + 'page_pk': instance.pk } def get_preview_view_query_dict(self, instance): @@ -255,12 +259,16 @@ class CarouselDocumentPageThumbnailWidget(BaseDocumentThumbnailWidget): class DocumentThumbnailWidget(BaseDocumentThumbnailWidget): def get_click_view_kwargs(self, instance): return { - 'pk': instance.latest_version.pages.first().pk + 'pk': instance.pk, + 'version_pk': instance.latest_version.pk, + 'page_pk': instance.latest_version.pages.first().pk } def get_preview_view_kwargs(self, instance): return { - 'pk': instance.latest_version.pages.first().pk + 'pk': instance.pk, + 'version_pk': instance.latest_version.pk, + 'page_pk': instance.latest_version.pages.first().pk } def get_title(self, instance):