Update the document app API endpoints.

Use resource/<pk>/subresource/<pk> scheme.
This commit is contained in:
Roberto Rosario
2017-02-14 02:42:40 -04:00
parent f67443f0d5
commit 81e090f375
7 changed files with 308 additions and 134 deletions

View File

@@ -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<pk>
/api/documents/document_pages<pk>
After:
/api/documents/<pk>/version/<version_pk>
/api/documents/<pk>/version/<version_pk>/pages/<page_pk>
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/<pk>/pages/<page_pk>/pages'.
Other changes
-------------

View File

@@ -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)

View File

@@ -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)

View File

@@ -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(

View File

@@ -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(

View File

@@ -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<pk>[0-9]+)/$',
APIDeletedDocumentView.as_view(), name='trasheddocument-detail'
),
url(
r'^trashed_documents/(?P<pk>[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<pk>[0-9]+)/$', APIDocumentView.as_view(),
name='document-detail'
),
url(
r'^documents/(?P<pk>[0-9]+)/versions/$',
APIDocumentVersionsListView.as_view(), name='document-version-list'
),
url(
r'^documents/(?P<pk>[0-9]+)/download/$',
APIDocumentDownloadView.as_view(), name='document-download'
),
url(
r'^document_version/(?P<pk>[0-9]+)/$',
r'^documents/(?P<pk>[0-9]+)/versions/$',
APIDocumentVersionsListView.as_view(), name='document-version-list'
),
url(
r'^documents/(?P<pk>[0-9]+)/versions/(?P<version_pk>[0-9]+)/$',
APIDocumentVersionView.as_view(), name='documentversion-detail'
),
url(
r'^document_version/(?P<pk>[0-9]+)/revert/$',
APIDocumentVersionRevertView.as_view(), name='documentversion-revert'
r'^documents/(?P<pk>[0-9]+)/versions/(?P<version_pk>[0-9]+)/pages/$',
APIDocumentVersionPageListView.as_view(), name='documentversion-page-list'
),
url(
r'^document_version/(?P<pk>[0-9]+)/download/$',
r'^documents/(?P<pk>[0-9]+)/versions/(?P<version_pk>[0-9]+)/download/$',
APIDocumentVersionDownloadView.as_view(),
name='documentversion-download'
),
url(
r'^document_page/(?P<pk>[0-9]+)/$', APIDocumentPageView.as_view(),
name='documentpage-detail'
r'^documents/(?P<pk>[0-9]+)/versions/(?P<version_pk>[0-9]+)/pages/(?P<page_pk>[0-9]+)$',
APIDocumentPageView.as_view(), name='documentpage-detail'
),
url(
r'^document_page/(?P<pk>[0-9]+)/image/$',
r'^documents/(?P<pk>[0-9]+)/versions/(?P<version_pk>[0-9]+)/pages/(?P<page_pk>[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<pk>[0-9]+)/$',
APIDeletedDocumentView.as_view(), name='trasheddocument-detail'
),
url(
r'^trashed_documents/(?P<pk>[0-9]+)/restore/$',
APIDeletedDocumentRestoreView.as_view(), name='trasheddocument-restore'
),
]

View File

@@ -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):