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:
@@ -71,7 +71,8 @@ Backward incompatible changes
|
||||
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.
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -2,21 +2,20 @@
|
||||
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.
|
||||
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.
|
||||
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
|
||||
file, they just modify the document's graphical representation.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -28,19 +28,19 @@ urlpatterns = [
|
||||
|
||||
api_urls = [
|
||||
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'
|
||||
),
|
||||
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'
|
||||
),
|
||||
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'
|
||||
),
|
||||
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'
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
75
mayan/apps/checkouts/tests/test_api.py
Normal file
75
mayan/apps/checkouts/tests/test_api.py
Normal 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
|
||||
)
|
||||
@@ -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<pk>[0-9]+)/$', APICheckedoutDocumentView.as_view(),
|
||||
r'^checkouts/(?P<pk>[0-9]+)/$', APICheckedoutDocumentView.as_view(),
|
||||
name='checkedout-document-view'
|
||||
),
|
||||
]
|
||||
|
||||
@@ -25,11 +25,11 @@ urlpatterns = [
|
||||
|
||||
api_urls = [
|
||||
url(
|
||||
r'^document/(?P<document_pk>[0-9]+)/comments/$',
|
||||
r'^documents/(?P<document_pk>[0-9]+)/comments/$',
|
||||
APICommentListView.as_view(), name='comment-list'
|
||||
),
|
||||
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'
|
||||
),
|
||||
]
|
||||
|
||||
@@ -72,12 +72,12 @@ urlpatterns = [
|
||||
|
||||
api_urls = [
|
||||
url(
|
||||
r'^index/node/(?P<pk>[0-9]+)/documents/$',
|
||||
r'^indexes/node/(?P<pk>[0-9]+)/documents/$',
|
||||
APIIndexNodeInstanceDocumentListView.as_view(),
|
||||
name='index-node-documents'
|
||||
),
|
||||
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'
|
||||
),
|
||||
url(
|
||||
@@ -85,12 +85,12 @@ api_urls = [
|
||||
name='index-detail'
|
||||
),
|
||||
url(
|
||||
r'^index/(?P<pk>[0-9]+)/template/$',
|
||||
r'^indexes/(?P<pk>[0-9]+)/template/$',
|
||||
APIIndexTemplateListView.as_view(), name='index-template-detail'
|
||||
),
|
||||
url(r'^indexes/$', APIIndexListView.as_view(), name='index-list'),
|
||||
url(
|
||||
r'^document/(?P<pk>[0-9]+)/indexes/$',
|
||||
r'^documents/(?P<pk>[0-9]+)/indexes/$',
|
||||
APIDocumentIndexListView.as_view(), name='document-index-list'
|
||||
),
|
||||
]
|
||||
|
||||
@@ -164,20 +164,20 @@ api_urls = [
|
||||
APIWorkflowTransitionView.as_view(), name='workflowtransition-detail'
|
||||
),
|
||||
url(
|
||||
r'^document/(?P<pk>[0-9]+)/workflows/$',
|
||||
r'^documents/(?P<pk>[0-9]+)/workflows/$',
|
||||
APIWorkflowInstanceListView.as_view(), name='workflowinstance-list'
|
||||
),
|
||||
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'
|
||||
),
|
||||
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(),
|
||||
name='workflowinstancelogentry-list'
|
||||
),
|
||||
url(
|
||||
r'^document_type/(?P<pk>[0-9]+)/workflows/$',
|
||||
r'^document_types/(?P<pk>[0-9]+)/workflows/$',
|
||||
APIDocumentTypeWorkflowListView.as_view(),
|
||||
name='documenttype-workflow-list'
|
||||
),
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -29,7 +29,7 @@ api_urls = [
|
||||
name='search-view'
|
||||
),
|
||||
url(
|
||||
r'^advanced/(?P<search_model>[\.\w]+)/$', APIAdvancedSearchView.as_view(),
|
||||
r'^search/advanced/(?P<search_model>[\.\w]+)/$', APIAdvancedSearchView.as_view(),
|
||||
name='advanced-search-view'
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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<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'
|
||||
),
|
||||
]
|
||||
|
||||
@@ -60,7 +60,7 @@ api_urls = [
|
||||
),
|
||||
url(r'^folders/$', APIFolderListView.as_view(), name='folder-list'),
|
||||
url(
|
||||
r'^document/(?P<pk>[0-9]+)/folders/$',
|
||||
r'^documents/(?P<pk>[0-9]+)/folders/$',
|
||||
APIDocumentFolderListView.as_view(), name='document-folder-list'
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -43,16 +43,16 @@ urlpatterns = [
|
||||
|
||||
api_urls = [
|
||||
url(
|
||||
r'^document/(?P<pk>\d+)/submit/$', APIDocumentOCRView.as_view(),
|
||||
r'^documents/(?P<pk>\d+)/ocr/$', APIDocumentOCRView.as_view(),
|
||||
name='document-ocr-submit-view'
|
||||
),
|
||||
url(
|
||||
r'^document_version/(?P<pk>\d+)/submit/$',
|
||||
r'^documents/(?P<document_pk>\d+)/versions/(?P<version_pk>\d+)/ocr/$',
|
||||
APIDocumentVersionOCRView.as_view(),
|
||||
name='document-version-ocr-submit-view'
|
||||
),
|
||||
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'
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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<pk>[0-9]+)/$', APIRoleView.as_view(), name='role-detail'),
|
||||
url(r'^$', APIPermissionList.as_view(), name='permission-list'),
|
||||
]
|
||||
|
||||
19
mayan/apps/rest_api/api_views.py
Normal file
19
mayan/apps/rest_api/api_views.py
Normal 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()
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
16
mayan/apps/rest_api/exceptions.py
Normal file
16
mayan/apps/rest_api/exceptions.py
Normal 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
|
||||
9
mayan/apps/rest_api/serializers.py
Normal file
9
mayan/apps/rest_api/serializers.py
Normal 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()
|
||||
@@ -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<path>.*)/?$', 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'
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user