diff --git a/mayan/apps/document_states/api_views.py b/mayan/apps/document_states/api_views.py new file mode 100644 index 0000000000..1f61c1980a --- /dev/null +++ b/mayan/apps/document_states/api_views.py @@ -0,0 +1,254 @@ +from __future__ import absolute_import, unicode_literals + +from django.core.exceptions import PermissionDenied +from django.shortcuts import get_object_or_404 + +from rest_framework import generics + +from acls.models import AccessControlList +from documents.permissions import permission_document_type_view +from documents.serializers import DocumentSerializer, DocumentTypeSerializer +from permissions import Permission +from rest_api.filters import MayanObjectPermissionsFilter +from rest_api.permissions import MayanPermission + +from .models import Workflow +from .permissions import ( + permission_workflow_create, permission_workflow_delete, + permission_workflow_edit, permission_workflow_view +) +from .serializers import ( + NewWorkflowDocumentTypeSerializer, WorkflowDocumentTypeSerializer, + WorkflowSerializer, WritableWorkflowSerializer +) + + +class APIWorkflowDocumentTypeList(generics.ListCreateAPIView): + filter_backends = (MayanObjectPermissionsFilter,) + mayan_object_permissions = { + 'GET': (permission_document_type_view,), + } + + def get(self, *args, **kwargs): + """ + Returns a list of all the document types attached to a workflow. + """ + + return super(APIWorkflowDocumentTypeList, self).get(*args, **kwargs) + + def get_queryset(self): + """ + This view returns a list of document types that belong to a workflow + RESEARCH: Could the documents.api_views.APIDocumentTypeList class + be subclasses for this? + """ + + return self.get_workflow().document_types.all() + + def get_serializer_class(self): + if self.request.method == 'GET': + return WorkflowDocumentTypeSerializer + elif self.request.method == 'POST': + return NewWorkflowDocumentTypeSerializer + + def get_serializer_context(self): + """ + Extra context provided to the serializer class. + """ + + return { + 'format': self.format_kwarg, + 'request': self.request, + 'workflow': self.get_workflow(), + 'view': self + } + + def get_workflow(self): + """ + Retrieve the parent workflow of the workflow document type. + Perform custom permission and access check. + """ + + if self.request.method == 'GET': + permission_required = permission_workflow_view + else: + permission_required = permission_workflow_edit + + workflow = get_object_or_404(Workflow, pk=self.kwargs['pk']) + + try: + Permission.check_permissions( + self.request.user, (permission_required,) + ) + except PermissionDenied: + AccessControlList.objects.check_access( + permission_required, self.request.user, workflow + ) + + return workflow + + #def perform_create(self, serializer): + # """ + # RESEARCH: This is not needed if the serializer uses the context + # dictionary instead. However is that an acceptable "proper" way + # to do it? + # """ + # + # serializer.save(workflow=self.get_workflow()) + + def post(self, request, *args, **kwargs): + """ + Attach a document type to a specified workflow. + """ + + return super( + APIWorkflowDocumentTypeList, self + ).post(request, *args, **kwargs) + + +class APIWorkflowDocumentTypeView(generics.RetrieveDestroyAPIView): + filter_backends = (MayanObjectPermissionsFilter,) + lookup_url_kwarg = 'document_pk' + mayan_object_permissions = { + 'GET': (permission_document_type_view,), + } + serializer_class = WorkflowDocumentTypeSerializer + + def delete(self, request, *args, **kwargs): + """ + Remove a document type from the selected workflow. + """ + + return super( + APIWorkflowDocumentTypeView, self + ).delete(request, *args, **kwargs) + + def get(self, *args, **kwargs): + """ + Returns the details of the selected workflow document type. + """ + + return super(APIWorkflowDocumentTypeView, self).get(*args, **kwargs) + + def get_queryset(self): + return self.get_workflow().document_types.all() + + def get_serializer_context(self): + """ + Extra context provided to the serializer class. + """ + return { + 'format': self.format_kwarg, + 'request': self.request, + 'workflow': self.get_workflow(), + 'view': self + } + + def get_workflow(self): + """ + This view returns a document types that belongs to a workflow + RESEARCH: Could the documents.api_views.APIDocumentTypeView class + be subclasses for this? + RESEARCH: Since this is a parent-child API view could this be made + into a generic API class? + RESEARCH: Reuse get_workflow method from APIWorkflowDocumentTypeList? + """ + + if self.request.method == 'GET': + permission_required = permission_workflow_view + else: + permission_required = permission_workflow_edit + + workflow = get_object_or_404(Workflow, pk=self.kwargs['pk']) + + try: + Permission.check_permissions( + self.request.user, (permission_required,) + ) + except PermissionDenied: + AccessControlList.objects.check_access( + permission_required, self.request.user, workflow + ) + + return workflow + + def perform_destroy(self, instance): + """ + RESEARCH: Move this kind of methods to the serializer instead it that + ability becomes available in Django REST framework + """ + print "DESTROY!" + self.get_workflow().documents.remove(instance) + + +class APIWorkflowListView(generics.ListCreateAPIView): + filter_backends = (MayanObjectPermissionsFilter,) + mayan_object_permissions = { + 'GET': (permission_workflow_view,), + 'POST': (permission_workflow_create,) + } + permission_classes = (MayanPermission,) + queryset = Workflow.objects.all() + + def get(self, *args, **kwargs): + """ + Returns a list of all the workflows. + """ + return super(APIWorkflowListView, self).get(*args, **kwargs) + + def get_serializer_class(self): + if self.request.method == 'GET': + return WorkflowSerializer + else: + return WritableWorkflowSerializer + + def post(self, *args, **kwargs): + """ + Create a new workflow. + """ + return super(APIWorkflowListView, self).post(*args, **kwargs) + + +class APIWorkflowView(generics.RetrieveUpdateDestroyAPIView): + filter_backends = (MayanObjectPermissionsFilter,) + mayan_object_permissions = { + 'DELETE': (permission_workflow_delete,), + 'GET': (permission_workflow_view,), + 'PATCH': (permission_workflow_edit,), + 'PUT': (permission_workflow_edit,) + } + queryset = Workflow.objects.all() + + def delete(self, *args, **kwargs): + """ + Delete the selected workflow. + """ + + return super(APIWorkflowView, self).delete(*args, **kwargs) + + def get(self, *args, **kwargs): + """ + Return the details of the selected workflow. + """ + + return super(APIWorkflowView, self).get(*args, **kwargs) + + def get_serializer_class(self): + if self.request.method == 'GET': + return WorkflowSerializer + else: + return WritableWorkflowSerializer + + def patch(self, *args, **kwargs): + """ + Edit the selected workflow. + """ + + return super(APIWorkflowView, self).patch(*args, **kwargs) + + def put(self, *args, **kwargs): + """ + Edit the selected workflow. + """ + + return super(APIWorkflowView, self).put(*args, **kwargs) diff --git a/mayan/apps/document_states/apps.py b/mayan/apps/document_states/apps.py index 7e71d5b1a2..706d7435d3 100644 --- a/mayan/apps/document_states/apps.py +++ b/mayan/apps/document_states/apps.py @@ -10,6 +10,7 @@ from common import ( ) from common.widgets import two_state_template from navigation import SourceColumn +from rest_api.classes import APIEndPoint from .handlers import launch_workflow from .links import ( @@ -33,6 +34,8 @@ class DocumentStatesApp(MayanAppConfig): def ready(self): super(DocumentStatesApp, self).ready() + APIEndPoint(app=self, version_string='1') + Document = apps.get_model( app_label='documents', model_name='Document' ) diff --git a/mayan/apps/document_states/serializers.py b/mayan/apps/document_states/serializers.py new file mode 100644 index 0000000000..33337b2819 --- /dev/null +++ b/mayan/apps/document_states/serializers.py @@ -0,0 +1,123 @@ +from __future__ import unicode_literals + +from django.utils.translation import ugettext_lazy as _ + +from rest_framework import serializers +from rest_framework.reverse import reverse + +from documents.models import DocumentType +from documents.serializers import DocumentTypeSerializer + +from .models import Workflow + + +class NewWorkflowDocumentTypeSerializer(serializers.Serializer): + document_type_pk = serializers.IntegerField( + help_text=_('Primary key of the document type to be added.') + ) + + def create(self, validated_data): + document_type = DocumentType.objects.get( + pk=validated_data['document_type_pk'] + ) + self.context['workflow'].document_types.add(document_type) + + return validated_data + + +class WorkflowDocumentTypeSerializer(DocumentTypeSerializer): + workflow_document_type_url = serializers.SerializerMethodField( + help_text=_( + 'API URL pointing to a document type in relation to the ' + 'workflow to which it is attached. This URL is different than ' + 'the canonical document type URL.' + ) + ) + + class Meta(DocumentTypeSerializer.Meta): + fields = DocumentTypeSerializer.Meta.fields + ( + 'workflow_document_type_url', + ) + read_only_fields = DocumentTypeSerializer.Meta.fields + + def get_workflow_document_type_url(self, instance): + return reverse( + 'rest_api:workflow-document-type-detail', args=( + self.context['workflow'].pk, instance.pk + ), request=self.context['request'], format=self.context['format'] + ) + + +class WorkflowSerializer(serializers.HyperlinkedModelSerializer): + document_types_url = serializers.HyperlinkedIdentityField( + view_name='rest_api:workflow-document-type-list' + ) + + class Meta: + extra_kwargs = { + 'url': {'view_name': 'rest_api:workflow-detail'}, + } + fields = ( + 'document_types_url', 'get_initial_state', 'id', 'label', 'url' + ) + model = Workflow + + +class WritableWorkflowSerializer(serializers.ModelSerializer): + document_types_pk_list = serializers.CharField( + help_text=_( + 'Comma separated list of document type primary keys to which this ' + 'workflow will be attached.' + ), required=False + ) + + class Meta: + extra_kwargs = { + 'url': {'view_name': 'rest_api:workflow-detail'}, + } + fields = ( + 'document_types_pk_list', 'label', 'id', 'url', + ) + model = Workflow + + def _add_document_types(self, document_types_pk_list, instance): + instance.document_types.add( + *DocumentType.objects.filter( + pk__in=document_types_pk_list.split(',') + ) + ) + + def create(self, validated_data): + document_types_pk_list = validated_data.pop( + 'document_types_pk_list', '' + ) + + instance = super(WritableWorkflowSerializer, self).create( + validated_data + ) + + if document_types_pk_list: + self._add_document_types( + document_types_pk_list=document_types_pk_list, + instance=instance + ) + + return instance + + def update(self, instance, validated_data): + document_types_pk_list = validated_data.pop( + 'document_types_pk_list', '' + ) + + instance = super(WritableWorkflowSerializer, self).update( + instance, validated_data + ) + + if document_types_pk_list: + instance.documents.clear() + self._add_documents( + document_types_pk_list=document_types_pk_list, + instance=instance + ) + + return instance diff --git a/mayan/apps/document_states/tests/literals.py b/mayan/apps/document_states/tests/literals.py index c8fa5f52d8..215d0093cd 100644 --- a/mayan/apps/document_states/tests/literals.py +++ b/mayan/apps/document_states/tests/literals.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals -TEST_WORKFLOW_LABEL = 'test workflow' +TEST_WORKFLOW_LABEL = 'test workflow label' +TEST_WORKFLOW_LABEL_EDITED = 'test workflow label edited' TEST_WORKFLOW_INITIAL_STATE_LABEL = 'test initial state' TEST_WORKFLOW_INITIAL_STATE_COMPLETION = 33 TEST_WORKFLOW_STATE_LABEL = 'test state' diff --git a/mayan/apps/document_states/tests/test_api.py b/mayan/apps/document_states/tests/test_api.py new file mode 100644 index 0000000000..3fd55a3f32 --- /dev/null +++ b/mayan/apps/document_states/tests/test_api.py @@ -0,0 +1,123 @@ +from __future__ import unicode_literals + +from django.contrib.auth import get_user_model +from django.core.urlresolvers import reverse +from django.test import override_settings + +from rest_framework.test import APITestCase + +from documents.models import DocumentType +from documents.permissions import permission_document_type_view +from documents.tests.literals 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 Workflow + +from .literals import TEST_WORKFLOW_LABEL, TEST_WORKFLOW_LABEL_EDITED + + +@override_settings(OCR_AUTO_OCR=False) +class WorkflowAPITestCase(APITestCase): + def setUp(self): + 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): + if hasattr(self, 'document_type'): + self.document_type.delete() + + def _create_workflow(self): + return Workflow.objects.create(label=TEST_WORKFLOW_LABEL) + + def test_workflow_create_view(self): + response = self.client.post( + reverse('rest_api:workflow-list'), { + 'label': TEST_WORKFLOW_LABEL + } + ) + + workflow = Workflow.objects.first() + self.assertEqual(Workflow.objects.count(), 1) + self.assertEqual(response.data['id'], workflow.pk) + + def test_workflow_create_with_document_type_view(self): + response = self.client.post( + reverse('rest_api:workflow-list'), { + 'label': TEST_WORKFLOW_LABEL, + 'document_types_pk_list': '{}'.format(self.document_type.pk) + } + ) + + workflow = Workflow.objects.first() + self.assertEqual(Workflow.objects.count(), 1) + self.assertQuerysetEqual( + workflow.document_types.all(), (repr(self.document_type),) + ) + self.assertEqual(response.data['id'], workflow.pk) + + def test_workflow_delete_view(self): + workflow = self._create_workflow() + + self.client.delete( + reverse('rest_api:workflow-detail', args=(workflow.pk,)) + ) + + self.assertEqual(Workflow.objects.count(), 0) + + def test_workflow_detail_view(self): + workflow = self._create_workflow() + + response = self.client.get( + reverse('rest_api:workflow-detail', args=(workflow.pk,)) + ) + + self.assertEqual(response.data['label'], workflow.label) + + def test_workflow_list_view(self): + workflow = self._create_workflow() + + response = self.client.get(reverse('rest_api:workflow-list')) + + self.assertEqual(response.data['results'][0]['label'], workflow.label) + + def test_workflow_put_view(self): + workflow = self._create_workflow() + + response = self.client.put( + reverse('rest_api:workflow-detail', args=(workflow.pk,)), + data={'label': TEST_WORKFLOW_LABEL_EDITED} + ) + + workflow.refresh_from_db() + self.assertEqual(workflow.label, TEST_WORKFLOW_LABEL_EDITED) + + def test_workflow_patch_view(self): + workflow = self._create_workflow() + + response = self.client.patch( + reverse('rest_api:workflow-detail', args=(workflow.pk,)), + data={'label': TEST_WORKFLOW_LABEL_EDITED} + ) + + workflow.refresh_from_db() + self.assertEqual(workflow.label, TEST_WORKFLOW_LABEL_EDITED) + diff --git a/mayan/apps/document_states/urls.py b/mayan/apps/document_states/urls.py index b4f91b1c91..34b9bffd16 100644 --- a/mayan/apps/document_states/urls.py +++ b/mayan/apps/document_states/urls.py @@ -2,6 +2,10 @@ from __future__ import unicode_literals from django.conf.urls import patterns, url +from .api_views import ( + APIWorkflowDocumentTypeList, APIWorkflowDocumentTypeView, + APIWorkflowListView, APIWorkflowView +) from .views import ( DocumentWorkflowInstanceListView, SetupWorkflowCreateView, SetupWorkflowDeleteView, SetupWorkflowDocumentTypesView, @@ -97,3 +101,21 @@ urlpatterns = patterns( name='setup_workflow_transition_edit' ), ) + +api_urls = [ + url(r'^workflows/$', APIWorkflowListView.as_view(), name='workflow-list'), + url( + r'^workflows/(?P[0-9]+)/$', APIWorkflowView.as_view(), + name='workflow-detail' + ), + url( + r'^workflows/(?P[0-9]+)/document_types/$', + APIWorkflowDocumentTypeList.as_view(), + name='workflow-document-type-list' + ), + url( + r'^workflows/(?P[0-9]+)/document_types/(?P[0-9]+)/$', + APIWorkflowDocumentTypeView.as_view(), + name='workflow-document-type-detail' + ), +]