Convert the API URL system from an App based one

to a resource based one.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
This commit is contained in:
Roberto Rosario
2017-05-12 17:53:44 -04:00
parent 105eab0740
commit acdc7dca48
30 changed files with 310 additions and 108 deletions

View File

@@ -71,7 +71,8 @@ Backward incompatible changes
Bugs fixed or issues closed Bugs fixed or issues closed
=========================== ===========================
* `GitLab issue #378 <https://gitlab.com/mayan-edms/mayan-edms/issues/378>`_ Add metadata widget changes from @Macrobb * `GitLab issue #366 <https://gitlab.com/mayan-edms/mayan-edms/issues/366>`_ Proofread documentation
* `GitLab issue #379 <https://gitlab.com/mayan-edms/mayan-edms/issues/379>`_ Add new document version list view permission.
* `GitLab issue #379 <https://gitlab.com/mayan-edms/mayan-edms/issues/379>`_ Add new document version list view permission. * `GitLab issue #379 <https://gitlab.com/mayan-edms/mayan-edms/issues/379>`_ Add new document version list view permission.

View File

@@ -3,7 +3,8 @@ Advanced deployment
=================== ===================
Mayan EDMS should be deployed like any other Django_ project and 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 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 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 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 .. code-block:: python
@@ -76,9 +77,9 @@ With Homebrew installed run the command:
Set the Binary paths Set the Binary paths
******************** ********************
Mayan EDMS by default will look in /usr/bin/ for the binary files it needs 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/ You can symlink the binaries installed via brew in /usr/local/bin/
to /usr/bin/ with ... to /usr/bin/ with:
.. code-block:: bash .. 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/pdftotext /usr/bin/pdftotext && \
sudo ln -s /usr/local/bin/gs /usr/bin/gs 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 .. 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 chown www-data:www-data /usr/share/mayan-edms -R
Restart the services:: Enable and restart the services [1_]::
systemctl enable supervisor systemctl enable supervisor
systemctl restart supervisor systemctl restart supervisor
systemctl restart nginx systemctl restart nginx
[1]: https://bugs.launchpad.net/ubuntu/+source/supervisor/+bug/1594740
.. _Debian: http://www.debian.org/ .. _Debian: http://www.debian.org/
.. _Django: http://www.djangoproject.com/ .. _Django: http://www.djangoproject.com/
.. _Python: http://www.python.org/ .. _Python: http://www.python.org/
.. _SQLite: https://www.sqlite.org/ .. _SQLite: https://www.sqlite.org/
.. _Ubuntu: http://www.ubuntu.com/ .. _Ubuntu: http://www.ubuntu.com/
.. _virtualenv: http://www.virtualenv.org/en/latest/index.html .. _virtualenv: http://www.virtualenv.org/en/latest/index.html
.. _1: https://bugs.launchpad.net/ubuntu/+source/supervisor/+bug/1594740

View File

@@ -16,8 +16,8 @@ request on GitLab_.
Project philosophies Project philosophies
-------------------- --------------------
How to think about Mayan EDMS when doing changes or adding new features, How to think about Mayan EDMS when doing changes or adding new features;
why things are the way they are in Mayan EDMS. why things are the way they are in Mayan EDMS:
- Functionality must be as market/sector independent as possible, code for the - Functionality must be as market/sector independent as possible, code for the
95% of use cases. 95% of use cases.
@@ -36,7 +36,7 @@ why things are the way they are in Mayan EDMS.
not viable/mature/efficient. not viable/mature/efficient.
- Each app is as independent and self contained as possible. Exceptions, the - Each app is as independent and self contained as possible. Exceptions, the
basic requirements: navigation, permissions, common, main. 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. 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 - Example: since indexing (document_indexing) only applies to documents, the
@@ -48,7 +48,7 @@ Coding conventions
Follow PEP8 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 .. code-block:: bash
@@ -103,9 +103,9 @@ Example:
) )
from .models import Index, IndexInstanceNode, DocumentRenameCount 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, 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: Incorrect:
@@ -128,7 +128,7 @@ Dependencies
Mayan EDMS apps follow a hierarchical model of dependency. Apps import from Mayan EDMS apps follow a hierarchical model of dependency. Apps import from
their parents or siblings, never from their children. Think plugins. A parent their parents or siblings, never from their children. Think plugins. A parent
app must never assume anything about a possible existing child app. The 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. import anything else. The common and main apps are the base apps.

View File

@@ -30,7 +30,7 @@ Features
* Dynamic default values for metadata. * Dynamic default values for metadata.
* Metadata fields can have an initial value, which can be static or determined * 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. * Documents can be uploaded from different sources.
@@ -68,7 +68,7 @@ Features
* Multi page document support. * Multi page document support.
* Multiple page PDFs and TIFFs files are supported. * Multiple page PDF and TIFF files are supported.
* Automatic OCR processing. * Automatic OCR processing.

View File

@@ -2,21 +2,20 @@
Transformations Transformations
=============== ===============
Transformation are persistent manipulations to the previews of the stored Transformations are persistent manipulations to the previews of the stored
documents. For example: a scanning equipment may only produce landscape PDFs. documents. For example: a scanning equipment may only produce landscape PDFs.
In this case an useful transformation for that document source would be to In this case a useful transformation for that document source would be to rotate
rotate all documents scanned by 270 degrees after being uploaded, this way all scanned documents by 270 degrees after being uploaded. By adding this
whenever a document is uploaded from that scanner it will appear in portrait transformation to the Mayan EDMS source that is connected to the scanner, all
orientation. In this case add a this transformation to the Mayan EDMS source pages scanned via that source will inherit the transformation as they are
that is connected to that device this way all pages scanned via that source created. The result is that whenever a document is uploaded from that scanner,
with inherit the transformation as they are created. it will appear in portrait orientation, instead of landscape orientation.
Transformations can also be added to existing documents, by clicking on a Transformations can also be added to existing documents by clicking on a
document's page, then clicking on "transformations". In this view the Actions document's page and then clicking on "transformations". In this view the Actions
menu will have a new option that reads "Create new transformation". At the menu will have a new option that reads "Create new transformation". Currently,
moment the rotation, zoom, crop, and resize transformations are available. the available transformations are: rotation, zoom, crop, and resize. Once the
Once the document image has been corrected resubmit it for OCR for improved document image has been corrected, resubmit it for OCR for improved results.
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. file, they just modify the document's graphical representation.

View File

@@ -115,8 +115,10 @@ class AccessControlListManager(models.Manager):
def filter_by_access(self, permission, user, queryset): def filter_by_access(self, permission, user, queryset):
if user.is_superuser or user.is_staff: if user.is_superuser or user.is_staff:
logger.debug('Unfiltered queryset returned to user "%s" as superuser or staff', logger.debug(
user) 'Unfiltered queryset returned to user "%s" as superuser '
'or staff', user
)
return queryset return queryset
try: try:

View File

@@ -28,19 +28,19 @@ urlpatterns = [
api_urls = [ api_urls = [
url( url(
r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/$', r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/$',
APIObjectACLListView.as_view(), name='accesscontrollist-list' APIObjectACLListView.as_view(), name='accesscontrollist-list'
), ),
url( url(
r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/(?P<pk>\d+)/$', r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/(?P<pk>\d+)/$',
APIObjectACLView.as_view(), name='accesscontrollist-detail' APIObjectACLView.as_view(), name='accesscontrollist-detail'
), ),
url( url(
r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/(?P<pk>\d+)/permissions/$', r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/(?P<pk>\d+)/permissions/$',
APIObjectACLPermissionListView.as_view(), name='accesscontrollist-permission-list' APIObjectACLPermissionListView.as_view(), name='accesscontrollist-permission-list'
), ),
url( url(
r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/(?P<pk>\d+)/permissions/(?P<permission_pk>\d+)/$', r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/(?P<pk>\d+)/permissions/(?P<permission_pk>\d+)/$',
APIObjectACLPermissionView.as_view(), name='accesscontrollist-permission-detail' APIObjectACLPermissionView.as_view(), name='accesscontrollist-permission-detail'
), ),
] ]

View File

@@ -1,20 +1,13 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
import pytz from rest_framework import generics
from django.shortcuts import get_object_or_404
from rest_framework import generics, status
from rest_framework.response import Response
from acls.models import AccessControlList from acls.models import AccessControlList
from documents.models import Document
from documents.permissions import permission_document_view from documents.permissions import permission_document_view
from .models import DocumentCheckout from .models import DocumentCheckout
from .permissions import ( from .permissions import (
permission_document_checkout, permission_document_checkin, permission_document_checkin, permission_document_checkin_override
permission_document_checkin_override
) )
from .serializers import ( from .serializers import (
DocumentCheckoutSerializer, NewDocumentCheckoutSerializer DocumentCheckoutSerializer, NewDocumentCheckoutSerializer
@@ -47,12 +40,23 @@ class APICheckedoutDocumentListView(generics.ListCreateAPIView):
APICheckedoutDocumentListView, self APICheckedoutDocumentListView, self
).get(request, *args, **kwargs) ).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): def post(self, request, *args, **kwargs):
""" """
Checkout a document. 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(): if serializer.is_valid():
document = get_object_or_404( document = get_object_or_404(
@@ -83,6 +87,7 @@ class APICheckedoutDocumentListView(generics.ListCreateAPIView):
return Response(status=status.HTTP_201_CREATED) return Response(status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
'''
class APICheckedoutDocumentView(generics.RetrieveDestroyAPIView): class APICheckedoutDocumentView(generics.RetrieveDestroyAPIView):

View File

@@ -57,7 +57,10 @@ class CheckoutsApp(MayanAppConfig):
Document.add_to_class( Document.add_to_class(
'check_in', '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( Document.add_to_class(
'checkout_info', 'checkout_info',

View File

@@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
class DocumentCheckoutManager(models.Manager): class DocumentCheckoutManager(models.Manager):
def checkout_document(self, document, expiration_datetime, user, block_new_version=True): def checkout_document(self, document, expiration_datetime, user, block_new_version=True):
self.create( return self.create(
document=document, expiration_datetime=expiration_datetime, document=document, expiration_datetime=expiration_datetime,
user=user, block_new_version=block_new_version user=user, block_new_version=block_new_version
) )

View File

@@ -1,15 +1,19 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers 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 .models import DocumentCheckout
from .permissions import permission_document_checkout
class DocumentCheckoutSerializer(serializers.ModelSerializer): class DocumentCheckoutSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs): 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) super(DocumentCheckoutSerializer, self).__init__(*args, **kwargs)
self.fields['document'] = DocumentSerializer() self.fields['document'] = DocumentSerializer()
@@ -17,7 +21,33 @@ class DocumentCheckoutSerializer(serializers.ModelSerializer):
model = DocumentCheckout model = DocumentCheckout
class NewDocumentCheckoutSerializer(serializers.Serializer): class NewDocumentCheckoutSerializer(serializers.ModelSerializer):
document = serializers.IntegerField()
expiration_datetime = serializers.DateTimeField()
block_new_version = serializers.BooleanField() 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
)

View File

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

View File

@@ -26,11 +26,11 @@ urlpatterns = [
api_urls = [ api_urls = [
url( url(
r'^documents/$', APICheckedoutDocumentListView.as_view(), r'^checkouts/$', APICheckedoutDocumentListView.as_view(),
name='checkout-document-list' name='checkout-document-list'
), ),
url( url(
r'^documents/(?P<pk>[0-9]+)/$', APICheckedoutDocumentView.as_view(), r'^checkouts/(?P<pk>[0-9]+)/$', APICheckedoutDocumentView.as_view(),
name='checkedout-document-view' name='checkedout-document-view'
), ),
] ]

View File

@@ -25,11 +25,11 @@ urlpatterns = [
api_urls = [ api_urls = [
url( url(
r'^document/(?P<document_pk>[0-9]+)/comments/$', r'^documents/(?P<document_pk>[0-9]+)/comments/$',
APICommentListView.as_view(), name='comment-list' APICommentListView.as_view(), name='comment-list'
), ),
url( url(
r'^document/(?P<document_pk>[0-9]+)/comments/(?P<comment_pk>[0-9]+)/$', r'^documents/(?P<document_pk>[0-9]+)/comments/(?P<comment_pk>[0-9]+)/$',
APICommentView.as_view(), name='comment-detail' APICommentView.as_view(), name='comment-detail'
), ),
] ]

View File

@@ -72,12 +72,12 @@ urlpatterns = [
api_urls = [ api_urls = [
url( url(
r'^index/node/(?P<pk>[0-9]+)/documents/$', r'^indexes/node/(?P<pk>[0-9]+)/documents/$',
APIIndexNodeInstanceDocumentListView.as_view(), APIIndexNodeInstanceDocumentListView.as_view(),
name='index-node-documents' name='index-node-documents'
), ),
url( url(
r'^index/template/(?P<pk>[0-9]+)/$', APIIndexTemplateView.as_view(), r'^indexes/template/(?P<pk>[0-9]+)/$', APIIndexTemplateView.as_view(),
name='index-template-detail' name='index-template-detail'
), ),
url( url(
@@ -85,12 +85,12 @@ api_urls = [
name='index-detail' name='index-detail'
), ),
url( url(
r'^index/(?P<pk>[0-9]+)/template/$', r'^indexes/(?P<pk>[0-9]+)/template/$',
APIIndexTemplateListView.as_view(), name='index-template-detail' APIIndexTemplateListView.as_view(), name='index-template-detail'
), ),
url(r'^indexes/$', APIIndexListView.as_view(), name='index-list'), url(r'^indexes/$', APIIndexListView.as_view(), name='index-list'),
url( url(
r'^document/(?P<pk>[0-9]+)/indexes/$', r'^documents/(?P<pk>[0-9]+)/indexes/$',
APIDocumentIndexListView.as_view(), name='document-index-list' APIDocumentIndexListView.as_view(), name='document-index-list'
), ),
] ]

View File

@@ -164,20 +164,20 @@ api_urls = [
APIWorkflowTransitionView.as_view(), name='workflowtransition-detail' APIWorkflowTransitionView.as_view(), name='workflowtransition-detail'
), ),
url( url(
r'^document/(?P<pk>[0-9]+)/workflows/$', r'^documents/(?P<pk>[0-9]+)/workflows/$',
APIWorkflowInstanceListView.as_view(), name='workflowinstance-list' APIWorkflowInstanceListView.as_view(), name='workflowinstance-list'
), ),
url( url(
r'^document/(?P<pk>[0-9]+)/workflows/(?P<workflow_pk>[0-9]+)/$', r'^documents/(?P<pk>[0-9]+)/workflows/(?P<workflow_pk>[0-9]+)/$',
APIWorkflowInstanceView.as_view(), name='workflowinstance-detail' APIWorkflowInstanceView.as_view(), name='workflowinstance-detail'
), ),
url( url(
r'^document/(?P<pk>[0-9]+)/workflows/(?P<workflow_pk>[0-9]+)/log_entries/$', r'^documents/(?P<pk>[0-9]+)/workflows/(?P<workflow_pk>[0-9]+)/log_entries/$',
APIWorkflowInstanceLogEntryListView.as_view(), APIWorkflowInstanceLogEntryListView.as_view(),
name='workflowinstancelogentry-list' name='workflowinstancelogentry-list'
), ),
url( url(
r'^document_type/(?P<pk>[0-9]+)/workflows/$', r'^document_types/(?P<pk>[0-9]+)/workflows/$',
APIDocumentTypeWorkflowListView.as_view(), APIDocumentTypeWorkflowListView.as_view(),
name='documenttype-workflow-list' name='documenttype-workflow-list'
), ),

View File

@@ -27,7 +27,7 @@ from events.links import link_events_for_object
from events.permissions import permission_events_view from events.permissions import permission_events_view
from mayan.celery import app from mayan.celery import app
from navigation import SourceColumn from navigation import SourceColumn
from rest_api.classes import APIEndPoint from rest_api.classes import APIEndPoint, APIResource
from rest_api.fields import DynamicSerializerField from rest_api.fields import DynamicSerializerField
from statistics.classes import StatisticNamespace, CharJSLine from statistics.classes import StatisticNamespace, CharJSLine
@@ -91,6 +91,9 @@ class DocumentsApp(MayanAppConfig):
from actstream import registry from actstream import registry
APIEndPoint(app=self, version_string='1') 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') DeletedDocument = self.get_model('DeletedDocument')
Document = self.get_model('Document') Document = self.get_model('Document')

View File

@@ -29,7 +29,7 @@ api_urls = [
name='search-view' name='search-view'
), ),
url( url(
r'^advanced/(?P<search_model>[\.\w]+)/$', APIAdvancedSearchView.as_view(), r'^search/advanced/(?P<search_model>[\.\w]+)/$', APIAdvancedSearchView.as_view(),
name='advanced-search-view' name='advanced-search-view'
), ),
] ]

View File

@@ -20,10 +20,10 @@ urlpatterns = [
] ]
api_urls = [ 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'^events/$', APIEventListView.as_view(), name='event-list'),
url( url(
r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/events/$', r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/events/$',
APIObjectEventListView.as_view(), name='object-event-list' APIObjectEventListView.as_view(), name='object-event-list'
), ),
] ]

View File

@@ -60,7 +60,7 @@ api_urls = [
), ),
url(r'^folders/$', APIFolderListView.as_view(), name='folder-list'), url(r'^folders/$', APIFolderListView.as_view(), name='folder-list'),
url( url(
r'^document/(?P<pk>[0-9]+)/folders/$', r'^documents/(?P<pk>[0-9]+)/folders/$',
APIDocumentFolderListView.as_view(), name='document-folder-list' APIDocumentFolderListView.as_view(), name='document-folder-list'
), ),
] ]

View File

@@ -3,6 +3,8 @@ from __future__ import absolute_import, unicode_literals
from rest_framework import generics, status from rest_framework import generics, status
from rest_framework.response import Response from rest_framework.response import Response
from django.shortcuts import get_object_or_404
from documents.models import Document, DocumentPage, DocumentVersion from documents.models import Document, DocumentPage, DocumentVersion
from rest_api.permissions import MayanPermission from rest_api.permissions import MayanPermission
@@ -26,10 +28,6 @@ class APIDocumentOCRView(generics.GenericAPIView):
Submit a document for OCR. Submit a document for OCR.
--- ---
omit_serializer: true omit_serializer: true
parameters:
- name: pk
paramType: path
type: number
responseMessages: responseMessages:
- code: 202 - code: 202
message: Accepted message: Accepted
@@ -40,12 +38,19 @@ class APIDocumentOCRView(generics.GenericAPIView):
class APIDocumentVersionOCRView(generics.GenericAPIView): class APIDocumentVersionOCRView(generics.GenericAPIView):
lookup_url_kwarg = 'version_pk'
mayan_object_permissions = { mayan_object_permissions = {
'POST': (permission_ocr_document,) 'POST': (permission_ocr_document,)
} }
permission_classes = (MayanPermission,) permission_classes = (MayanPermission,)
queryset = DocumentVersion.objects.all() 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): def get_serializer_class(self):
return None return None
@@ -54,10 +59,6 @@ class APIDocumentVersionOCRView(generics.GenericAPIView):
Submit a document version for OCR. Submit a document version for OCR.
--- ---
omit_serializer: true omit_serializer: true
parameters:
- name: pk
paramType: path
type: number
responseMessages: responseMessages:
- code: 202 - code: 202
message: Accepted message: Accepted
@@ -70,20 +71,25 @@ class APIDocumentVersionOCRView(generics.GenericAPIView):
class APIDocumentPageContentView(generics.RetrieveAPIView): class APIDocumentPageContentView(generics.RetrieveAPIView):
""" """
Returns the OCR content of the selected document page. 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 = { mayan_object_permissions = {
'GET': (permission_ocr_content_view,), 'GET': (permission_ocr_content_view,),
} }
permission_classes = (MayanPermission,) permission_classes = (MayanPermission,)
serializer_class = DocumentPageContentSerializer 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): def retrieve(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()

View File

@@ -63,7 +63,9 @@ class OCRAPITestCase(BaseAPITestCase):
response = self.client.post( response = self.client.post(
reverse( reverse(
'rest_api:document-version-ocr-submit-view', '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( response = self.client.get(
reverse( reverse(
'rest_api:document-page-content-view', '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,
)
), ),
) )

View File

@@ -43,16 +43,16 @@ urlpatterns = [
api_urls = [ api_urls = [
url( url(
r'^document/(?P<pk>\d+)/submit/$', APIDocumentOCRView.as_view(), r'^documents/(?P<pk>\d+)/ocr/$', APIDocumentOCRView.as_view(),
name='document-ocr-submit-view' name='document-ocr-submit-view'
), ),
url( url(
r'^document_version/(?P<pk>\d+)/submit/$', r'^documents/(?P<document_pk>\d+)/versions/(?P<version_pk>\d+)/ocr/$',
APIDocumentVersionOCRView.as_view(), APIDocumentVersionOCRView.as_view(),
name='document-version-ocr-submit-view' name='document-version-ocr-submit-view'
), ),
url( url(
r'^page/(?P<pk>\d+)/content/$', APIDocumentPageContentView.as_view(), r'^documents/(?P<document_pk>\d+)/versions/(?P<version_pk>\d+)/pages/(?P<page_pk>\d+)/ocr/$', APIDocumentPageContentView.as_view(),
name='document-page-content-view' name='document-page-content-view'
), ),
] ]

View File

@@ -30,5 +30,4 @@ api_urls = [
url(r'^permissions/$', APIPermissionList.as_view(), name='permission-list'), url(r'^permissions/$', APIPermissionList.as_view(), name='permission-list'),
url(r'^roles/$', APIRoleListView.as_view(), name='role-list'), url(r'^roles/$', APIRoleListView.as_view(), name='role-list'),
url(r'^roles/(?P<pk>[0-9]+)/$', APIRoleView.as_view(), name='role-detail'), url(r'^roles/(?P<pk>[0-9]+)/$', APIRoleView.as_view(), name='role-detail'),
url(r'^$', APIPermissionList.as_view(), name='permission-list'),
] ]

View File

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

View File

@@ -4,9 +4,33 @@ from django.conf.urls import include, url
from django.conf import settings from django.conf import settings
from django.utils.module_loading import import_string 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): class APIEndPoint(object):
_registry = {} _registry = {}
_patterns = []
@classmethod @classmethod
def get_all(cls): def get_all(cls):
@@ -46,6 +70,12 @@ class APIEndPoint(object):
def register_urls(self, urlpatterns): def register_urls(self, urlpatterns):
from .urls import urlpatterns as app_urls from .urls import urlpatterns as app_urls
app_urls += [ for url in urlpatterns:
url(r'^%s/' % (self.name or self.app.name), include(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)
)

View File

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

View File

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

View File

@@ -2,15 +2,18 @@ from __future__ import unicode_literals
from django.conf.urls import url 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 = [ api_urls = [
url(r'^$', APIBase.as_view(), name='api_root'), url(r'^$', APIBase.as_view(), name='api_root'),
url(r'^api/(?P<path>.*)/?$', APIAppView.as_view(), name='api_app'), url(
r'^resources/$', APIResourceTypeListView.as_view(),
name='resource-list'
),
url( url(
r'^auth/token/obtain/$', BrowseableObtainAuthToken.as_view(), r'^auth/token/obtain/$', BrowseableObtainAuthToken.as_view(),
name='auth_token_obtain' name='auth_token_obtain'

View File

@@ -13,13 +13,6 @@ class APIBase(SwaggerResourcesView):
renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer) renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer)
class APIAppView(SwaggerApiView):
"""
Entry points of the selected app.
"""
renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer)
class BrowseableObtainAuthToken(ObtainAuthToken): class BrowseableObtainAuthToken(ObtainAuthToken):
""" """
Obtain an API authentication token. Obtain an API authentication token.