diff --git a/mayan/apps/document_states/api_views.py b/mayan/apps/document_states/api_views.py index 3caf7e78bf..a0b287560e 100644 --- a/mayan/apps/document_states/api_views.py +++ b/mayan/apps/document_states/api_views.py @@ -7,19 +7,19 @@ 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, WorkflowState +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, WorkflowStateSerializer, WritableWorkflowSerializer + WorkflowSerializer, WorkflowStateSerializer, WorkflowTransitionSerializer, + WritableWorkflowSerializer, WritableWorkflowTransitionSerializer ) @@ -245,7 +245,7 @@ class APIWorkflowView(generics.RetrieveUpdateDestroyAPIView): return super(APIWorkflowView, self).put(*args, **kwargs) -## Workflow state views +# Workflow state views class APIWorkflowStateListView(generics.ListCreateAPIView): @@ -299,6 +299,7 @@ class APIWorkflowStateListView(generics.ListCreateAPIView): class APIWorkflowStateView(generics.RetrieveUpdateDestroyAPIView): lookup_url_kwarg = 'state_pk' + serializer_class = WorkflowStateSerializer def delete(self, *args, **kwargs): """ @@ -317,12 +318,6 @@ class APIWorkflowStateView(generics.RetrieveUpdateDestroyAPIView): def get_queryset(self): return self.get_workflow().states.all() - def get_serializer_class(self): - if self.request.method == 'GET': - return WorkflowStateSerializer - else: - return WorkflowStateSerializer # TODO: Writable - def get_serializer_context(self): """ Extra context provided to the serializer class. @@ -368,3 +363,128 @@ class APIWorkflowStateView(generics.RetrieveUpdateDestroyAPIView): return super(APIWorkflowStateView, self).put(*args, **kwargs) +# Workflow transition views + + +class APIWorkflowTransitionListView(generics.ListCreateAPIView): + def get(self, *args, **kwargs): + """ + Returns a list of all the workflow transitions. + """ + return super(APIWorkflowTransitionListView, self).get(*args, **kwargs) + + def get_queryset(self): + return self.get_workflow().transitions.all() + + def get_serializer_class(self): + if self.request.method == 'GET': + return WorkflowTransitionSerializer + else: + return WritableWorkflowTransitionSerializer + + 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): + 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 post(self, *args, **kwargs): + """ + Create a new workflow transition. + """ + return super(APIWorkflowTransitionListView, self).post(*args, **kwargs) + + +class APIWorkflowTransitionView(generics.RetrieveUpdateDestroyAPIView): + lookup_url_kwarg = 'transition_pk' + + def delete(self, *args, **kwargs): + """ + Delete the selected workflow transition. + """ + + return super(APIWorkflowTransitionView, self).delete(*args, **kwargs) + + def get(self, *args, **kwargs): + """ + Return the details of the selected workflow transition. + """ + + return super(APIWorkflowTransitionView, self).get(*args, **kwargs) + + def get_queryset(self): + return self.get_workflow().transitions.all() + + def get_serializer_class(self): + if self.request.method == 'GET': + return WorkflowTransitionSerializer + else: + return WritableWorkflowTransitionSerializer + + 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): + 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 patch(self, *args, **kwargs): + """ + Edit the selected workflow transition. + """ + + return super(APIWorkflowTransitionView, self).patch(*args, **kwargs) + + def put(self, *args, **kwargs): + """ + Edit the selected workflow transition. + """ + + return super(APIWorkflowTransitionView, self).put(*args, **kwargs) diff --git a/mayan/apps/document_states/serializers.py b/mayan/apps/document_states/serializers.py index 947d9ced86..3f44f4a4dc 100644 --- a/mayan/apps/document_states/serializers.py +++ b/mayan/apps/document_states/serializers.py @@ -8,7 +8,7 @@ from rest_framework.reverse import reverse from documents.models import DocumentType from documents.serializers import DocumentTypeSerializer -from .models import Workflow, WorkflowState +from .models import Workflow, WorkflowState, WorkflowTransition class NewWorkflowDocumentTypeSerializer(serializers.Serializer): @@ -49,8 +49,8 @@ class WorkflowDocumentTypeSerializer(DocumentTypeSerializer): class WorkflowStateSerializer(serializers.HyperlinkedModelSerializer): - workflow_url = serializers.SerializerMethodField() url = serializers.SerializerMethodField() + workflow_url = serializers.SerializerMethodField() class Meta: fields = ( @@ -77,18 +77,107 @@ class WorkflowStateSerializer(serializers.HyperlinkedModelSerializer): ) +class WorkflowTransitionSerializer(serializers.HyperlinkedModelSerializer): + destination_state = WorkflowStateSerializer() + origin_state = WorkflowStateSerializer() + url = serializers.SerializerMethodField() + workflow_url = serializers.SerializerMethodField() + + class Meta: + fields = ( + 'destination_state', 'id', 'label', 'origin_state', 'url', + 'workflow_url', + ) + model = WorkflowTransition + + def get_url(self, instance): + return reverse( + 'rest_api:workflowtransition-detail', args=( + instance.workflow.pk, instance.pk + ), request=self.context['request'], format=self.context['format'] + ) + + def get_workflow_url(self, instance): + return reverse( + 'rest_api:workflow-detail', args=( + instance.workflow.pk, + ), request=self.context['request'], format=self.context['format'] + ) + + +class WritableWorkflowTransitionSerializer(serializers.ModelSerializer): + destination_state_pk = serializers.IntegerField( + help_text=_('Primary key of the destination state to be added.'), + write_only=True + ) + origin_state_pk = serializers.IntegerField( + help_text=_('Primary key of the origin state to be added.'), + write_only=True + ) + url = serializers.SerializerMethodField() + workflow_url = serializers.SerializerMethodField() + + class Meta: + fields = ( + 'destination_state_pk', 'id', 'label', 'origin_state_pk', 'url', + 'workflow_url', + ) + model = WorkflowTransition + + def create(self, validated_data): + validated_data['destination_state'] = WorkflowState.objects.get( + pk=validated_data.pop('destination_state_pk') + ) + validated_data['origin_state'] = WorkflowState.objects.get( + pk=validated_data.pop('origin_state_pk') + ) + + validated_data['workflow'] = self.context['workflow'] + return super(WritableWorkflowTransitionSerializer, self).create( + validated_data + ) + + def get_url(self, instance): + return reverse( + 'rest_api:workflowtransition-detail', args=( + instance.workflow.pk, instance.pk + ), request=self.context['request'], format=self.context['format'] + ) + + def get_workflow_url(self, instance): + return reverse( + 'rest_api:workflow-detail', args=( + instance.workflow.pk, + ), request=self.context['request'], format=self.context['format'] + ) + + def update(self, instance, validated_data): + validated_data['destination_state'] = WorkflowState.objects.get( + pk=validated_data.pop('destination_state_pk') + ) + validated_data['origin_state'] = WorkflowState.objects.get( + pk=validated_data.pop('origin_state_pk') + ) + + return super(WritableWorkflowTransitionSerializer, self).update( + instance, validated_data + ) + + class WorkflowSerializer(serializers.HyperlinkedModelSerializer): document_types_url = serializers.HyperlinkedIdentityField( view_name='rest_api:workflow-document-type-list' ) states = WorkflowStateSerializer(many=True, required=False) + transitions = WorkflowTransitionSerializer(many=True, required=False) class Meta: extra_kwargs = { 'url': {'view_name': 'rest_api:workflow-detail'}, } fields = ( - 'document_types_url', 'id', 'label', 'states', 'url' + 'document_types_url', 'id', 'label', 'states', 'transitions', + 'url' ) model = Workflow diff --git a/mayan/apps/document_states/tests/literals.py b/mayan/apps/document_states/tests/literals.py index 215d0093cd..aadc1485e1 100644 --- a/mayan/apps/document_states/tests/literals.py +++ b/mayan/apps/document_states/tests/literals.py @@ -4,6 +4,8 @@ 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' +TEST_WORKFLOW_STATE_LABEL = 'test state label' +TEST_WORKFLOW_STATE_LABEL_EDITED = 'test state label edited' TEST_WORKFLOW_STATE_COMPLETION = 66 -TEST_WORKFLOW_TRANSITION_LABEL = 'test transtition' +TEST_WORKFLOW_TRANSITION_LABEL = 'test transtition label' +TEST_WORKFLOW_TRANSITION_LABEL_EDITED = 'test transtition label edited' diff --git a/mayan/apps/document_states/tests/test_api.py b/mayan/apps/document_states/tests/test_api.py index 7da2cecd1a..491f1ba539 100644 --- a/mayan/apps/document_states/tests/test_api.py +++ b/mayan/apps/document_states/tests/test_api.py @@ -8,7 +8,6 @@ from django.utils.encoding import force_text 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 ) @@ -20,7 +19,10 @@ from ..models import Workflow from .literals import ( TEST_WORKFLOW_LABEL, TEST_WORKFLOW_LABEL_EDITED, - TEST_WORKFLOW_STATE_COMPLETION, TEST_WORKFLOW_STATE_LABEL + TEST_WORKFLOW_INITIAL_STATE_COMPLETION, TEST_WORKFLOW_INITIAL_STATE_LABEL, + TEST_WORKFLOW_STATE_COMPLETION, TEST_WORKFLOW_STATE_LABEL, + TEST_WORKFLOW_STATE_LABEL_EDITED, TEST_WORKFLOW_TRANSITION_LABEL, + TEST_WORKFLOW_TRANSITION_LABEL_EDITED ) @@ -99,7 +101,7 @@ class WorkflowAPITestCase(APITestCase): def test_workflow_document_type_create_view(self): workflow = self._create_workflow() - response = self.client.post( + self.client.post( reverse( 'rest_api:workflow-document-type-list', args=(workflow.pk,) @@ -114,7 +116,7 @@ class WorkflowAPITestCase(APITestCase): workflow = self._create_workflow() workflow.document_types.add(self.document_type) - response = self.client.delete( + self.client.delete( reverse( 'rest_api:workflow-document-type-detail', args=(workflow.pk, self.document_type.pk) @@ -147,8 +149,9 @@ class WorkflowAPITestCase(APITestCase): workflow.document_types.add(self.document_type) response = self.client.get( - reverse('rest_api:workflow-document-type-list', - args=(workflow.pk,)) + reverse( + 'rest_api:workflow-document-type-list', args=(workflow.pk,) + ) ) self.assertEqual( @@ -165,7 +168,7 @@ class WorkflowAPITestCase(APITestCase): def test_workflow_put_view(self): workflow = self._create_workflow() - response = self.client.put( + self.client.put( reverse('rest_api:workflow-detail', args=(workflow.pk,)), data={'label': TEST_WORKFLOW_LABEL_EDITED} ) @@ -176,7 +179,7 @@ class WorkflowAPITestCase(APITestCase): def test_workflow_patch_view(self): workflow = self._create_workflow() - response = self.client.patch( + self.client.patch( reverse('rest_api:workflow-detail', args=(workflow.pk,)), data={'label': TEST_WORKFLOW_LABEL_EDITED} ) @@ -223,7 +226,7 @@ class WorkflowStatesAPITestCase(APITestCase): def test_workflow_state_create_view(self): self._create_workflow() - response = self.client.post( + self.client.post( reverse( 'rest_api:workflowstate-list', args=(self.workflow.pk,) ), data={ @@ -241,7 +244,7 @@ class WorkflowStatesAPITestCase(APITestCase): def test_workflow_state_delete_view(self): self._create_workflow_state() - response = self.client.delete( + self.client.delete( reverse( 'rest_api:workflowstate-detail', args=(self.workflow.pk, self.workflow_state.pk) @@ -276,3 +279,209 @@ class WorkflowStatesAPITestCase(APITestCase): self.assertEqual( response.data['results'][0]['label'], TEST_WORKFLOW_STATE_LABEL ) + + def test_workflow_state_patch_view(self): + self._create_workflow_state() + + self.client.patch( + reverse( + 'rest_api:workflowstate-detail', + args=(self.workflow.pk, self.workflow_state.pk) + ), + data={'label': TEST_WORKFLOW_STATE_LABEL_EDITED} + ) + + self.workflow_state.refresh_from_db() + + self.assertEqual( + self.workflow_state.label, + TEST_WORKFLOW_STATE_LABEL_EDITED + ) + + def test_workflow_state_put_view(self): + self._create_workflow_state() + + self.client.put( + reverse( + 'rest_api:workflowstate-detail', + args=(self.workflow.pk, self.workflow_state.pk) + ), + data={'label': TEST_WORKFLOW_STATE_LABEL_EDITED} + ) + + self.workflow_state.refresh_from_db() + + self.assertEqual( + self.workflow_state.label, + TEST_WORKFLOW_STATE_LABEL_EDITED + ) + + +@override_settings(OCR_AUTO_OCR=False) +class WorkflowTransitionsAPITestCase(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): + self.workflow = Workflow.objects.create(label=TEST_WORKFLOW_LABEL) + + def _create_workflow_states(self): + self._create_workflow() + self.workflow_state_1 = self.workflow.states.create( + completion=TEST_WORKFLOW_INITIAL_STATE_COMPLETION, + label=TEST_WORKFLOW_INITIAL_STATE_LABEL + ) + self.workflow_state_2 = self.workflow.states.create( + completion=TEST_WORKFLOW_STATE_COMPLETION, + label=TEST_WORKFLOW_STATE_LABEL + ) + + def _create_workflow_transition(self): + self._create_workflow_states() + self.workflow_transition = self.workflow.transitions.create( + label=TEST_WORKFLOW_TRANSITION_LABEL, + origin_state=self.workflow_state_1, + destination_state=self.workflow_state_2, + ) + + def test_workflow_transition_create_view(self): + self._create_workflow_states() + + self.client.post( + reverse( + 'rest_api:workflowtransition-list', args=(self.workflow.pk,) + ), data={ + 'label': TEST_WORKFLOW_TRANSITION_LABEL, + 'origin_state_pk': self.workflow_state_1.pk, + 'destination_state_pk': self.workflow_state_2.pk, + } + ) + + self.workflow.refresh_from_db() + + self.assertEqual( + self.workflow.transitions.first().label, + TEST_WORKFLOW_TRANSITION_LABEL + ) + + def test_workflow_transition_delete_view(self): + self._create_workflow_transition() + + self.client.delete( + reverse( + 'rest_api:workflowtransition-detail', + args=(self.workflow.pk, self.workflow_transition.pk) + ), + ) + + self.workflow.refresh_from_db() + + self.assertEqual(self.workflow.transitions.count(), 0) + + def test_workflow_transition_detail_view(self): + self._create_workflow_transition() + + response = self.client.get( + reverse( + 'rest_api:workflowtransition-detail', + args=(self.workflow.pk, self.workflow_transition.pk) + ), + ) + + self.assertEqual( + response.data['label'], TEST_WORKFLOW_TRANSITION_LABEL + ) + + def test_workflow_transition_list_view(self): + self._create_workflow_transition() + + response = self.client.get( + reverse( + 'rest_api:workflowtransition-list', args=(self.workflow.pk,) + ), + ) + + self.assertEqual( + response.data['results'][0]['label'], + TEST_WORKFLOW_TRANSITION_LABEL + ) + + def test_workflow_transition_patch_view(self): + self._create_workflow_transition() + + self.client.patch( + reverse( + 'rest_api:workflowtransition-detail', + args=(self.workflow.pk, self.workflow_transition.pk) + ), + data={ + 'label': TEST_WORKFLOW_TRANSITION_LABEL_EDITED, + 'origin_state_pk': self.workflow_state_2.pk, + 'destination_state_pk': self.workflow_state_1.pk, + } + ) + + self.workflow_transition.refresh_from_db() + + self.assertEqual( + self.workflow_transition.label, + TEST_WORKFLOW_TRANSITION_LABEL_EDITED + ) + self.assertEqual( + self.workflow_transition.origin_state, + self.workflow_state_2 + ) + self.assertEqual( + self.workflow_transition.destination_state, + self.workflow_state_1 + ) + + def test_workflow_transition_put_view(self): + self._create_workflow_transition() + + self.client.put( + reverse( + 'rest_api:workflowtransition-detail', + args=(self.workflow.pk, self.workflow_transition.pk) + ), + data={ + 'label': TEST_WORKFLOW_TRANSITION_LABEL_EDITED, + 'origin_state_pk': self.workflow_state_2.pk, + 'destination_state_pk': self.workflow_state_1.pk, + } + ) + + self.workflow_transition.refresh_from_db() + + self.assertEqual( + self.workflow_transition.label, + TEST_WORKFLOW_TRANSITION_LABEL_EDITED + ) + self.assertEqual( + self.workflow_transition.origin_state, + self.workflow_state_2 + ) + self.assertEqual( + self.workflow_transition.destination_state, + self.workflow_state_1 + ) diff --git a/mayan/apps/document_states/urls.py b/mayan/apps/document_states/urls.py index 5eced2ab0c..bcb25a6584 100644 --- a/mayan/apps/document_states/urls.py +++ b/mayan/apps/document_states/urls.py @@ -5,6 +5,7 @@ from django.conf.urls import patterns, url from .api_views import ( APIWorkflowDocumentTypeList, APIWorkflowDocumentTypeView, APIWorkflowListView, APIWorkflowStateListView, APIWorkflowStateView, + APIWorkflowTransitionListView, APIWorkflowTransitionView, APIWorkflowView ) from .views import ( @@ -104,14 +105,6 @@ urlpatterns = patterns( ) api_urls = [ - url( - r'^workflows/(?P[0-9]+)/states/$', - APIWorkflowStateListView.as_view(), name='workflowstate-list' - ), - url( - r'^workflows/(?P[0-9]+)/states/(?P[0-9]+)/$', - APIWorkflowStateView.as_view(), name='workflowstate-detail' - ), url(r'^workflows/$', APIWorkflowListView.as_view(), name='workflow-list'), url( r'^workflows/(?P[0-9]+)/$', APIWorkflowView.as_view(), @@ -127,4 +120,20 @@ api_urls = [ APIWorkflowDocumentTypeView.as_view(), name='workflow-document-type-detail' ), + url( + r'^workflows/(?P[0-9]+)/states/$', + APIWorkflowStateListView.as_view(), name='workflowstate-list' + ), + url( + r'^workflows/(?P[0-9]+)/states/(?P[0-9]+)/$', + APIWorkflowStateView.as_view(), name='workflowstate-detail' + ), + url( + r'^workflows/(?P[0-9]+)/transitions/$', + APIWorkflowTransitionListView.as_view(), name='workflowtransition-list' + ), + url( + r'^workflows/(?P[0-9]+)/transitions/(?P[0-9]+)/$', + APIWorkflowTransitionView.as_view(), name='workflowtransition-detail' + ), ]