diff --git a/docs/releases/3.0.rst b/docs/releases/3.0.rst index c7602c8009..bd4fc5a4b7 100644 --- a/docs/releases/3.0.rst +++ b/docs/releases/3.0.rst @@ -71,7 +71,8 @@ Backward incompatible changes Bugs fixed or issues closed =========================== -* `GitLab issue #378 `_ Add metadata widget changes from @Macrobb +* `GitLab issue #366 `_ Proofread documentation +* `GitLab issue #379 `_ Add new document version list view permission. * `GitLab issue #379 `_ Add new document version list view permission. diff --git a/docs/topics/deploying.rst b/docs/topics/deploying.rst index f16bbd59ad..8952357901 100644 --- a/docs/topics/deploying.rst +++ b/docs/topics/deploying.rst @@ -3,7 +3,8 @@ Advanced deployment =================== Mayan EDMS should be deployed like any other Django_ project and -preferably using virtualenv_. +preferably using virtualenv_. Below are some ways to deploy and use Mayan EDMS. +Do not use more than one method. Being a Django_ and a Python_ project, familiarity with these technologies is recommended to better understand why Mayan EDMS does some of the things it @@ -58,7 +59,7 @@ to /usr/bin/ with ... sudo ln -s /opt/local/bin/tesseract /usr/bin/tesseract -... alternatively set the paths in the ``settings/locals.py`` +Alternatively, set the paths in the ``settings/locals.py`` .. code-block:: python @@ -76,9 +77,9 @@ With Homebrew installed run the command: Set the Binary paths ******************** -Mayan EDMS by default will look in /usr/bin/ for the binary files it needs -so either you can symlink the binaries installed via brew in /usr/local/bin/ -to /usr/bin/ with ... +Mayan EDMS by default will look in /usr/bin/ for the binary files it needs. +You can symlink the binaries installed via brew in /usr/local/bin/ +to /usr/bin/ with: .. code-block:: bash @@ -87,7 +88,7 @@ to /usr/bin/ with ... sudo ln -s /usr/local/bin/pdftotext /usr/bin/pdftotext && \ sudo ln -s /usr/local/bin/gs /usr/bin/gs -... alternatively set the paths in the ``settings/locals.py`` +Alternatively, set the paths in the ``settings/locals.py`` .. code-block:: python @@ -265,15 +266,18 @@ Make the installation directory readable and writable by the webserver user:: chown www-data:www-data /usr/share/mayan-edms -R -Restart the services:: +Enable and restart the services [1_]:: systemctl enable supervisor systemctl restart supervisor systemctl restart nginx +[1]: https://bugs.launchpad.net/ubuntu/+source/supervisor/+bug/1594740 + .. _Debian: http://www.debian.org/ .. _Django: http://www.djangoproject.com/ .. _Python: http://www.python.org/ .. _SQLite: https://www.sqlite.org/ .. _Ubuntu: http://www.ubuntu.com/ .. _virtualenv: http://www.virtualenv.org/en/latest/index.html +.. _1: https://bugs.launchpad.net/ubuntu/+source/supervisor/+bug/1594740 diff --git a/docs/topics/development.rst b/docs/topics/development.rst index 1682d08c12..91312b2165 100644 --- a/docs/topics/development.rst +++ b/docs/topics/development.rst @@ -16,8 +16,8 @@ request on GitLab_. Project philosophies -------------------- -How to think about Mayan EDMS when doing changes or adding new features, -why things are the way they are in Mayan EDMS. +How to think about Mayan EDMS when doing changes or adding new features; +why things are the way they are in Mayan EDMS: - Functionality must be as market/sector independent as possible, code for the 95% of use cases. @@ -36,7 +36,7 @@ why things are the way they are in Mayan EDMS. not viable/mature/efficient. - Each app is as independent and self contained as possible. Exceptions, the basic requirements: navigation, permissions, common, main. -- If an app is meant to be used by more than one other app it should be as +- If an app is meant to be used by more than one other app, it should be as generic as possible in regard to the project and another app will bridge the functionality. - Example: since indexing (document_indexing) only applies to documents, the @@ -48,7 +48,7 @@ Coding conventions Follow PEP8 ~~~~~~~~~~~ -Whenever possible, but don't obsess over things like line length. +Whenever possible, but don't obsess over things like line length: .. code-block:: bash @@ -103,9 +103,9 @@ Example: ) from .models import Index, IndexInstanceNode, DocumentRenameCount -All local app module imports are in relative form, local app module name is to +All local app module imports are in relative form. Local app module name is to be referenced as little as possible, unless required by a specific feature, -trick, restriction, ie: Runtime modification of the module's attributes. +trick, restriction (e.g., Runtime modification of the module's attributes). Incorrect: @@ -128,7 +128,7 @@ Dependencies Mayan EDMS apps follow a hierarchical model of dependency. Apps import from their parents or siblings, never from their children. Think plugins. A parent app must never assume anything about a possible existing child app. The -documents app and the Document model are the basic entities they must never +documents app and the Document model are the basic entities; they must never import anything else. The common and main apps are the base apps. diff --git a/docs/topics/features.rst b/docs/topics/features.rst index 33d32c7159..3d93dc0145 100644 --- a/docs/topics/features.rst +++ b/docs/topics/features.rst @@ -30,7 +30,7 @@ Features * Dynamic default values for metadata. * Metadata fields can have an initial value, which can be static or determined - by an user provided template code snippet. + by a template code snippet provided by the user. * Documents can be uploaded from different sources. @@ -68,7 +68,7 @@ Features * Multi page document support. - * Multiple page PDFs and TIFFs files are supported. + * Multiple page PDF and TIFF files are supported. * Automatic OCR processing. diff --git a/docs/topics/transformations.rst b/docs/topics/transformations.rst index ffde0c7c7c..ebe4355be8 100644 --- a/docs/topics/transformations.rst +++ b/docs/topics/transformations.rst @@ -2,21 +2,20 @@ Transformations =============== -Transformation are persistent manipulations to the previews of the stored -documents. For example: a scanning equipment may only produce landscape PDFs. -In this case an useful transformation for that document source would be to -rotate all documents scanned by 270 degrees after being uploaded, this way -whenever a document is uploaded from that scanner it will appear in portrait -orientation. In this case add a this transformation to the Mayan EDMS source -that is connected to that device this way all pages scanned via that source -with inherit the transformation as they are created. +Transformations are persistent manipulations to the previews of the stored +documents. For example: a scanning equipment may only produce landscape PDFs. +In this case a useful transformation for that document source would be to rotate +all scanned documents by 270 degrees after being uploaded. By adding this +transformation to the Mayan EDMS source that is connected to the scanner, all +pages scanned via that source will inherit the transformation as they are +created. The result is that whenever a document is uploaded from that scanner, +it will appear in portrait orientation, instead of landscape orientation. -Transformations can also be added to existing documents, by clicking on a -document's page, then clicking on "transformations". In this view the Actions -menu will have a new option that reads "Create new transformation". At the -moment the rotation, zoom, crop, and resize transformations are available. -Once the document image has been corrected resubmit it for OCR for improved -results. +Transformations can also be added to existing documents by clicking on a +document's page and then clicking on "transformations". In this view the Actions +menu will have a new option that reads "Create new transformation". Currently, +the available transformations are: rotation, zoom, crop, and resize. Once the +document image has been corrected, resubmit it for OCR for improved results. -Transformations are not destructive and do not physically modify the document +Transformations are not destructive and do not physically modify the document file, they just modify the document's graphical representation. diff --git a/mayan/apps/acls/managers.py b/mayan/apps/acls/managers.py index bfe0c5dd0c..e1facbb712 100644 --- a/mayan/apps/acls/managers.py +++ b/mayan/apps/acls/managers.py @@ -115,8 +115,10 @@ class AccessControlListManager(models.Manager): def filter_by_access(self, permission, user, queryset): if user.is_superuser or user.is_staff: - logger.debug('Unfiltered queryset returned to user "%s" as superuser or staff', - user) + logger.debug( + 'Unfiltered queryset returned to user "%s" as superuser ' + 'or staff', user + ) return queryset try: diff --git a/mayan/apps/acls/urls.py b/mayan/apps/acls/urls.py index 325dccb67a..ea0adb9d8b 100644 --- a/mayan/apps/acls/urls.py +++ b/mayan/apps/acls/urls.py @@ -28,19 +28,19 @@ urlpatterns = [ api_urls = [ url( - r'^object/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/acls/$', + r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/acls/$', APIObjectACLListView.as_view(), name='accesscontrollist-list' ), url( - r'^object/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/acls/(?P\d+)/$', + r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/acls/(?P\d+)/$', APIObjectACLView.as_view(), name='accesscontrollist-detail' ), url( - r'^object/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/acls/(?P\d+)/permissions/$', + r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/acls/(?P\d+)/permissions/$', APIObjectACLPermissionListView.as_view(), name='accesscontrollist-permission-list' ), url( - r'^object/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/acls/(?P\d+)/permissions/(?P\d+)/$', + r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/acls/(?P\d+)/permissions/(?P\d+)/$', APIObjectACLPermissionView.as_view(), name='accesscontrollist-permission-detail' ), ] diff --git a/mayan/apps/checkouts/api_views.py b/mayan/apps/checkouts/api_views.py index 6264dfc4a4..42b859af08 100644 --- a/mayan/apps/checkouts/api_views.py +++ b/mayan/apps/checkouts/api_views.py @@ -1,20 +1,13 @@ from __future__ import absolute_import, unicode_literals -import pytz - -from django.shortcuts import get_object_or_404 - -from rest_framework import generics, status -from rest_framework.response import Response +from rest_framework import generics from acls.models import AccessControlList -from documents.models import Document from documents.permissions import permission_document_view from .models import DocumentCheckout from .permissions import ( - permission_document_checkout, permission_document_checkin, - permission_document_checkin_override + permission_document_checkin, permission_document_checkin_override ) from .serializers import ( DocumentCheckoutSerializer, NewDocumentCheckoutSerializer @@ -47,12 +40,23 @@ class APICheckedoutDocumentListView(generics.ListCreateAPIView): APICheckedoutDocumentListView, self ).get(request, *args, **kwargs) + def get_serializer_context(self): + """ + Extra context provided to the serializer class. + """ + return { + 'format': self.format_kwarg, + 'request': self.request, + 'view': self + } + + ''' def post(self, request, *args, **kwargs): """ Checkout a document. """ - serializer = self.get_serializer(data=request.DATA, files=request.FILES) + serializer = self.get_serializer(data=request.data, files=request.file) if serializer.is_valid(): document = get_object_or_404( @@ -83,6 +87,7 @@ class APICheckedoutDocumentListView(generics.ListCreateAPIView): return Response(status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + ''' class APICheckedoutDocumentView(generics.RetrieveDestroyAPIView): diff --git a/mayan/apps/checkouts/apps.py b/mayan/apps/checkouts/apps.py index 2f57b48efa..f20e602aa6 100644 --- a/mayan/apps/checkouts/apps.py +++ b/mayan/apps/checkouts/apps.py @@ -57,7 +57,10 @@ class CheckoutsApp(MayanAppConfig): Document.add_to_class( 'check_in', - lambda document, user=None: DocumentCheckout.objects.check_in_document(document, user) + lambda document, + user=None: DocumentCheckout.objects.check_in_document( + document, user + ) ) Document.add_to_class( 'checkout_info', diff --git a/mayan/apps/checkouts/managers.py b/mayan/apps/checkouts/managers.py index 064c4bda37..6d795729ce 100644 --- a/mayan/apps/checkouts/managers.py +++ b/mayan/apps/checkouts/managers.py @@ -19,7 +19,7 @@ logger = logging.getLogger(__name__) class DocumentCheckoutManager(models.Manager): def checkout_document(self, document, expiration_datetime, user, block_new_version=True): - self.create( + return self.create( document=document, expiration_datetime=expiration_datetime, user=user, block_new_version=block_new_version ) diff --git a/mayan/apps/checkouts/serializers.py b/mayan/apps/checkouts/serializers.py index 72f7947cca..d322111069 100644 --- a/mayan/apps/checkouts/serializers.py +++ b/mayan/apps/checkouts/serializers.py @@ -1,15 +1,19 @@ from __future__ import unicode_literals +from django.utils.translation import ugettext_lazy as _ + from rest_framework import serializers +from acls.models import AccessControlList +from documents.models import Document +from documents.serializers import DocumentSerializer + from .models import DocumentCheckout +from .permissions import permission_document_checkout class DocumentCheckoutSerializer(serializers.ModelSerializer): def __init__(self, *args, **kwargs): - # Hide this import otherwise strange circular import error occur - from documents.serializers import DocumentSerializer - super(DocumentCheckoutSerializer, self).__init__(*args, **kwargs) self.fields['document'] = DocumentSerializer() @@ -17,7 +21,33 @@ class DocumentCheckoutSerializer(serializers.ModelSerializer): model = DocumentCheckout -class NewDocumentCheckoutSerializer(serializers.Serializer): - document = serializers.IntegerField() - expiration_datetime = serializers.DateTimeField() +class NewDocumentCheckoutSerializer(serializers.ModelSerializer): block_new_version = serializers.BooleanField() + document_pk = serializers.IntegerField( + help_text=_('Primary key of the document to be checked out.'), + write_only=True + ) + expiration_datetime = serializers.DateTimeField() + + class Meta: + fields = ( + 'block_new_version', 'document', 'document_pk', + 'expiration_datetime', 'id' + ) + model = DocumentCheckout + read_only_fields = ('document',) + write_only_fields = ('document_pk',) + + def create(self, validated_data): + document = Document.objects.get(pk=validated_data.pop('document_pk')) + + AccessControlList.objects.check_access( + permissions=permission_document_checkout, + user=self.context['request'].user, obj=document + ) + + validated_data['document'] = document + validated_data['user'] = self.context['request'].user + return super(NewDocumentCheckoutSerializer, self).create( + validated_data + ) diff --git a/mayan/apps/checkouts/tests/test_api.py b/mayan/apps/checkouts/tests/test_api.py new file mode 100644 index 0000000000..eccdb4d265 --- /dev/null +++ b/mayan/apps/checkouts/tests/test_api.py @@ -0,0 +1,75 @@ +from __future__ import unicode_literals + +import datetime + +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.timezone import now + +from rest_framework.test import APITestCase + +from documents.models import DocumentType +from documents.tests import TEST_DOCUMENT_TYPE, TEST_SMALL_DOCUMENT_PATH +from user_management.tests.literals import ( + TEST_ADMIN_EMAIL, TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME +) + +from ..models import DocumentCheckout + + +@override_settings(OCR_AUTO_OCR=False) +class CheckoutAPITestCase(APITestCase): + def setUp(self): + super(CheckoutAPITestCase, 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 + ) + + self.document_type = DocumentType.objects.create( + label=TEST_DOCUMENT_TYPE + ) + + with open(TEST_SMALL_DOCUMENT_PATH) as file_object: + self.document = self.document_type.new_document( + file_object=file_object, + ) + + def tearDown(self): + self.document_type.delete() + super(CheckoutAPITestCase, self).tearDown() + + def test_document_checkout_get_view(self): + expiration_datetime = now() + datetime.timedelta(days=1) + + DocumentCheckout.objects.checkout_document( + document=self.document, expiration_datetime=expiration_datetime, + user=self.admin_user, block_new_version=True + ) + + response = self.client.get(reverse('rest_api:checkout-document-list')) + + self.assertEqual( + response.data['results'][0]['document']['uuid'], + force_text(self.document.uuid) + ) + + def test_document_checkout_post_view(self): + response = self.client.post( + reverse('rest_api:checkout-document-list'), data={ + 'document_pk': self.document.pk, + 'expiration_datetime': '2099-01-01T12:00' + } + ) + + self.assertEqual(response.status_code, 201) + self.assertEqual( + DocumentCheckout.objects.first().document, self.document + ) diff --git a/mayan/apps/checkouts/urls.py b/mayan/apps/checkouts/urls.py index bbc29b12c6..dabf479251 100644 --- a/mayan/apps/checkouts/urls.py +++ b/mayan/apps/checkouts/urls.py @@ -26,11 +26,11 @@ urlpatterns = [ api_urls = [ url( - r'^documents/$', APICheckedoutDocumentListView.as_view(), + r'^checkouts/$', APICheckedoutDocumentListView.as_view(), name='checkout-document-list' ), url( - r'^documents/(?P[0-9]+)/$', APICheckedoutDocumentView.as_view(), + r'^checkouts/(?P[0-9]+)/$', APICheckedoutDocumentView.as_view(), name='checkedout-document-view' ), ] diff --git a/mayan/apps/document_comments/urls.py b/mayan/apps/document_comments/urls.py index b46f295399..2dd6929abf 100644 --- a/mayan/apps/document_comments/urls.py +++ b/mayan/apps/document_comments/urls.py @@ -25,11 +25,11 @@ urlpatterns = [ api_urls = [ url( - r'^document/(?P[0-9]+)/comments/$', + r'^documents/(?P[0-9]+)/comments/$', APICommentListView.as_view(), name='comment-list' ), url( - r'^document/(?P[0-9]+)/comments/(?P[0-9]+)/$', + r'^documents/(?P[0-9]+)/comments/(?P[0-9]+)/$', APICommentView.as_view(), name='comment-detail' ), ] diff --git a/mayan/apps/document_indexing/urls.py b/mayan/apps/document_indexing/urls.py index bdf0a432cc..e2b81beebd 100644 --- a/mayan/apps/document_indexing/urls.py +++ b/mayan/apps/document_indexing/urls.py @@ -72,12 +72,12 @@ urlpatterns = [ api_urls = [ url( - r'^index/node/(?P[0-9]+)/documents/$', + r'^indexes/node/(?P[0-9]+)/documents/$', APIIndexNodeInstanceDocumentListView.as_view(), name='index-node-documents' ), url( - r'^index/template/(?P[0-9]+)/$', APIIndexTemplateView.as_view(), + r'^indexes/template/(?P[0-9]+)/$', APIIndexTemplateView.as_view(), name='index-template-detail' ), url( @@ -85,12 +85,12 @@ api_urls = [ name='index-detail' ), url( - r'^index/(?P[0-9]+)/template/$', + r'^indexes/(?P[0-9]+)/template/$', APIIndexTemplateListView.as_view(), name='index-template-detail' ), url(r'^indexes/$', APIIndexListView.as_view(), name='index-list'), url( - r'^document/(?P[0-9]+)/indexes/$', + r'^documents/(?P[0-9]+)/indexes/$', APIDocumentIndexListView.as_view(), name='document-index-list' ), ] diff --git a/mayan/apps/document_states/urls.py b/mayan/apps/document_states/urls.py index e6217a66cf..1492f37156 100644 --- a/mayan/apps/document_states/urls.py +++ b/mayan/apps/document_states/urls.py @@ -164,20 +164,20 @@ api_urls = [ APIWorkflowTransitionView.as_view(), name='workflowtransition-detail' ), url( - r'^document/(?P[0-9]+)/workflows/$', + r'^documents/(?P[0-9]+)/workflows/$', APIWorkflowInstanceListView.as_view(), name='workflowinstance-list' ), url( - r'^document/(?P[0-9]+)/workflows/(?P[0-9]+)/$', + r'^documents/(?P[0-9]+)/workflows/(?P[0-9]+)/$', APIWorkflowInstanceView.as_view(), name='workflowinstance-detail' ), url( - r'^document/(?P[0-9]+)/workflows/(?P[0-9]+)/log_entries/$', + r'^documents/(?P[0-9]+)/workflows/(?P[0-9]+)/log_entries/$', APIWorkflowInstanceLogEntryListView.as_view(), name='workflowinstancelogentry-list' ), url( - r'^document_type/(?P[0-9]+)/workflows/$', + r'^document_types/(?P[0-9]+)/workflows/$', APIDocumentTypeWorkflowListView.as_view(), name='documenttype-workflow-list' ), diff --git a/mayan/apps/documents/apps.py b/mayan/apps/documents/apps.py index 8e0468f6a8..65d9200e5c 100644 --- a/mayan/apps/documents/apps.py +++ b/mayan/apps/documents/apps.py @@ -27,7 +27,7 @@ from events.links import link_events_for_object 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.classes import APIEndPoint, APIResource from rest_api.fields import DynamicSerializerField from statistics.classes import StatisticNamespace, CharJSLine @@ -91,6 +91,9 @@ class DocumentsApp(MayanAppConfig): from actstream import registry APIEndPoint(app=self, version_string='1') + APIResource(label=_('Document types'), name='document_types') + APIResource(label=_('Documents'), name='documents') + APIResource(label=_('Trashed documents'), name='trashed_documents') DeletedDocument = self.get_model('DeletedDocument') Document = self.get_model('Document') diff --git a/mayan/apps/dynamic_search/urls.py b/mayan/apps/dynamic_search/urls.py index 1300f2fa61..ddcb1aab9a 100644 --- a/mayan/apps/dynamic_search/urls.py +++ b/mayan/apps/dynamic_search/urls.py @@ -29,7 +29,7 @@ api_urls = [ name='search-view' ), url( - r'^advanced/(?P[\.\w]+)/$', APIAdvancedSearchView.as_view(), + r'^search/advanced/(?P[\.\w]+)/$', APIAdvancedSearchView.as_view(), name='advanced-search-view' ), ] diff --git a/mayan/apps/events/urls.py b/mayan/apps/events/urls.py index 873a38a576..f5fc5b8fbb 100644 --- a/mayan/apps/events/urls.py +++ b/mayan/apps/events/urls.py @@ -20,10 +20,10 @@ urlpatterns = [ ] api_urls = [ - url(r'^types/$', APIEventTypeListView.as_view(), name='event-type-list'), + url(r'^event_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/$', + r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/events/$', APIObjectEventListView.as_view(), name='object-event-list' ), ] diff --git a/mayan/apps/folders/urls.py b/mayan/apps/folders/urls.py index e8b3496bb9..47ae13b2ec 100644 --- a/mayan/apps/folders/urls.py +++ b/mayan/apps/folders/urls.py @@ -60,7 +60,7 @@ api_urls = [ ), url(r'^folders/$', APIFolderListView.as_view(), name='folder-list'), url( - r'^document/(?P[0-9]+)/folders/$', + r'^documents/(?P[0-9]+)/folders/$', APIDocumentFolderListView.as_view(), name='document-folder-list' ), ] diff --git a/mayan/apps/ocr/api_views.py b/mayan/apps/ocr/api_views.py index ded56e8ed8..c7449d7a50 100644 --- a/mayan/apps/ocr/api_views.py +++ b/mayan/apps/ocr/api_views.py @@ -3,6 +3,8 @@ from __future__ import absolute_import, unicode_literals from rest_framework import generics, status from rest_framework.response import Response +from django.shortcuts import get_object_or_404 + from documents.models import Document, DocumentPage, DocumentVersion from rest_api.permissions import MayanPermission @@ -26,10 +28,6 @@ class APIDocumentOCRView(generics.GenericAPIView): Submit a document for OCR. --- omit_serializer: true - parameters: - - name: pk - paramType: path - type: number responseMessages: - code: 202 message: Accepted @@ -40,12 +38,19 @@ class APIDocumentOCRView(generics.GenericAPIView): class APIDocumentVersionOCRView(generics.GenericAPIView): + lookup_url_kwarg = 'version_pk' mayan_object_permissions = { 'POST': (permission_ocr_document,) } permission_classes = (MayanPermission,) queryset = DocumentVersion.objects.all() + def get_document(self): + return get_object_or_404(Document, pk=self.kwargs['document_pk']) + + def get_queryset(self): + return self.get_document().versions.all() + def get_serializer_class(self): return None @@ -54,10 +59,6 @@ class APIDocumentVersionOCRView(generics.GenericAPIView): Submit a document version for OCR. --- omit_serializer: true - parameters: - - name: pk - paramType: path - type: number responseMessages: - code: 202 message: Accepted @@ -70,20 +71,25 @@ class APIDocumentVersionOCRView(generics.GenericAPIView): class APIDocumentPageContentView(generics.RetrieveAPIView): """ Returns the OCR content of the selected document page. - --- - GET: - parameters: - - name: pk - paramType: path - type: number """ + lookup_url_kwarg = 'page_pk' mayan_object_permissions = { 'GET': (permission_ocr_content_view,), } permission_classes = (MayanPermission,) serializer_class = DocumentPageContentSerializer - queryset = DocumentPage.objects.all() + + def get_document(self): + return get_object_or_404(Document, pk=self.kwargs['document_pk']) + + 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 retrieve(self, request, *args, **kwargs): instance = self.get_object() diff --git a/mayan/apps/ocr/tests/test_api.py b/mayan/apps/ocr/tests/test_api.py index 181ff14ca1..bb6619d0c9 100644 --- a/mayan/apps/ocr/tests/test_api.py +++ b/mayan/apps/ocr/tests/test_api.py @@ -63,7 +63,9 @@ class OCRAPITestCase(BaseAPITestCase): response = self.client.post( reverse( 'rest_api:document-version-ocr-submit-view', - args=(self.document.latest_version.pk,) + args=( + self.document.pk, self.document.latest_version.pk, + ) ) ) @@ -77,7 +79,10 @@ class OCRAPITestCase(BaseAPITestCase): response = self.client.get( reverse( 'rest_api:document-page-content-view', - args=(self.document.latest_version.pages.first().pk,) + args=( + self.document.pk, self.document.latest_version.pk, + self.document.latest_version.pages.first().pk, + ) ), ) diff --git a/mayan/apps/ocr/urls.py b/mayan/apps/ocr/urls.py index a96060f904..8e31b8cc1f 100644 --- a/mayan/apps/ocr/urls.py +++ b/mayan/apps/ocr/urls.py @@ -43,16 +43,16 @@ urlpatterns = [ api_urls = [ url( - r'^document/(?P\d+)/submit/$', APIDocumentOCRView.as_view(), + r'^documents/(?P\d+)/ocr/$', APIDocumentOCRView.as_view(), name='document-ocr-submit-view' ), url( - r'^document_version/(?P\d+)/submit/$', + r'^documents/(?P\d+)/versions/(?P\d+)/ocr/$', APIDocumentVersionOCRView.as_view(), name='document-version-ocr-submit-view' ), url( - r'^page/(?P\d+)/content/$', APIDocumentPageContentView.as_view(), + r'^documents/(?P\d+)/versions/(?P\d+)/pages/(?P\d+)/ocr/$', APIDocumentPageContentView.as_view(), name='document-page-content-view' ), ] diff --git a/mayan/apps/permissions/urls.py b/mayan/apps/permissions/urls.py index 6044f99aec..2eb8d57546 100644 --- a/mayan/apps/permissions/urls.py +++ b/mayan/apps/permissions/urls.py @@ -30,5 +30,4 @@ api_urls = [ 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'^$', APIPermissionList.as_view(), name='permission-list'), ] diff --git a/mayan/apps/rest_api/api_views.py b/mayan/apps/rest_api/api_views.py new file mode 100644 index 0000000000..93ab0e3f4d --- /dev/null +++ b/mayan/apps/rest_api/api_views.py @@ -0,0 +1,19 @@ +from __future__ import unicode_literals + +from rest_framework import generics + +from rest_api.filters import MayanObjectPermissionsFilter +from rest_api.permissions import MayanPermission + +from .classes import APIResource +from .serializers import APIResourceSerializer + + +class APIResourceTypeListView(generics.ListAPIView): + """ + Returns a list of all the available API resources. + """ + + serializer_class = APIResourceSerializer + def get_queryset(self): + return APIResource.all() diff --git a/mayan/apps/rest_api/classes.py b/mayan/apps/rest_api/classes.py index 6a47538bbe..3394b446c1 100644 --- a/mayan/apps/rest_api/classes.py +++ b/mayan/apps/rest_api/classes.py @@ -4,9 +4,33 @@ from django.conf.urls import include, url from django.conf import settings from django.utils.module_loading import import_string +from .exceptions import APIResourcePatternError + + +class APIResource(object): + _registry = {} + + @classmethod + def all(cls): + return cls._registry.values() + + @classmethod + def get(cls, name): + return cls._registry[name] + + def __unicode__(self): + return unicode(self.name) + + def __init__(self, name, label, description=None): + self.label = label + self.name = name + self.description = description + self.__class__._registry[self.name] = self + class APIEndPoint(object): _registry = {} + _patterns = [] @classmethod def get_all(cls): @@ -46,6 +70,12 @@ class APIEndPoint(object): def register_urls(self, urlpatterns): from .urls import urlpatterns as app_urls - app_urls += [ - url(r'^%s/' % (self.name or self.app.name), include(urlpatterns)), - ] + for url in urlpatterns: + if url.regex.pattern not in self.__class__._patterns: + app_urls.append(url) + self.__class__._patterns.append(url.regex.pattern) + else: + raise APIResourcePatternError( + 'App "{}" tried to register API URL pattern "{}", which ' + 'already exists'.format(self.app.label, url.regex.pattern) + ) diff --git a/mayan/apps/rest_api/exceptions.py b/mayan/apps/rest_api/exceptions.py new file mode 100644 index 0000000000..4e0e6ac964 --- /dev/null +++ b/mayan/apps/rest_api/exceptions.py @@ -0,0 +1,16 @@ +from __future__ import unicode_literals + + +class APIError(Exception): + """ + Base exception for the API app + """ + pass + + +class APIResourcePatternError(APIError): + """ + Raised when an app tries to override an existing URL regular expression + pattern + """ + pass diff --git a/mayan/apps/rest_api/serializers.py b/mayan/apps/rest_api/serializers.py new file mode 100644 index 0000000000..ecfa25faec --- /dev/null +++ b/mayan/apps/rest_api/serializers.py @@ -0,0 +1,9 @@ +from __future__ import unicode_literals + +from rest_framework import serializers + + +class APIResourceSerializer(serializers.Serializer): + description = serializers.CharField() + label = serializers.CharField() + name = serializers.CharField() diff --git a/mayan/apps/rest_api/urls.py b/mayan/apps/rest_api/urls.py index cfe3955b66..9f2a4a95d3 100644 --- a/mayan/apps/rest_api/urls.py +++ b/mayan/apps/rest_api/urls.py @@ -2,15 +2,18 @@ from __future__ import unicode_literals from django.conf.urls import url -from .views import APIBase, APIAppView, BrowseableObtainAuthToken +from .api_views import APIResourceTypeListView +from .views import APIBase, BrowseableObtainAuthToken -urlpatterns = [ -] +urlpatterns = [] api_urls = [ url(r'^$', APIBase.as_view(), name='api_root'), - url(r'^api/(?P.*)/?$', APIAppView.as_view(), name='api_app'), + url( + r'^resources/$', APIResourceTypeListView.as_view(), + name='resource-list' + ), url( r'^auth/token/obtain/$', BrowseableObtainAuthToken.as_view(), name='auth_token_obtain' diff --git a/mayan/apps/rest_api/views.py b/mayan/apps/rest_api/views.py index f5e8118697..8a2064fd36 100644 --- a/mayan/apps/rest_api/views.py +++ b/mayan/apps/rest_api/views.py @@ -13,13 +13,6 @@ class APIBase(SwaggerResourcesView): renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer) -class APIAppView(SwaggerApiView): - """ - Entry points of the selected app. - """ - renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer) - - class BrowseableObtainAuthToken(ObtainAuthToken): """ Obtain an API authentication token.