From 7340989dc556b739025cb5d308b658914e036b6d Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 7 Feb 2017 20:46:30 -0400 Subject: [PATCH 01/32] Fix trashed document list API view. Add addition trashed document API tests. --- mayan/apps/documents/serializers.py | 4 +- mayan/apps/documents/tests/test_api.py | 133 ++++++++++++++-------- mayan/apps/documents/tests/test_models.py | 2 +- mayan/apps/documents/tests/test_views.py | 1 - mayan/apps/documents/urls.py | 3 +- 5 files changed, 89 insertions(+), 54 deletions(-) diff --git a/mayan/apps/documents/serializers.py b/mayan/apps/documents/serializers.py index 7ff99b2689..a6858e1de1 100644 --- a/mayan/apps/documents/serializers.py +++ b/mayan/apps/documents/serializers.py @@ -106,7 +106,7 @@ class NewDocumentVersionSerializer(serializers.Serializer): class DeletedDocumentSerializer(serializers.HyperlinkedModelSerializer): document_type_label = serializers.SerializerMethodField() restore = serializers.HyperlinkedIdentityField( - view_name='rest_api:deleteddocument-restore' + view_name='rest_api:trasheddocument-restore' ) def get_document_type_label(self, instance): @@ -115,7 +115,7 @@ class DeletedDocumentSerializer(serializers.HyperlinkedModelSerializer): class Meta: extra_kwargs = { 'document_type': {'view_name': 'rest_api:documenttype-detail'}, - 'url': {'view_name': 'rest_api:deleteddocument-detail'} + 'url': {'view_name': 'rest_api:trasheddocument-detail'} } fields = ( 'date_added', 'deleted_date_time', 'description', 'document_type', diff --git a/mayan/apps/documents/tests/test_api.py b/mayan/apps/documents/tests/test_api.py index 7195b01b15..fc1a8225c0 100644 --- a/mayan/apps/documents/tests/test_api.py +++ b/mayan/apps/documents/tests/test_api.py @@ -10,6 +10,7 @@ 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.six import BytesIO from rest_framework import status @@ -93,10 +94,6 @@ class DocumentTypeAPITestCase(APITestCase): @override_settings(OCR_AUTO_OCR=False) class DocumentAPITestCase(APITestCase): - """ - Test document API endpoints - """ - def setUp(self): self.admin_user = get_user_model().objects.create_superuser( username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL, @@ -155,51 +152,6 @@ class DocumentAPITestCase(APITestCase): ) self.assertEqual(document.page_count, 47) - def test_document_move_to_trash(self): - with open(TEST_SMALL_DOCUMENT_PATH) as file_object: - document = self.document_type.new_document( - file_object=file_object, - ) - - self.client.delete( - reverse('rest_api:document-detail', args=(document.pk,)) - ) - - self.assertEqual(Document.objects.count(), 0) - self.assertEqual(Document.trash.count(), 1) - - def test_deleted_document_delete_from_trash(self): - with open(TEST_SMALL_DOCUMENT_PATH) as file_object: - document = self.document_type.new_document( - file_object=file_object, - ) - - document.delete() - - self.assertEqual(Document.objects.count(), 0) - self.assertEqual(Document.trash.count(), 1) - - self.client.delete( - reverse('rest_api:trasheddocument-detail', args=(document.pk,)) - ) - - self.assertEqual(Document.trash.count(), 0) - - def test_deleted_document_restore(self): - with open(TEST_SMALL_DOCUMENT_PATH) as file_object: - document = self.document_type.new_document( - file_object=file_object, - ) - - document.delete() - - self.client.post( - reverse('rest_api:trasheddocument-restore', args=(document.pk,)) - ) - - self.assertEqual(Document.trash.count(), 0) - self.assertEqual(Document.objects.count(), 1) - def test_document_new_version_upload(self): with open(TEST_SMALL_DOCUMENT_PATH) as file_object: document = self.document_type.new_document( @@ -366,5 +318,88 @@ class DocumentAPITestCase(APITestCase): TEST_DOCUMENT_DESCRIPTION_EDITED ) + +@override_settings(OCR_AUTO_OCR=False) +class TrashedDocumentAPITestCase(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 + ) + + def tearDown(self): + self.admin_user.delete() + self.document_type.delete() + + def _upload_document(self): + with open(TEST_SMALL_DOCUMENT_PATH) as file_object: + document = self.document_type.new_document( + file_object=file_object, + ) + + return document + + def test_document_move_to_trash(self): + document = self._upload_document() + + self.client.delete( + reverse('rest_api:document-detail', args=(document.pk,)) + ) + + self.assertEqual(Document.objects.count(), 0) + self.assertEqual(Document.trash.count(), 1) + + def test_trashed_document_delete_from_trash(self): + document = self._upload_document() + document.delete() + + self.assertEqual(Document.objects.count(), 0) + self.assertEqual(Document.trash.count(), 1) + + self.client.delete( + reverse('rest_api:trasheddocument-detail', args=(document.pk,)) + ) + + self.assertEqual(Document.trash.count(), 0) + + def test_trashed_document_detail_view(self): + document = self._upload_document() + document.delete() + + response = self.client.get( + reverse('rest_api:trasheddocument-detail', args=(document.pk,)) + ) + + self.assertEqual(response.data['uuid'], force_text(document.uuid)) + + def test_trashed_document_list_view(self): + document = self._upload_document() + document.delete() + + response = self.client.get( + reverse('rest_api:trasheddocument-list') + ) + + self.assertEqual(response.data['results'][0]['uuid'], force_text(document.uuid)) + + def test_trashed_document_restore(self): + document = self._upload_document() + document.delete() + + self.client.post( + reverse('rest_api:trasheddocument-restore', args=(document.pk,)) + ) + + self.assertEqual(Document.trash.count(), 0) + self.assertEqual(Document.objects.count(), 1) + # TODO: def test_document_set_document_type(self): # pass diff --git a/mayan/apps/documents/tests/test_models.py b/mayan/apps/documents/tests/test_models.py index ff9983b656..a272d18b0b 100644 --- a/mayan/apps/documents/tests/test_models.py +++ b/mayan/apps/documents/tests/test_models.py @@ -4,7 +4,7 @@ from datetime import timedelta import time from common.tests import BaseTestCase -from django.test import TestCase, override_settings +from django.test import override_settings from ..exceptions import NewDocumentVersionNotAllowed from ..literals import STUB_EXPIRATION_INTERVAL diff --git a/mayan/apps/documents/tests/test_views.py b/mayan/apps/documents/tests/test_views.py index 4645de3501..19fb13b19c 100644 --- a/mayan/apps/documents/tests/test_views.py +++ b/mayan/apps/documents/tests/test_views.py @@ -380,7 +380,6 @@ class DocumentsViewsTestCase(GenericDocumentViewTestCase): self.assertContains(response, text='queued', status_code=200) self.assertEqual(self.document.pages.count(), page_count) - def test_document_multiple_update_page_count_view_no_permission(self): self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD) diff --git a/mayan/apps/documents/urls.py b/mayan/apps/documents/urls.py index c7dfd59cfd..f1714dd1d0 100644 --- a/mayan/apps/documents/urls.py +++ b/mayan/apps/documents/urls.py @@ -281,7 +281,8 @@ api_urls = patterns( ), url( r'^document_version/(?P[0-9]+)/download/$', - APIDocumentVersionDownloadView.as_view(), name='documentversion-download' + APIDocumentVersionDownloadView.as_view(), + name='documentversion-download' ), url( r'^document_page/(?P[0-9]+)/$', APIDocumentPageView.as_view(), From 10e106ba83728463a16e2d42fb7e3356d8a59211 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 7 Feb 2017 22:06:03 -0400 Subject: [PATCH 02/32] Improve tag serializer adding bulk document tagging on creation and editing. Improve and add additiona tag API tests. --- mayan/apps/tags/api_views.py | 67 +++++++++++++++------------- mayan/apps/tags/serializers.py | 48 ++++++++++++++++++-- mayan/apps/tags/tests/test_api.py | 73 +++++++++++++++++++++---------- 3 files changed, 131 insertions(+), 57 deletions(-) diff --git a/mayan/apps/tags/api_views.py b/mayan/apps/tags/api_views.py index f24e267ef3..87c37c49e1 100644 --- a/mayan/apps/tags/api_views.py +++ b/mayan/apps/tags/api_views.py @@ -21,11 +21,39 @@ from .permissions import ( permission_tag_remove, permission_tag_view ) from .serializers import ( - DocumentTagSerializer, NewDocumentTagSerializer, NewTagSerializer, - TagSerializer + DocumentTagSerializer, NewDocumentTagSerializer, TagSerializer, + WritableTagSerializer ) +class APITagListView(generics.ListCreateAPIView): + filter_backends = (MayanObjectPermissionsFilter,) + mayan_object_permissions = {'GET': (permission_tag_view,)} + mayan_view_permissions = {'POST': (permission_tag_create,)} + permission_classes = (MayanPermission,) + queryset = Tag.objects.all() + + def get_serializer_class(self): + if self.request.method == 'GET': + return TagSerializer + elif self.request.method == 'POST': + return WritableTagSerializer + + def get(self, *args, **kwargs): + """ + Returns a list of all the tags. + """ + + return super(APITagListView, self).get(*args, **kwargs) + + def post(self, *args, **kwargs): + """ + Create a new tag. + """ + + return super(APITagListView, self).post(*args, **kwargs) + + class APITagView(generics.RetrieveUpdateDestroyAPIView): filter_backends = (MayanObjectPermissionsFilter,) mayan_object_permissions = { @@ -35,7 +63,6 @@ class APITagView(generics.RetrieveUpdateDestroyAPIView): 'PUT': (permission_tag_edit,) } queryset = Tag.objects.all() - serializer_class = TagSerializer def delete(self, *args, **kwargs): """ @@ -51,6 +78,12 @@ class APITagView(generics.RetrieveUpdateDestroyAPIView): return super(APITagView, self).get(*args, **kwargs) + def get_serializer_class(self): + if self.request.method == 'GET': + return TagSerializer + else: + return WritableTagSerializer + def patch(self, *args, **kwargs): """ Edit the selected tag. @@ -66,34 +99,6 @@ class APITagView(generics.RetrieveUpdateDestroyAPIView): return super(APITagView, self).put(*args, **kwargs) -class APITagListView(generics.ListCreateAPIView): - filter_backends = (MayanObjectPermissionsFilter,) - mayan_object_permissions = {'GET': (permission_tag_view,)} - mayan_view_permissions = {'POST': (permission_tag_create,)} - permission_classes = (MayanPermission,) - queryset = Tag.objects.all() - - def get_serializer_class(self): - if self.request.method == 'GET': - return TagSerializer - elif self.request.method == 'POST': - return NewTagSerializer - - def get(self, *args, **kwargs): - """ - Returns a list of all the tags. - """ - - return super(APITagListView, self).get(*args, **kwargs) - - def post(self, *args, **kwargs): - """ - Create a new tag. - """ - - return super(APITagListView, self).post(*args, **kwargs) - - class APITagDocumentListView(generics.ListAPIView): """ Returns a list of all the documents tagged by a particular tag. diff --git a/mayan/apps/tags/serializers.py b/mayan/apps/tags/serializers.py index 3f49cf2e42..2d78f5e9bb 100644 --- a/mayan/apps/tags/serializers.py +++ b/mayan/apps/tags/serializers.py @@ -8,6 +8,7 @@ from rest_framework.exceptions import ValidationError from rest_framework.reverse import reverse from acls.models import AccessControlList +from documents.models import Document from permissions import Permission from .models import Tag @@ -15,7 +16,7 @@ from .permissions import permission_tag_attach class TagSerializer(serializers.HyperlinkedModelSerializer): - documents = serializers.HyperlinkedIdentityField( + documents_url = serializers.HyperlinkedIdentityField( view_name='rest_api:tag-document-list' ) documents_count = serializers.SerializerMethodField() @@ -25,7 +26,7 @@ class TagSerializer(serializers.HyperlinkedModelSerializer): 'url': {'view_name': 'rest_api:tag-detail'}, } fields = ( - 'color', 'documents', 'documents_count', 'id', 'label', 'url' + 'color', 'documents_count', 'documents_url', 'id', 'label', 'url' ) model = Tag @@ -33,13 +34,52 @@ class TagSerializer(serializers.HyperlinkedModelSerializer): return instance.documents.count() -class NewTagSerializer(serializers.ModelSerializer): +class WritableTagSerializer(serializers.ModelSerializer): + documents_pk_list = serializers.CharField( + help_text=_( + 'Comma separated list of document primary keys to which this tag ' + 'will be attached.' + ), required=False + ) + class Meta: fields = ( - 'color', 'label', 'id' + 'color', 'documents_pk_list', 'id', 'label', ) model = Tag + def _add_documents(self, documents_pk_list, instance): + instance.documents.add( + *Document.objects.filter(pk__in=documents_pk_list.split(',')) + ) + + def create(self, validated_data): + documents_pk_list = validated_data.pop('documents_pk_list', '') + + instance = super(WritableTagSerializer, self).create(validated_data) + + if documents_pk_list: + self._add_documents( + documents_pk_list=documents_pk_list, instance=instance + ) + + return instance + + def update(self, instance, validated_data): + documents_pk_list = validated_data.pop('documents_pk_list', '') + + instance = super(WritableTagSerializer, self).update( + instance, validated_data + ) + + if documents_pk_list: + instance.documents.clear() + self._add_documents( + documents_pk_list=documents_pk_list, instance=instance + ) + + return instance + class NewDocumentTagSerializer(serializers.Serializer): tag = serializers.IntegerField( diff --git a/mayan/apps/tags/tests/test_api.py b/mayan/apps/tags/tests/test_api.py index d714a5a14f..072ab962f4 100644 --- a/mayan/apps/tags/tests/test_api.py +++ b/mayan/apps/tags/tests/test_api.py @@ -20,6 +20,7 @@ from .literals import ( ) +@override_settings(OCR_AUTO_OCR=False) class TagAPITestCase(APITestCase): """ Test the tag API endpoints @@ -37,6 +38,20 @@ class TagAPITestCase(APITestCase): def tearDown(self): self.admin_user.delete() + if hasattr(self, 'document_type'): + self.document_type.delete() + + def _document_create(self): + self.document_type = DocumentType.objects.create( + label=TEST_DOCUMENT_TYPE + ) + + with open(TEST_SMALL_DOCUMENT_PATH) as file_object: + document = self.document_type.new_document( + file_object=file_object, + ) + + return document def test_tag_create(self): response = self.client.post( @@ -46,7 +61,22 @@ class TagAPITestCase(APITestCase): ) tag = Tag.objects.first() + self.assertEqual(response.data['id'], tag.pk) + self.assertEqual(response.data['label'], TEST_TAG_LABEL) + self.assertEqual(response.data['color'], TEST_TAG_COLOR) + self.assertEqual(Tag.objects.count(), 1) + self.assertEqual(tag.label, TEST_TAG_LABEL) + self.assertEqual(tag.color, TEST_TAG_COLOR) + + def test_tag_create_with_documents(self): + response = self.client.post( + reverse('rest_api:tag-list'), { + 'label': TEST_TAG_LABEL, 'color': TEST_TAG_COLOR + } + ) + + tag = Tag.objects.first() self.assertEqual(response.data['id'], tag.pk) self.assertEqual(response.data['label'], TEST_TAG_LABEL) self.assertEqual(response.data['color'], TEST_TAG_COLOR) @@ -62,7 +92,23 @@ class TagAPITestCase(APITestCase): self.assertEqual(Tag.objects.count(), 0) - def test_tag_edit(self): + def test_tag_edit_via_patch(self): + tag = Tag.objects.create(color=TEST_TAG_COLOR, label=TEST_TAG_LABEL) + + self.client.patch( + reverse('rest_api:tag-detail', args=(tag.pk,)), + { + 'label': TEST_TAG_LABEL_EDITED, + 'color': TEST_TAG_COLOR_EDITED + } + ) + + tag = Tag.objects.first() + + self.assertEqual(tag.label, TEST_TAG_LABEL_EDITED) + self.assertEqual(tag.color, TEST_TAG_COLOR_EDITED) + + def test_tag_edit_via_put(self): tag = Tag.objects.create(color=TEST_TAG_COLOR, label=TEST_TAG_LABEL) self.client.put( @@ -78,18 +124,10 @@ class TagAPITestCase(APITestCase): self.assertEqual(tag.label, TEST_TAG_LABEL_EDITED) self.assertEqual(tag.color, TEST_TAG_COLOR_EDITED) - @override_settings(OCR_AUTO_OCR=False) - def test_tag_add_document(self): + def test_tag_document_add(self): tag = Tag.objects.create(color=TEST_TAG_COLOR, label=TEST_TAG_LABEL) - document_type = DocumentType.objects.create( - label=TEST_DOCUMENT_TYPE - ) - - with open(TEST_SMALL_DOCUMENT_PATH) as file_object: - document = document_type.new_document( - file_object=file_object, - ) + document = self._document_create() self.client.post( reverse('rest_api:document-tag-list', args=(document.pk,)), @@ -98,19 +136,10 @@ class TagAPITestCase(APITestCase): self.assertEqual(tag.documents.count(), 1) - @override_settings(OCR_AUTO_OCR=False) - def test_tag_remove_document(self): + def test_tag_document_remove(self): tag = Tag.objects.create(color=TEST_TAG_COLOR, label=TEST_TAG_LABEL) - document_type = DocumentType.objects.create( - label=TEST_DOCUMENT_TYPE - ) - - with open(TEST_SMALL_DOCUMENT_PATH) as file_object: - document = document_type.new_document( - file_object=file_object, - ) - + document = self._document_create() tag.documents.add(document) self.client.delete( From 84e8330d5b9bb35c9c2e54a674c41aa1c3d83044 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 7 Feb 2017 22:07:01 -0400 Subject: [PATCH 03/32] Fix markup typo --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 67acd269bb..426c881519 100644 --- a/README.rst +++ b/README.rst @@ -60,6 +60,6 @@ account. :target: https://codecov.io/gitlab/mayan-edms/mayan-edms?branch=master .. |Documentation| image:: https://readthedocs.org/projects/mayan/badge/?version=latest :target: http://mayan.readthedocs.io/en/latest -.. |Python version| images:: https://img.shields.io/pypi/pyversions/mayan-edms.svg +.. |Python version| image:: https://img.shields.io/pypi/pyversions/mayan-edms.svg |Analytics| From 66fb3a4530e533934c6c8b9ffa298bbfc8c6e43f Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Tue, 7 Feb 2017 23:53:35 -0400 Subject: [PATCH 04/32] Keep the django-mptt version mayan-cabinets may have installed. --- requirements/base.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index c87051054e..b563633ab0 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -15,7 +15,7 @@ django-filetransfers==0.1.0 django-formtools==1.0 django-pure-pagination==0.3.0 django-model-utils==2.4 -django-mptt==0.8.0 +django-mptt>=0.8.0 django-qsstats-magic==0.7.2 django-rest-swagger==0.3.4 django-stronghold==0.2.7 diff --git a/setup.py b/setup.py index e2ede2177b..29aff914e9 100644 --- a/setup.py +++ b/setup.py @@ -72,7 +72,7 @@ django-filetransfers==0.1.0 django-formtools==1.0 django-pure-pagination==0.3.0 django-model-utils==2.4 -django-mptt==0.8.0 +django-mptt>=0.8.0 django-qsstats-magic==0.7.2 django-rest-swagger==0.3.4 django-stronghold==0.2.7 From f885d886bda9ba0501b3bedfac116d23ee0dc0b1 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 8 Feb 2017 00:53:33 -0400 Subject: [PATCH 05/32] Improve the document tag serializer. Add document tag detail view. Add more API tests. Tweak URLs to conform with API design best practices. --- mayan/apps/tags/api_views.py | 34 +++++++++++++--------- mayan/apps/tags/serializers.py | 42 +++++++++++++++------------ mayan/apps/tags/tests/test_api.py | 48 ++++++++++++++++++++++++++----- mayan/apps/tags/urls.py | 8 +++--- 4 files changed, 89 insertions(+), 43 deletions(-) diff --git a/mayan/apps/tags/api_views.py b/mayan/apps/tags/api_views.py index 87c37c49e1..41555a758c 100644 --- a/mayan/apps/tags/api_views.py +++ b/mayan/apps/tags/api_views.py @@ -17,8 +17,8 @@ from rest_api.permissions import MayanPermission from .models import Tag from .permissions import ( - permission_tag_create, permission_tag_delete, permission_tag_edit, - permission_tag_remove, permission_tag_view + permission_tag_attach, permission_tag_create, permission_tag_delete, + permission_tag_edit, permission_tag_remove, permission_tag_view ) from .serializers import ( DocumentTagSerializer, NewDocumentTagSerializer, TagSerializer, @@ -123,15 +123,21 @@ class APITagDocumentListView(generics.ListAPIView): class APIDocumentTagListView(generics.ListCreateAPIView): - """ - Returns a list of all the tags attached to a document. - """ - filter_backends = (MayanObjectPermissionsFilter,) - mayan_object_permissions = {'GET': (permission_tag_view,)} + mayan_object_permissions = { + 'GET': (permission_tag_view,), + 'POST': (permission_tag_attach,) + } + + def get(self, *args, **kwargs): + """ + Returns a list of all the tags attached to a document. + """ + + return super(APIDocumentTagListView, self).get(*args, **kwargs) def get_document(self): - return get_object_or_404(Document, pk=self.kwargs['pk']) + return get_object_or_404(Document, pk=self.kwargs['document_pk']) def get_queryset(self): document = self.get_document() @@ -146,6 +152,12 @@ class APIDocumentTagListView(generics.ListCreateAPIView): return document.attached_tags().all() + def get_serializer_class(self): + if self.request.method == 'GET': + return DocumentTagSerializer + elif self.request.method == 'POST': + return NewDocumentTagSerializer + def get_serializer_context(self): """ Extra context provided to the serializer class. @@ -157,12 +169,6 @@ class APIDocumentTagListView(generics.ListCreateAPIView): 'view': self } - def get_serializer_class(self): - if self.request.method == 'GET': - return DocumentTagSerializer - elif self.request.method == 'POST': - return NewDocumentTagSerializer - def perform_create(self, serializer): serializer.save(document=self.get_document()) diff --git a/mayan/apps/tags/serializers.py b/mayan/apps/tags/serializers.py index 2d78f5e9bb..da9265ff3e 100644 --- a/mayan/apps/tags/serializers.py +++ b/mayan/apps/tags/serializers.py @@ -81,14 +81,35 @@ class WritableTagSerializer(serializers.ModelSerializer): return instance +class DocumentTagSerializer(TagSerializer): + document_tag_url = serializers.SerializerMethodField( + help_text=_( + 'API URL pointing to a tag in relation to the document ' + 'attached to it. This URL is different than the canonical ' + 'tag URL.' + ) + ) + + class Meta(TagSerializer.Meta): + fields = TagSerializer.Meta.fields + ('document_tag_url',) + read_only_fields = TagSerializer.Meta.fields + + def get_document_tag_url(self, instance): + return reverse( + 'rest_api:document-tag-detail', args=( + self.context['document'].pk, instance.pk + ), request=self.context['request'], format=self.context['format'] + ) + + class NewDocumentTagSerializer(serializers.Serializer): - tag = serializers.IntegerField( + tag_pk = serializers.IntegerField( help_text=_('Primary key of the tag to be added.') ) def create(self, validated_data): try: - tag = Tag.objects.get(pk=validated_data['tag']) + tag = Tag.objects.get(pk=validated_data['tag_pk']) try: Permission.check_permissions( @@ -103,19 +124,4 @@ class NewDocumentTagSerializer(serializers.Serializer): except Exception as exception: raise ValidationError(exception) - return {'tag': tag.pk} - - -class DocumentTagSerializer(TagSerializer): - remove = serializers.SerializerMethodField() - - def get_remove(self, instance): - return reverse( - 'rest_api:document-tag', args=( - self.context['document'].pk, instance.pk, - ), request=self.context['request'], format=self.context['format'] - ) - - class Meta(TagSerializer.Meta): - fields = TagSerializer.Meta.fields + ('remove',) - read_only_fields = TagSerializer.Meta.fields + return {'tag_pk': tag.pk} diff --git a/mayan/apps/tags/tests/test_api.py b/mayan/apps/tags/tests/test_api.py index 072ab962f4..1beb3441b7 100644 --- a/mayan/apps/tags/tests/test_api.py +++ b/mayan/apps/tags/tests/test_api.py @@ -3,6 +3,7 @@ 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 django.utils.encoding import force_text from rest_framework.test import APITestCase @@ -92,6 +93,19 @@ class TagAPITestCase(APITestCase): self.assertEqual(Tag.objects.count(), 0) + def test_tag_document_list_view(self): + tag = Tag.objects.create(color=TEST_TAG_COLOR, label=TEST_TAG_LABEL) + document = self._document_create() + tag.documents.add(document) + + response = self.client.get( + reverse('rest_api:tag-document-list', args=(tag.pk,)) + ) + + self.assertEqual( + response.data['results'][0]['uuid'], force_text(document.uuid) + ) + def test_tag_edit_via_patch(self): tag = Tag.objects.create(color=TEST_TAG_COLOR, label=TEST_TAG_LABEL) @@ -124,26 +138,46 @@ class TagAPITestCase(APITestCase): self.assertEqual(tag.label, TEST_TAG_LABEL_EDITED) self.assertEqual(tag.color, TEST_TAG_COLOR_EDITED) - def test_tag_document_add(self): + def test_document_attach_tag_view(self): tag = Tag.objects.create(color=TEST_TAG_COLOR, label=TEST_TAG_LABEL) - document = self._document_create() - self.client.post( + response = self.client.post( reverse('rest_api:document-tag-list', args=(document.pk,)), - {'tag': tag.pk} + {'tag_pk': tag.pk} + ) + self.assertQuerysetEqual(document.tags.all(), (repr(tag),)) + + def test_document_tag_detail_view(self): + tag = Tag.objects.create(color=TEST_TAG_COLOR, label=TEST_TAG_LABEL) + document = self._document_create() + tag.documents.add(document) + + response = self.client.get( + reverse('rest_api:document-tag-detail', args=(document.pk, tag.pk)) ) - self.assertEqual(tag.documents.count(), 1) + self.assertEqual(response.data['label'], tag.label) - def test_tag_document_remove(self): + def test_document_tag_list_view(self): tag = Tag.objects.create(color=TEST_TAG_COLOR, label=TEST_TAG_LABEL) + document = self._document_create() + tag.documents.add(document) + response = self.client.get( + reverse('rest_api:document-tag-list', args=(document.pk,)) + ) + self.assertEqual(response.data['results'][0]['label'], tag.label) + + def test_document_tag_remove_view(self): + tag = Tag.objects.create(color=TEST_TAG_COLOR, label=TEST_TAG_LABEL) document = self._document_create() tag.documents.add(document) self.client.delete( - reverse('rest_api:document-tag', args=(document.pk, tag.pk)), + reverse( + 'rest_api:document-tag-detail', args=(document.pk, tag.pk) + ), ) self.assertEqual(tag.documents.count(), 0) diff --git a/mayan/apps/tags/urls.py b/mayan/apps/tags/urls.py index 0ee9d5f19f..83223389e0 100644 --- a/mayan/apps/tags/urls.py +++ b/mayan/apps/tags/urls.py @@ -61,11 +61,11 @@ api_urls = patterns( url(r'^tags/(?P[0-9]+)/$', APITagView.as_view(), name='tag-detail'), url(r'^tags/$', APITagListView.as_view(), name='tag-list'), url( - r'^document/(?P[0-9]+)/tags/$', APIDocumentTagListView.as_view(), - name='document-tag-list' + r'^documents/(?P[0-9]+)/tags/$', + APIDocumentTagListView.as_view(), name='document-tag-list' ), url( - r'^document/(?P[0-9]+)/tags/(?P[0-9]+)/$', - APIDocumentTagView.as_view(), name='document-tag' + r'^documents/(?P[0-9]+)/tags/(?P[0-9]+)/$', + APIDocumentTagView.as_view(), name='document-tag-detail' ), ) From 651e37019100099c9fa9255e52e102e7a15b0916 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 8 Feb 2017 01:28:30 -0400 Subject: [PATCH 06/32] Add key serializer, API endpoints and API tests to the django_gpg app. --- mayan/apps/django_gpg/api_views.py | 59 +++++++++++++++++++++++++ mayan/apps/django_gpg/apps.py | 2 + mayan/apps/django_gpg/serializers.py | 17 +++++++ mayan/apps/django_gpg/tests/test_api.py | 59 +++++++++++++++++++++++++ mayan/apps/django_gpg/urls.py | 9 ++++ 5 files changed, 146 insertions(+) create mode 100644 mayan/apps/django_gpg/api_views.py create mode 100644 mayan/apps/django_gpg/serializers.py create mode 100644 mayan/apps/django_gpg/tests/test_api.py diff --git a/mayan/apps/django_gpg/api_views.py b/mayan/apps/django_gpg/api_views.py new file mode 100644 index 0000000000..bb8762f0a0 --- /dev/null +++ b/mayan/apps/django_gpg/api_views.py @@ -0,0 +1,59 @@ +from __future__ import absolute_import, unicode_literals + +from rest_framework import generics + +from rest_api.filters import MayanObjectPermissionsFilter +from rest_api.permissions import MayanPermission + +from .models import Key +from .permissions import ( + permission_key_delete, permission_key_upload, permission_key_view +) +from .serializers import KeySerializer + + +class APIKeyListView(generics.ListCreateAPIView): + filter_backends = (MayanObjectPermissionsFilter,) + mayan_object_permissions = { + 'GET': (permission_key_view,), + 'POST': (permission_key_upload,) + } + permission_classes = (MayanPermission,) + queryset = Key.objects.all() + serializer_class = KeySerializer + + def get(self, *args, **kwargs): + """ + Returns a list of all the keys. + """ + return super(APIKeyListView, self).get(*args, **kwargs) + + def post(self, *args, **kwargs): + """ + Upload a new key. + """ + return super(APIKeyListView, self).post(*args, **kwargs) + + +class APIKeyView(generics.RetrieveDestroyAPIView): + filter_backends = (MayanObjectPermissionsFilter,) + mayan_object_permissions = { + 'DELETE': (permission_key_delete,), + 'GET': (permission_key_view,), + } + queryset = Key.objects.all() + serializer_class = KeySerializer + + def delete(self, *args, **kwargs): + """ + Delete the selected key. + """ + + return super(APIKeyView, self).delete(*args, **kwargs) + + def get(self, *args, **kwargs): + """ + Return the details of the selected key. + """ + + return super(APIKeyView, self).get(*args, **kwargs) diff --git a/mayan/apps/django_gpg/apps.py b/mayan/apps/django_gpg/apps.py index b3abb60d18..9a3c8340ab 100644 --- a/mayan/apps/django_gpg/apps.py +++ b/mayan/apps/django_gpg/apps.py @@ -10,6 +10,7 @@ from common import ( ) from common.classes import Package from navigation import SourceColumn +from rest_api.classes import APIEndPoint from .classes import KeyStub from .links import ( @@ -32,6 +33,7 @@ class DjangoGPGApp(MayanAppConfig): def ready(self): super(DjangoGPGApp, self).ready() + APIEndPoint(app=self, version_string='1') Key = self.get_model('Key') ModelPermission.register( diff --git a/mayan/apps/django_gpg/serializers.py b/mayan/apps/django_gpg/serializers.py new file mode 100644 index 0000000000..df3c965690 --- /dev/null +++ b/mayan/apps/django_gpg/serializers.py @@ -0,0 +1,17 @@ +from __future__ import unicode_literals + +from rest_framework import serializers + +from .models import Key + + +class KeySerializer(serializers.ModelSerializer): + class Meta: + extra_kwargs = { + 'url': {'view_name': 'rest_api:key-detail'}, + } + fields = ( + 'algorithm', 'creation_date', 'expiration_date', 'fingerprint', + 'id', 'key_data', 'key_type', 'length', 'url', 'user_id' + ) + model = Key diff --git a/mayan/apps/django_gpg/tests/test_api.py b/mayan/apps/django_gpg/tests/test_api.py new file mode 100644 index 0000000000..927fed2ff5 --- /dev/null +++ b/mayan/apps/django_gpg/tests/test_api.py @@ -0,0 +1,59 @@ +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 user_management.tests.literals import ( + TEST_ADMIN_EMAIL, TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME +) + +from ..models import Key + +from .literals import TEST_KEY_DATA, TEST_KEY_FINGERPRINT + + +@override_settings(OCR_AUTO_OCR=False) +class KeyAPITestCase(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 + ) + + def _create_key(self): + return Key.objects.create(key_data=TEST_KEY_DATA) + + def test_key_create_view(self): + response = self.client.post( + reverse('rest_api:key-list'), { + 'key_data': TEST_KEY_DATA + } + ) + self.assertEqual(response.data['fingerprint'], TEST_KEY_FINGERPRINT) + + key = Key.objects.first() + self.assertEqual(Key.objects.count(), 1) + self.assertEqual(key.fingerprint, TEST_KEY_FINGERPRINT) + + def test_key_delete_view(self): + key = self._create_key() + + self.client.delete(reverse('rest_api:key-detail', args=(key.pk,))) + + self.assertEqual(Key.objects.count(), 0) + + def test_key_detail_view(self): + key = self._create_key() + + response = self.client.get( + reverse('rest_api:key-detail', args=(key.pk,)) + ) + + self.assertEqual(response.data['fingerprint'], key.fingerprint) diff --git a/mayan/apps/django_gpg/urls.py b/mayan/apps/django_gpg/urls.py index 929e613e71..39068f3039 100644 --- a/mayan/apps/django_gpg/urls.py +++ b/mayan/apps/django_gpg/urls.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from django.conf.urls import patterns, url +from .api_views import APIKeyListView, APIKeyView from .views import ( KeyDeleteView, KeyDetailView, KeyDownloadView, KeyQueryView, KeyQueryResultView, KeyReceive, KeyUploadView, PrivateKeyListView, @@ -39,3 +40,11 @@ urlpatterns = patterns( r'^receive/(?P.+)/$', KeyReceive.as_view(), name='key_receive' ), ) + +api_urls = [ + url( + r'^keys/(?P[0-9]+)/$', APIKeyView.as_view(), + name='key-detail' + ), + url(r'^keys/$', APIKeyListView.as_view(), name='key-list'), +] From 146459d5bca355926e47c21ba9c095dd6055977b Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 8 Feb 2017 17:03:36 -0400 Subject: [PATCH 07/32] Inital work on the document states API --- mayan/apps/document_states/api_views.py | 254 +++++++++++++++++++ mayan/apps/document_states/apps.py | 3 + mayan/apps/document_states/serializers.py | 123 +++++++++ mayan/apps/document_states/tests/literals.py | 3 +- mayan/apps/document_states/tests/test_api.py | 123 +++++++++ mayan/apps/document_states/urls.py | 22 ++ 6 files changed, 527 insertions(+), 1 deletion(-) create mode 100644 mayan/apps/document_states/api_views.py create mode 100644 mayan/apps/document_states/serializers.py create mode 100644 mayan/apps/document_states/tests/test_api.py 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' + ), +] From 5de3a607257b8fcafff3fbeaf009e25e7d7c6f05 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 8 Feb 2017 17:03:57 -0400 Subject: [PATCH 08/32] Rename the document type serializer link to its documents to documents_url according to the new guidelines. --- mayan/apps/documents/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mayan/apps/documents/serializers.py b/mayan/apps/documents/serializers.py index a6858e1de1..4a4a729e46 100644 --- a/mayan/apps/documents/serializers.py +++ b/mayan/apps/documents/serializers.py @@ -46,7 +46,7 @@ class DocumentPageSerializer(serializers.HyperlinkedModelSerializer): class DocumentTypeSerializer(serializers.HyperlinkedModelSerializer): - documents = serializers.HyperlinkedIdentityField( + documents_url = serializers.HyperlinkedIdentityField( view_name='rest_api:documenttype-document-list', ) documents_count = serializers.SerializerMethodField() @@ -59,7 +59,7 @@ class DocumentTypeSerializer(serializers.HyperlinkedModelSerializer): 'url': {'view_name': 'rest_api:documenttype-detail'}, } fields = ( - 'delete_time_period', 'delete_time_unit', 'documents', + 'delete_time_period', 'delete_time_unit', 'documents_url', 'documents_count', 'id', 'label', 'trash_time_period', 'trash_time_unit', 'url' ) From 3b7a241c0276979e0ce59406c0da06a0bcde7597 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 8 Feb 2017 17:04:40 -0400 Subject: [PATCH 09/32] Tag API tests cleanups. --- mayan/apps/tags/tests/test_api.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mayan/apps/tags/tests/test_api.py b/mayan/apps/tags/tests/test_api.py index 1beb3441b7..06393e6bcb 100644 --- a/mayan/apps/tags/tests/test_api.py +++ b/mayan/apps/tags/tests/test_api.py @@ -38,7 +38,6 @@ class TagAPITestCase(APITestCase): ) def tearDown(self): - self.admin_user.delete() if hasattr(self, 'document_type'): self.document_type.delete() @@ -49,8 +48,8 @@ class TagAPITestCase(APITestCase): with open(TEST_SMALL_DOCUMENT_PATH) as file_object: document = self.document_type.new_document( - file_object=file_object, - ) + file_object=file_object, + ) return document @@ -142,7 +141,7 @@ class TagAPITestCase(APITestCase): tag = Tag.objects.create(color=TEST_TAG_COLOR, label=TEST_TAG_LABEL) document = self._document_create() - response = self.client.post( + self.client.post( reverse('rest_api:document-tag-list', args=(document.pk,)), {'tag_pk': tag.pk} ) From e4da3eb7866d659f2b8f219588d59bcc8f8ff378 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Wed, 8 Feb 2017 20:46:34 -0400 Subject: [PATCH 10/32] Finish base document states API views. --- mayan/apps/document_states/api_views.py | 6 +-- mayan/apps/document_states/tests/test_api.py | 56 +++++++++++++++++++- mayan/apps/document_states/urls.py | 2 +- 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/mayan/apps/document_states/api_views.py b/mayan/apps/document_states/api_views.py index 1f61c1980a..c838a82849 100644 --- a/mayan/apps/document_states/api_views.py +++ b/mayan/apps/document_states/api_views.py @@ -108,7 +108,7 @@ class APIWorkflowDocumentTypeList(generics.ListCreateAPIView): class APIWorkflowDocumentTypeView(generics.RetrieveDestroyAPIView): filter_backends = (MayanObjectPermissionsFilter,) - lookup_url_kwarg = 'document_pk' + lookup_url_kwarg = 'document_type_pk' mayan_object_permissions = { 'GET': (permission_document_type_view,), } @@ -177,8 +177,8 @@ class APIWorkflowDocumentTypeView(generics.RetrieveDestroyAPIView): 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) + + self.get_workflow().document_types.remove(instance) class APIWorkflowListView(generics.ListCreateAPIView): diff --git a/mayan/apps/document_states/tests/test_api.py b/mayan/apps/document_states/tests/test_api.py index 3fd55a3f32..bf2ea54d82 100644 --- a/mayan/apps/document_states/tests/test_api.py +++ b/mayan/apps/document_states/tests/test_api.py @@ -3,6 +3,7 @@ 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 django.utils.encoding import force_text from rest_framework.test import APITestCase @@ -92,6 +93,60 @@ class WorkflowAPITestCase(APITestCase): self.assertEqual(response.data['label'], workflow.label) + def test_workflow_document_type_create_view(self): + workflow = self._create_workflow() + + response = self.client.post( + reverse( + 'rest_api:workflow-document-type-list', + args=(workflow.pk,) + ), data={'document_type_pk': self.document_type.pk} + ) + + self.assertQuerysetEqual( + workflow.document_types.all(), (repr(self.document_type),) + ) + + def test_workflow_document_type_delete_view(self): + workflow = self._create_workflow() + workflow.document_types.add(self.document_type) + + response = self.client.delete( + reverse( + 'rest_api:workflow-document-type-detail', + args=(workflow.pk, self.document_type.pk) + ) + ) + + workflow.refresh_from_db() + self.assertQuerysetEqual(workflow.document_types.all(), ()) + + def test_workflow_document_type_detail_view(self): + workflow = self._create_workflow() + workflow.document_types.add(self.document_type) + + response = self.client.get( + reverse( + 'rest_api:workflow-document-type-detail', + args=(workflow.pk, self.document_type.pk) + ) + ) + + self.assertEqual(response.data['label'], self.document_type.label) + + def test_workflow_document_type_list_view(self): + workflow = self._create_workflow() + workflow.document_types.add(self.document_type) + + response = self.client.get( + reverse('rest_api:workflow-document-type-list', + args=(workflow.pk,)) + ) + + self.assertEqual( + response.data['results'][0]['label'], self.document_type.label + ) + def test_workflow_list_view(self): workflow = self._create_workflow() @@ -120,4 +175,3 @@ class WorkflowAPITestCase(APITestCase): 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 34b9bffd16..a11c453718 100644 --- a/mayan/apps/document_states/urls.py +++ b/mayan/apps/document_states/urls.py @@ -114,7 +114,7 @@ api_urls = [ name='workflow-document-type-list' ), url( - r'^workflows/(?P[0-9]+)/document_types/(?P[0-9]+)/$', + r'^workflows/(?P[0-9]+)/document_types/(?P[0-9]+)/$', APIWorkflowDocumentTypeView.as_view(), name='workflow-document-type-detail' ), From d12d2d986582537c8155776916dd0ca0ecddd092 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 9 Feb 2017 00:19:42 -0400 Subject: [PATCH 11/32] Initial commit to support workflow states API endpoints. --- mayan/apps/document_states/api_views.py | 38 ++++++++++++----- mayan/apps/document_states/serializers.py | 15 ++++++- mayan/apps/document_states/tests/test_api.py | 45 ++++++++++++++++++++ mayan/apps/document_states/urls.py | 12 +++++- 4 files changed, 96 insertions(+), 14 deletions(-) diff --git a/mayan/apps/document_states/api_views.py b/mayan/apps/document_states/api_views.py index c838a82849..aff3529f01 100644 --- a/mayan/apps/document_states/api_views.py +++ b/mayan/apps/document_states/api_views.py @@ -12,14 +12,14 @@ from permissions import Permission from rest_api.filters import MayanObjectPermissionsFilter from rest_api.permissions import MayanPermission -from .models import Workflow +from .models import Workflow, WorkflowState from .permissions import ( permission_workflow_create, permission_workflow_delete, permission_workflow_edit, permission_workflow_view ) from .serializers import ( NewWorkflowDocumentTypeSerializer, WorkflowDocumentTypeSerializer, - WorkflowSerializer, WritableWorkflowSerializer + WorkflowSerializer, WorkflowStateSerializer, WritableWorkflowSerializer ) @@ -87,15 +87,6 @@ class APIWorkflowDocumentTypeList(generics.ListCreateAPIView): 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. @@ -252,3 +243,28 @@ class APIWorkflowView(generics.RetrieveUpdateDestroyAPIView): """ return super(APIWorkflowView, self).put(*args, **kwargs) + + +## Workflow state views + + +class APIWorkflowStateListView(generics.ListCreateAPIView): + serializer_class = WorkflowStateSerializer + queryset = WorkflowState.objects.all() + + def get(self, *args, **kwargs): + """ + Returns a list of all the workflow states. + """ + return super(APIWorkflowStateListView, self).get(*args, **kwargs) + + def post(self, *args, **kwargs): + """ + Create a new workflow state. + """ + return super(APIWorkflowStateListView, self).post(*args, **kwargs) + + +class APIWorkflowStateView(generics.RetrieveAPIView): + queryset = WorkflowState.objects.all() + serializer_class = WorkflowStateSerializer diff --git a/mayan/apps/document_states/serializers.py b/mayan/apps/document_states/serializers.py index 33337b2819..322c37743e 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 +from .models import Workflow, WorkflowState class NewWorkflowDocumentTypeSerializer(serializers.Serializer): @@ -48,17 +48,28 @@ class WorkflowDocumentTypeSerializer(DocumentTypeSerializer): ) +class WorkflowStateSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + extra_kwargs = { + 'url': {'view_name': 'rest_api:workflowstate-detail'}, + 'workflow': {'view_name': 'rest_api:workflow-detail'}, + } + fields = ('completion', 'id', 'initial', 'label', 'workflow', 'url') + model = WorkflowState + + class WorkflowSerializer(serializers.HyperlinkedModelSerializer): document_types_url = serializers.HyperlinkedIdentityField( view_name='rest_api:workflow-document-type-list' ) + states = WorkflowStateSerializer(many=True, required=False) class Meta: extra_kwargs = { 'url': {'view_name': 'rest_api:workflow-detail'}, } fields = ( - 'document_types_url', 'get_initial_state', 'id', 'label', 'url' + 'document_types_url', 'id', 'label', 'states', 'url' ) model = Workflow diff --git a/mayan/apps/document_states/tests/test_api.py b/mayan/apps/document_states/tests/test_api.py index bf2ea54d82..d1276791e4 100644 --- a/mayan/apps/document_states/tests/test_api.py +++ b/mayan/apps/document_states/tests/test_api.py @@ -120,6 +120,11 @@ class WorkflowAPITestCase(APITestCase): workflow.refresh_from_db() self.assertQuerysetEqual(workflow.document_types.all(), ()) + # The workflow document type entry was deleted and not the document + # type itself. + self.assertQuerysetEqual( + DocumentType.objects.all(), (repr(self.document_type),) + ) def test_workflow_document_type_detail_view(self): workflow = self._create_workflow() @@ -175,3 +180,43 @@ class WorkflowAPITestCase(APITestCase): workflow.refresh_from_db() self.assertEqual(workflow.label, TEST_WORKFLOW_LABEL_EDITED) + + +@override_settings(OCR_AUTO_OCR=False) +class WorkflowStatesAPITestCase(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) diff --git a/mayan/apps/document_states/urls.py b/mayan/apps/document_states/urls.py index a11c453718..bfdcac7617 100644 --- a/mayan/apps/document_states/urls.py +++ b/mayan/apps/document_states/urls.py @@ -4,7 +4,8 @@ from django.conf.urls import patterns, url from .api_views import ( APIWorkflowDocumentTypeList, APIWorkflowDocumentTypeView, - APIWorkflowListView, APIWorkflowView + APIWorkflowListView, APIWorkflowStateListView, APIWorkflowStateView, + APIWorkflowView ) from .views import ( DocumentWorkflowInstanceListView, SetupWorkflowCreateView, @@ -103,6 +104,15 @@ urlpatterns = patterns( ) api_urls = [ + url( + r'^states/$', APIWorkflowStateListView.as_view(), + name='workflowstate-list' + ), + url( + r'^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(), From ab68723cf674a8d1e3109cdd1d0be387044af673 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 9 Feb 2017 04:04:58 -0400 Subject: [PATCH 12/32] Commit working workflow state serializer, API views and tests. --- mayan/apps/document_states/api_views.py | 108 ++++++++++++++++++- mayan/apps/document_states/serializers.py | 29 ++++- mayan/apps/document_states/tests/test_api.py | 72 +++++++++++-- mayan/apps/document_states/urls.py | 9 +- 4 files changed, 196 insertions(+), 22 deletions(-) diff --git a/mayan/apps/document_states/api_views.py b/mayan/apps/document_states/api_views.py index aff3529f01..3caf7e78bf 100644 --- a/mayan/apps/document_states/api_views.py +++ b/mayan/apps/document_states/api_views.py @@ -250,7 +250,6 @@ class APIWorkflowView(generics.RetrieveUpdateDestroyAPIView): class APIWorkflowStateListView(generics.ListCreateAPIView): serializer_class = WorkflowStateSerializer - queryset = WorkflowState.objects.all() def get(self, *args, **kwargs): """ @@ -258,6 +257,39 @@ class APIWorkflowStateListView(generics.ListCreateAPIView): """ return super(APIWorkflowStateListView, self).get(*args, **kwargs) + def get_queryset(self): + return self.get_workflow().states.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): + 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 state. @@ -265,6 +297,74 @@ class APIWorkflowStateListView(generics.ListCreateAPIView): return super(APIWorkflowStateListView, self).post(*args, **kwargs) -class APIWorkflowStateView(generics.RetrieveAPIView): - queryset = WorkflowState.objects.all() - serializer_class = WorkflowStateSerializer +class APIWorkflowStateView(generics.RetrieveUpdateDestroyAPIView): + lookup_url_kwarg = 'state_pk' + + def delete(self, *args, **kwargs): + """ + Delete the selected workflow state. + """ + + return super(APIWorkflowStateView, self).delete(*args, **kwargs) + + def get(self, *args, **kwargs): + """ + Return the details of the selected workflow state. + """ + + return super(APIWorkflowStateView, self).get(*args, **kwargs) + + 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. + """ + 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 state. + """ + + return super(APIWorkflowStateView, self).patch(*args, **kwargs) + + def put(self, *args, **kwargs): + """ + Edit the selected workflow state. + """ + + return super(APIWorkflowStateView, self).put(*args, **kwargs) + + diff --git a/mayan/apps/document_states/serializers.py b/mayan/apps/document_states/serializers.py index 322c37743e..947d9ced86 100644 --- a/mayan/apps/document_states/serializers.py +++ b/mayan/apps/document_states/serializers.py @@ -49,14 +49,33 @@ class WorkflowDocumentTypeSerializer(DocumentTypeSerializer): class WorkflowStateSerializer(serializers.HyperlinkedModelSerializer): + workflow_url = serializers.SerializerMethodField() + url = serializers.SerializerMethodField() + class Meta: - extra_kwargs = { - 'url': {'view_name': 'rest_api:workflowstate-detail'}, - 'workflow': {'view_name': 'rest_api:workflow-detail'}, - } - fields = ('completion', 'id', 'initial', 'label', 'workflow', 'url') + fields = ( + 'completion', 'id', 'initial', 'label', 'url', 'workflow_url', + ) model = WorkflowState + def create(self, validated_data): + validated_data['workflow'] = self.context['workflow'] + return super(WorkflowStateSerializer, self).create(validated_data) + + def get_url(self, instance): + return reverse( + 'rest_api:workflowstate-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 WorkflowSerializer(serializers.HyperlinkedModelSerializer): document_types_url = serializers.HyperlinkedIdentityField( diff --git a/mayan/apps/document_states/tests/test_api.py b/mayan/apps/document_states/tests/test_api.py index d1276791e4..7da2cecd1a 100644 --- a/mayan/apps/document_states/tests/test_api.py +++ b/mayan/apps/document_states/tests/test_api.py @@ -18,7 +18,10 @@ from user_management.tests.literals import ( from ..models import Workflow -from .literals import TEST_WORKFLOW_LABEL, TEST_WORKFLOW_LABEL_EDITED +from .literals import ( + TEST_WORKFLOW_LABEL, TEST_WORKFLOW_LABEL_EDITED, + TEST_WORKFLOW_STATE_COMPLETION, TEST_WORKFLOW_STATE_LABEL +) @override_settings(OCR_AUTO_OCR=False) @@ -208,15 +211,68 @@ class WorkflowStatesAPITestCase(APITestCase): self.document_type.delete() def _create_workflow(self): - return Workflow.objects.create(label=TEST_WORKFLOW_LABEL) + self.workflow = Workflow.objects.create(label=TEST_WORKFLOW_LABEL) + + def _create_workflow_state(self): + self._create_workflow() + self.workflow_state = self.workflow.states.create( + completion=TEST_WORKFLOW_STATE_COMPLETION, + label=TEST_WORKFLOW_STATE_LABEL + ) + + def test_workflow_state_create_view(self): + self._create_workflow() - def test_workflow_create_view(self): response = self.client.post( - reverse('rest_api:workflow-list'), { - 'label': TEST_WORKFLOW_LABEL + reverse( + 'rest_api:workflowstate-list', args=(self.workflow.pk,) + ), data={ + 'completion': TEST_WORKFLOW_STATE_COMPLETION, + 'label': TEST_WORKFLOW_STATE_LABEL } ) - workflow = Workflow.objects.first() - self.assertEqual(Workflow.objects.count(), 1) - self.assertEqual(response.data['id'], workflow.pk) + self.workflow.refresh_from_db() + + self.assertEqual( + self.workflow.states.first().label, TEST_WORKFLOW_STATE_LABEL + ) + + def test_workflow_state_delete_view(self): + self._create_workflow_state() + + response = self.client.delete( + reverse( + 'rest_api:workflowstate-detail', + args=(self.workflow.pk, self.workflow_state.pk) + ), + ) + + self.workflow.refresh_from_db() + + self.assertEqual(self.workflow.states.count(), 0) + + def test_workflow_state_detail_view(self): + self._create_workflow_state() + + response = self.client.get( + reverse( + 'rest_api:workflowstate-detail', + args=(self.workflow.pk, self.workflow_state.pk) + ), + ) + + self.assertEqual( + response.data['label'], TEST_WORKFLOW_STATE_LABEL + ) + + def test_workflow_state_list_view(self): + self._create_workflow_state() + + response = self.client.get( + reverse('rest_api:workflowstate-list', args=(self.workflow.pk,)), + ) + + self.assertEqual( + response.data['results'][0]['label'], TEST_WORKFLOW_STATE_LABEL + ) diff --git a/mayan/apps/document_states/urls.py b/mayan/apps/document_states/urls.py index bfdcac7617..5eced2ab0c 100644 --- a/mayan/apps/document_states/urls.py +++ b/mayan/apps/document_states/urls.py @@ -105,13 +105,12 @@ urlpatterns = patterns( api_urls = [ url( - r'^states/$', APIWorkflowStateListView.as_view(), - name='workflowstate-list' + r'^workflows/(?P[0-9]+)/states/$', + APIWorkflowStateListView.as_view(), name='workflowstate-list' ), url( - r'^states/(?P[0-9]+)/$', - APIWorkflowStateView.as_view(), - name='workflowstate-detail' + 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( From 0ff0841826d378e31e357e550de450be7596a5c1 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Thu, 9 Feb 2017 05:16:06 -0400 Subject: [PATCH 13/32] Add workflow transition API endpoints and tests. --- mayan/apps/document_states/api_views.py | 140 +++++++++++- mayan/apps/document_states/serializers.py | 95 +++++++- mayan/apps/document_states/tests/literals.py | 6 +- mayan/apps/document_states/tests/test_api.py | 229 ++++++++++++++++++- mayan/apps/document_states/urls.py | 25 +- 5 files changed, 462 insertions(+), 33 deletions(-) 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' + ), ] From 448400dc213068b012d644fd4afe4745da7f428d Mon Sep 17 00:00:00 2001 From: Jesaja Everling Date: Thu, 9 Feb 2017 01:21:41 +0200 Subject: [PATCH 14/32] Raise ValidationError when IntegrityError occurs for metadata_type_pk --- mayan/apps/metadata/serializers.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/mayan/apps/metadata/serializers.py b/mayan/apps/metadata/serializers.py index 12b70a50c7..36a1abdd27 100644 --- a/mayan/apps/metadata/serializers.py +++ b/mayan/apps/metadata/serializers.py @@ -1,8 +1,10 @@ from __future__ import unicode_literals +from django.db import IntegrityError from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers +from rest_framework.exceptions import ValidationError from .models import DocumentMetadata, MetadataType, DocumentTypeMetadataType @@ -26,7 +28,7 @@ class DocumentMetadataSerializer(serializers.ModelSerializer): class DocumentTypeMetadataTypeSerializer(serializers.ModelSerializer): class Meta: - fields = ('metadata_type', ) + fields = ('metadata_type',) model = DocumentTypeMetadataType @@ -52,10 +54,15 @@ class DocumentNewMetadataSerializer(serializers.Serializer): metadata_type = MetadataType.objects.get( pk=validated_data['metadata_type_pk'] ) - instance = self.document.metadata.create( - metadata_type=metadata_type, value=validated_data['value'] - ) - return instance + try: + instance = self.document.metadata.create( + metadata_type=metadata_type, value=validated_data['value'] + ) + return instance + except IntegrityError: + detail = 'Metadata type with pk {} is already defined for Document with pk {}'.format(metadata_type.pk, + self.document.pk) + raise ValidationError(detail) class DocumentTypeNewMetadataTypeSerializer(serializers.Serializer): From 280f0e74be8faebea78a09a62772b37b16c46d60 Mon Sep 17 00:00:00 2001 From: Jesaja Everling Date: Thu, 9 Feb 2017 20:13:14 +0200 Subject: [PATCH 15/32] Passing `self.request.data` to serializer not `self.request.POST` --- mayan/apps/metadata/api_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mayan/apps/metadata/api_views.py b/mayan/apps/metadata/api_views.py index 569a95277d..5bda3a6138 100644 --- a/mayan/apps/metadata/api_views.py +++ b/mayan/apps/metadata/api_views.py @@ -266,7 +266,7 @@ class APIDocumentTypeMetadataTypeOptionalListView(generics.ListCreateAPIView): document_type ) - serializer = self.get_serializer(data=self.request.POST) + serializer = self.get_serializer(data=self.request.data) if serializer.is_valid(): metadata_type = get_object_or_404( From 75b77d6059d778142135d91918b4d82a744599b8 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 10 Feb 2017 03:13:26 -0400 Subject: [PATCH 16/32] Add workflow instance API endpoints and tests. --- mayan/apps/document_states/api_views.py | 127 +++++++++++++++++- mayan/apps/document_states/models.py | 18 ++- mayan/apps/document_states/permissions.py | 3 +- mayan/apps/document_states/serializers.py | 115 +++++++++++++++- mayan/apps/document_states/tests/literals.py | 1 + mayan/apps/document_states/tests/test_api.py | 134 ++++++++++++++++++- mayan/apps/document_states/urls.py | 20 ++- 7 files changed, 405 insertions(+), 13 deletions(-) diff --git a/mayan/apps/document_states/api_views.py b/mayan/apps/document_states/api_views.py index a0b287560e..2c3735e159 100644 --- a/mayan/apps/document_states/api_views.py +++ b/mayan/apps/document_states/api_views.py @@ -6,6 +6,7 @@ from django.shortcuts import get_object_or_404 from rest_framework import generics from acls.models import AccessControlList +from documents.models import Document from documents.permissions import permission_document_type_view from permissions import Permission from rest_api.filters import MayanObjectPermissionsFilter @@ -14,12 +15,15 @@ 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 + permission_workflow_edit, permission_workflow_transition, + permission_workflow_view ) from .serializers import ( NewWorkflowDocumentTypeSerializer, WorkflowDocumentTypeSerializer, + WorkflowInstanceSerializer, WorkflowInstanceLogEntrySerializer, WorkflowSerializer, WorkflowStateSerializer, WorkflowTransitionSerializer, - WritableWorkflowSerializer, WritableWorkflowTransitionSerializer + WritableWorkflowInstanceLogEntrySerializer, WritableWorkflowSerializer, + WritableWorkflowTransitionSerializer ) @@ -488,3 +492,122 @@ class APIWorkflowTransitionView(generics.RetrieveUpdateDestroyAPIView): """ return super(APIWorkflowTransitionView, self).put(*args, **kwargs) + + +# Document workflow views + + +class APIWorkflowInstanceListView(generics.ListAPIView): + serializer_class = WorkflowInstanceSerializer + + def get(self, *args, **kwargs): + """ + Returns a list of all the document workflows. + """ + return super(APIWorkflowInstanceListView, self).get(*args, **kwargs) + + def get_document(self): + document = get_object_or_404(Document, pk=self.kwargs['pk']) + + try: + Permission.check_permissions( + self.request.user, (permission_workflow_view,) + ) + except PermissionDenied: + AccessControlList.objects.check_access( + permission_workflow_view, self.request.user, document + ) + + return document + + def get_queryset(self): + return self.get_document().workflows.all() + + +class APIWorkflowInstanceView(generics.RetrieveAPIView): + lookup_url_kwarg = 'workflow_pk' + serializer_class = WorkflowInstanceSerializer + + def get(self, *args, **kwargs): + """ + Return the details of the selected document workflow. + """ + + return super(APIWorkflowInstanceView, self).get(*args, **kwargs) + + def get_document(self): + document = get_object_or_404(Document, pk=self.kwargs['pk']) + + try: + Permission.check_permissions( + self.request.user, (permission_workflow_view,) + ) + except PermissionDenied: + AccessControlList.objects.check_access( + permission_workflow_view, self.request.user, document + ) + + return document + + def get_queryset(self): + return self.get_document().workflows.all() + + +class APIWorkflowInstanceLogEntryListView(generics.ListCreateAPIView): + def get(self, *args, **kwargs): + """ + Returns a list of all the document workflows log entries. + """ + return super(APIWorkflowInstanceLogEntryListView, self).get( + *args, **kwargs + ) + + def get_document(self): + if self.request.method == 'GET': + permission_required = permission_workflow_view + else: + permission_required = permission_workflow_transition + + document = get_object_or_404(Document, pk=self.kwargs['pk']) + + try: + Permission.check_permissions( + self.request.user, (permission_required,) + ) + except PermissionDenied: + AccessControlList.objects.check_access( + permission_required, self.request.user, document + ) + + return document + + def get_serializer_class(self): + if self.request.method == 'GET': + return WorkflowInstanceLogEntrySerializer + else: + return WritableWorkflowInstanceLogEntrySerializer + + def get_serializer_context(self): + return { + 'format': self.format_kwarg, + 'request': self.request, + 'workflow_instance': self.get_workflow_instance(), + 'view': self + } + + def get_queryset(self): + return self.get_workflow_instance().log_entries.all() + + def get_workflow_instance(self): + workflow = get_object_or_404( + self.get_document().workflows, pk=self.kwargs['workflow_pk'] + ) + + return workflow + + def post(self, *args, **kwargs): + """ + Transition a document workflow by creating a new document workflow + log entry. + """ + return super(APIWorkflowInstanceLogEntryListView, self).post(*args, **kwargs) diff --git a/mayan/apps/document_states/models.py b/mayan/apps/document_states/models.py index 09fc7cf345..203b29574f 100644 --- a/mayan/apps/document_states/models.py +++ b/mayan/apps/document_states/models.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals import logging from django.conf import settings +from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse from django.db import IntegrityError, models from django.utils.encoding import python_2_unicode_compatible @@ -166,7 +167,18 @@ class WorkflowInstance(models.Model): return None def get_transition_choices(self): - return self.get_current_state().origin_transitions.all() + current_state = self.get_current_state() + + if current_state: + return current_state.origin_transitions.all() + else: + """ + This happens when a workflow has no initial state and a document + whose document type has this workflow is created. We return an + empty transition queryset. + """ + + return WorkflowTransition.objects.none() class Meta: unique_together = ('document', 'workflow') @@ -195,3 +207,7 @@ class WorkflowInstanceLogEntry(models.Model): class Meta: verbose_name = _('Workflow instance log entry') verbose_name_plural = _('Workflow instance log entries') + + def clean(self): + if self.transition not in self.workflow_instance.get_transition_choices(): + raise ValidationError(_('Not a valid transition choice.')) diff --git a/mayan/apps/document_states/permissions.py b/mayan/apps/document_states/permissions.py index c992864d20..54ac90f282 100644 --- a/mayan/apps/document_states/permissions.py +++ b/mayan/apps/document_states/permissions.py @@ -22,6 +22,5 @@ permission_workflow_view = namespace.add_permission( # 'transition workflows' from one state to another, to move the workflow # forwards permission_workflow_transition = namespace.add_permission( - name='workflow_transition', - label=_('Transition workflows') + name='workflow_transition', label=_('Transition workflows') ) diff --git a/mayan/apps/document_states/serializers.py b/mayan/apps/document_states/serializers.py index 3f44f4a4dc..4193a46104 100644 --- a/mayan/apps/document_states/serializers.py +++ b/mayan/apps/document_states/serializers.py @@ -3,12 +3,17 @@ from __future__ import unicode_literals from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers +from rest_framework.exceptions import ValidationError from rest_framework.reverse import reverse from documents.models import DocumentType from documents.serializers import DocumentTypeSerializer +from user_management.serializers import UserSerializer -from .models import Workflow, WorkflowState, WorkflowTransition +from .models import ( + Workflow, WorkflowInstance, WorkflowInstanceLogEntry, WorkflowState, + WorkflowTransition +) class NewWorkflowDocumentTypeSerializer(serializers.Serializer): @@ -182,6 +187,71 @@ class WorkflowSerializer(serializers.HyperlinkedModelSerializer): model = Workflow +class WorkflowInstanceLogEntrySerializer(serializers.ModelSerializer): + document_workflow_url = serializers.SerializerMethodField() + transition = WorkflowTransitionSerializer(read_only=True) + user = UserSerializer(read_only=True) + + class Meta: + fields = ( + 'comment', 'datetime', 'document_workflow_url', 'transition', + 'user' + ) + model = WorkflowInstanceLogEntry + + def get_document_workflow_url(self, instance): + return reverse( + 'rest_api:workflowinstance-detail', args=( + instance.workflow_instance.document.pk, + instance.workflow_instance.pk, + ), request=self.context['request'], format=self.context['format'] + ) + + +class WorkflowInstanceSerializer(serializers.ModelSerializer): + current_state = WorkflowStateSerializer( + read_only=True, source='get_current_state' + ) + document_workflow_url = serializers.SerializerMethodField( + help_text=_( + 'API URL pointing to a workflow in relation to the ' + 'document to which it is attached. This URL is different than ' + 'the canonical workflow URL.' + ) + ) + last_log_entry = WorkflowInstanceLogEntrySerializer( + read_only=True, source='get_last_log_entry' + ) + log_entries_url = serializers.SerializerMethodField( + help_text=_('A link to the entire history of this workflow.') + ) + transition_choices = WorkflowTransitionSerializer( + many=True, read_only=True, source='get_transition_choices' + ) + workflow = WorkflowSerializer(read_only=True) + + class Meta: + fields = ( + 'current_state', 'document_workflow_url', 'last_log_entry', + 'log_entries_url', 'transition_choices', 'workflow', + ) + model = WorkflowInstance + + def get_document_workflow_url(self, instance): + return reverse( + 'rest_api:workflowinstance-detail', args=( + instance.document.pk, instance.pk, + ), request=self.context['request'], format=self.context['format'] + ) + + def get_log_entries_url(self, instance): + return reverse( + 'rest_api:workflowinstancelogentry-list', args=( + instance.document.pk, instance.pk, + ), request=self.context['request'], format=self.context['format'] + ) + + class WritableWorkflowSerializer(serializers.ModelSerializer): document_types_pk_list = serializers.CharField( help_text=_( @@ -240,3 +310,46 @@ class WritableWorkflowSerializer(serializers.ModelSerializer): ) return instance + + +class WritableWorkflowInstanceLogEntrySerializer(serializers.ModelSerializer): + document_workflow_url = serializers.SerializerMethodField() + transition_pk = serializers.IntegerField( + help_text=_('Primary key of the transition to be added.'), + write_only=True + ) + transition = WorkflowTransitionSerializer(read_only=True) + user = UserSerializer(read_only=True) + + class Meta: + fields = ( + 'comment', 'datetime', 'document_workflow_url', 'transition', + 'transition_pk', 'user' + ) + model = WorkflowInstanceLogEntry + + def create(self, validated_data): + validated_data['transition'] = WorkflowTransition.objects.get( + pk=validated_data.pop('transition_pk') + ) + validated_data['user'] = self.context['request'].user + validated_data['workflow_instance'] = self.context['workflow_instance'] + + if validated_data['transition'] not in validated_data['workflow_instance'].get_transition_choices(): + raise ValidationError( + { + 'transition_pk': _('Not a valid transition choice.') + } + ) + + return super(WritableWorkflowInstanceLogEntrySerializer, self).create( + validated_data + ) + + def get_document_workflow_url(self, instance): + return reverse( + 'rest_api:workflowinstance-detail', args=( + instance.workflow_instance.document.pk, + instance.workflow_instance.pk, + ), request=self.context['request'], format=self.context['format'] + ) diff --git a/mayan/apps/document_states/tests/literals.py b/mayan/apps/document_states/tests/literals.py index aadc1485e1..fac41db489 100644 --- a/mayan/apps/document_states/tests/literals.py +++ b/mayan/apps/document_states/tests/literals.py @@ -4,6 +4,7 @@ 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_INSTANCE_LOG_ENTRY_COMMENT = 'test workflow instance log entry comment' TEST_WORKFLOW_STATE_LABEL = 'test state label' TEST_WORKFLOW_STATE_LABEL_EDITED = 'test state label edited' TEST_WORKFLOW_STATE_COMPLETION = 66 diff --git a/mayan/apps/document_states/tests/test_api.py b/mayan/apps/document_states/tests/test_api.py index 491f1ba539..6545450ee7 100644 --- a/mayan/apps/document_states/tests/test_api.py +++ b/mayan/apps/document_states/tests/test_api.py @@ -3,7 +3,6 @@ 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 django.utils.encoding import force_text from rest_framework.test import APITestCase @@ -20,9 +19,9 @@ from ..models import Workflow from .literals import ( TEST_WORKFLOW_LABEL, TEST_WORKFLOW_LABEL_EDITED, 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 + TEST_WORKFLOW_INSTANCE_LOG_ENTRY_COMMENT, TEST_WORKFLOW_STATE_COMPLETION, + TEST_WORKFLOW_STATE_LABEL, TEST_WORKFLOW_STATE_LABEL_EDITED, + TEST_WORKFLOW_TRANSITION_LABEL, TEST_WORKFLOW_TRANSITION_LABEL_EDITED ) @@ -485,3 +484,130 @@ class WorkflowTransitionsAPITestCase(APITestCase): self.workflow_transition.destination_state, self.workflow_state_1 ) + + +@override_settings(OCR_AUTO_OCR=False) +class DocumentWorkflowsAPITestCase(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 + ) + + def tearDown(self): + if hasattr(self, 'document_type'): + self.document_type.delete() + + def _create_document(self): + with open(TEST_SMALL_DOCUMENT_PATH) as file_object: + self.document = self.document_type.new_document( + file_object=file_object + ) + + def _create_workflow(self): + self.workflow = Workflow.objects.create(label=TEST_WORKFLOW_LABEL) + self.workflow.document_types.add(self.document_type) + + def _create_workflow_states(self): + self._create_workflow() + self.workflow_state_1 = self.workflow.states.create( + completion=TEST_WORKFLOW_INITIAL_STATE_COMPLETION, + initial=True, 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 _create_workflow_instance_log_entry(self): + self.document.workflows.first().log_entries.create( + comment=TEST_WORKFLOW_INSTANCE_LOG_ENTRY_COMMENT, transition=self.workflow_transition, + user=self.admin_user + ) + + def test_workflow_instance_detail_view(self): + self._create_workflow_transition() + self._create_document() + + response = self.client.get( + reverse( + 'rest_api:workflowinstance-detail', args=( + self.document.pk, self.document.workflows.first().pk + ) + ), + ) + + self.assertEqual( + response.data['workflow']['label'], + TEST_WORKFLOW_LABEL + ) + + def test_workflow_instance_list_view(self): + self._create_workflow_transition() + self._create_document() + + response = self.client.get( + reverse( + 'rest_api:workflowinstance-list', args=(self.document.pk,) + ), + ) + + self.assertEqual( + response.data['results'][0]['workflow']['label'], + TEST_WORKFLOW_LABEL + ) + + def test_workflow_instance_log_entries_create_view(self): + self._create_workflow_transition() + self._create_document() + + workflow_instance = self.document.workflows.first() + + self.client.post( + reverse( + 'rest_api:workflowinstancelogentry-list', args=( + self.document.pk, workflow_instance.pk + ), + ), data={'transition_pk': self.workflow_transition.pk} + ) + + workflow_instance.refresh_from_db() + + self.assertEqual( + workflow_instance.log_entries.first().transition.label, + TEST_WORKFLOW_TRANSITION_LABEL + ) + + def test_workflow_instance_log_entries_list_view(self): + self._create_workflow_transition() + self._create_document() + self._create_workflow_instance_log_entry() + + response = self.client.get( + reverse( + 'rest_api:workflowinstancelogentry-list', args=( + self.document.pk, self.document.workflows.first().pk + ) + ), + ) + + self.assertEqual( + response.data['results'][0]['transition']['label'], + TEST_WORKFLOW_TRANSITION_LABEL + ) diff --git a/mayan/apps/document_states/urls.py b/mayan/apps/document_states/urls.py index bcb25a6584..d612b28324 100644 --- a/mayan/apps/document_states/urls.py +++ b/mayan/apps/document_states/urls.py @@ -4,9 +4,10 @@ from django.conf.urls import patterns, url from .api_views import ( APIWorkflowDocumentTypeList, APIWorkflowDocumentTypeView, - APIWorkflowListView, APIWorkflowStateListView, APIWorkflowStateView, - APIWorkflowTransitionListView, APIWorkflowTransitionView, - APIWorkflowView + APIWorkflowInstanceListView, APIWorkflowInstanceView, + APIWorkflowInstanceLogEntryListView, APIWorkflowListView, + APIWorkflowStateListView, APIWorkflowStateView, + APIWorkflowTransitionListView, APIWorkflowTransitionView, APIWorkflowView ) from .views import ( DocumentWorkflowInstanceListView, SetupWorkflowCreateView, @@ -136,4 +137,17 @@ api_urls = [ r'^workflows/(?P[0-9]+)/transitions/(?P[0-9]+)/$', APIWorkflowTransitionView.as_view(), name='workflowtransition-detail' ), + url( + r'^document/(?P[0-9]+)/workflows/$', + APIWorkflowInstanceListView.as_view(), name='workflowinstance-list' + ), + url( + r'^document/(?P[0-9]+)/workflows/(?P[0-9]+)/$', + APIWorkflowInstanceView.as_view(), name='workflowinstance-detail' + ), + url( + r'^document/(?P[0-9]+)/workflows/(?P[0-9]+)/log_entries/$', + APIWorkflowInstanceLogEntryListView.as_view(), + name='workflowinstancelogentry-list' + ), ] From 7a1b3e2ee2d2c7fe679da4fd0e3e388cc8d877a3 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 10 Feb 2017 17:46:03 -0400 Subject: [PATCH 17/32] Unify tag API test names and methodology. --- mayan/apps/tags/tests/test_api.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/mayan/apps/tags/tests/test_api.py b/mayan/apps/tags/tests/test_api.py index 06393e6bcb..454ecad870 100644 --- a/mayan/apps/tags/tests/test_api.py +++ b/mayan/apps/tags/tests/test_api.py @@ -41,6 +41,9 @@ class TagAPITestCase(APITestCase): if hasattr(self, 'document_type'): self.document_type.delete() + def _create_tag(self): + return Tag.objects.create(color=TEST_TAG_COLOR, label=TEST_TAG_LABEL) + def _document_create(self): self.document_type = DocumentType.objects.create( label=TEST_DOCUMENT_TYPE @@ -53,7 +56,7 @@ class TagAPITestCase(APITestCase): return document - def test_tag_create(self): + def test_tag_create_view(self): response = self.client.post( reverse('rest_api:tag-list'), { 'label': TEST_TAG_LABEL, 'color': TEST_TAG_COLOR @@ -69,7 +72,7 @@ class TagAPITestCase(APITestCase): self.assertEqual(tag.label, TEST_TAG_LABEL) self.assertEqual(tag.color, TEST_TAG_COLOR) - def test_tag_create_with_documents(self): + def test_tag_create_with_documents_view(self): response = self.client.post( reverse('rest_api:tag-list'), { 'label': TEST_TAG_LABEL, 'color': TEST_TAG_COLOR @@ -85,15 +88,15 @@ class TagAPITestCase(APITestCase): self.assertEqual(tag.label, TEST_TAG_LABEL) self.assertEqual(tag.color, TEST_TAG_COLOR) - def test_tag_delete(self): - tag = Tag.objects.create(color=TEST_TAG_COLOR, label=TEST_TAG_LABEL) + def test_tag_delete_view(self): + tag = self._create_tag() self.client.delete(reverse('rest_api:tag-detail', args=(tag.pk,))) self.assertEqual(Tag.objects.count(), 0) def test_tag_document_list_view(self): - tag = Tag.objects.create(color=TEST_TAG_COLOR, label=TEST_TAG_LABEL) + tag = self._create_tag() document = self._document_create() tag.documents.add(document) @@ -106,7 +109,7 @@ class TagAPITestCase(APITestCase): ) def test_tag_edit_via_patch(self): - tag = Tag.objects.create(color=TEST_TAG_COLOR, label=TEST_TAG_LABEL) + tag = self._create_tag() self.client.patch( reverse('rest_api:tag-detail', args=(tag.pk,)), @@ -116,13 +119,13 @@ class TagAPITestCase(APITestCase): } ) - tag = Tag.objects.first() + tag.refresh_from_db() self.assertEqual(tag.label, TEST_TAG_LABEL_EDITED) self.assertEqual(tag.color, TEST_TAG_COLOR_EDITED) def test_tag_edit_via_put(self): - tag = Tag.objects.create(color=TEST_TAG_COLOR, label=TEST_TAG_LABEL) + tag = self._create_tag() self.client.put( reverse('rest_api:tag-detail', args=(tag.pk,)), @@ -132,13 +135,13 @@ class TagAPITestCase(APITestCase): } ) - tag = Tag.objects.first() + tag.refresh_from_db() self.assertEqual(tag.label, TEST_TAG_LABEL_EDITED) self.assertEqual(tag.color, TEST_TAG_COLOR_EDITED) def test_document_attach_tag_view(self): - tag = Tag.objects.create(color=TEST_TAG_COLOR, label=TEST_TAG_LABEL) + tag = self._create_tag() document = self._document_create() self.client.post( @@ -148,7 +151,7 @@ class TagAPITestCase(APITestCase): self.assertQuerysetEqual(document.tags.all(), (repr(tag),)) def test_document_tag_detail_view(self): - tag = Tag.objects.create(color=TEST_TAG_COLOR, label=TEST_TAG_LABEL) + tag = self._create_tag() document = self._document_create() tag.documents.add(document) @@ -159,7 +162,7 @@ class TagAPITestCase(APITestCase): self.assertEqual(response.data['label'], tag.label) def test_document_tag_list_view(self): - tag = Tag.objects.create(color=TEST_TAG_COLOR, label=TEST_TAG_LABEL) + tag = self._create_tag() document = self._document_create() tag.documents.add(document) @@ -169,7 +172,7 @@ class TagAPITestCase(APITestCase): self.assertEqual(response.data['results'][0]['label'], tag.label) def test_document_tag_remove_view(self): - tag = Tag.objects.create(color=TEST_TAG_COLOR, label=TEST_TAG_LABEL) + tag = self._create_tag() document = self._document_create() tag.documents.add(document) From c218819728f641a7f4d666fffeab5cc864da5433 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 10 Feb 2017 18:16:46 -0400 Subject: [PATCH 18/32] Add API endpoints for the message of the day app. --- docs/releases/2.1.8.rst | 80 ++++++++++++++ docs/releases/index.rst | 1 + mayan/apps/motd/api_views.py | 76 +++++++++++++ mayan/apps/motd/apps.py | 3 + mayan/apps/motd/migrations/0001_initial.py | 37 ++++++- .../migrations/0005_auto_20160510_0025.py | 20 +++- mayan/apps/motd/serializers.py | 17 +++ mayan/apps/motd/tests/literals.py | 6 + mayan/apps/motd/tests/test_api.py | 103 ++++++++++++++++++ mayan/apps/motd/tests/test_models.py | 3 +- mayan/apps/motd/urls.py | 13 ++- 11 files changed, 346 insertions(+), 13 deletions(-) create mode 100644 docs/releases/2.1.8.rst create mode 100644 mayan/apps/motd/api_views.py create mode 100644 mayan/apps/motd/serializers.py create mode 100644 mayan/apps/motd/tests/literals.py create mode 100644 mayan/apps/motd/tests/test_api.py diff --git a/docs/releases/2.1.8.rst b/docs/releases/2.1.8.rst new file mode 100644 index 0000000000..196d21b395 --- /dev/null +++ b/docs/releases/2.1.8.rst @@ -0,0 +1,80 @@ +=============================== +Mayan EDMS v2.1.8 release notes +=============================== + +Released: February XX, 2017 + +What's new +========== + +This is a bug-fix release and all users are encouraged to upgrade. The focus +of this micro release was REST API improvement. + +Changes +------------- + +- Fixes in the trashed document API endpoints. +- Improved tags API PUT and PATCH endpoints. +- Bulk document adding when creating and editing tags. +- The version of django-mptt is preserved in case mayan-cabinets is installed. +- Add Django GPG API endpoints for singing keys. +- Add API endpoints for the document states app. +- Add API endpoints for the messsage of the day (MOTD) app. + + +Removals +-------- +* None + +Upgrading from a previous version +--------------------------------- + +Using PIP +~~~~~~~~~ + +Type in the console:: + + $ pip install -U mayan-edms + +the requirements will also be updated automatically. + +Using Git +~~~~~~~~~ + +If you installed Mayan EDMS by cloning the Git repository issue the commands:: + + $ git reset --hard HEAD + $ git pull + +otherwise download the compressed archived and uncompress it overriding the +existing installation. + +Next upgrade/add the new requirements:: + + $ pip install --upgrade -r requirements.txt + +Common steps +~~~~~~~~~~~~ + +Migrate existing database schema with:: + + $ mayan-edms.py performupgrade + +Add new static media:: + + $ mayan-edms.py collectstatic --noinput + +The upgrade procedure is now complete. + + +Backward incompatible changes +============================= + +* None + +Bugs fixed or issues closed +=========================== + +* None + +.. _PyPI: https://pypi.python.org/pypi/mayan-edms/ diff --git a/docs/releases/index.rst b/docs/releases/index.rst index de40317d59..84cb057583 100644 --- a/docs/releases/index.rst +++ b/docs/releases/index.rst @@ -22,6 +22,7 @@ versions of the documentation contain the release notes for any later releases. .. toctree:: :maxdepth: 1 + 2.1.8 2.1.7 2.1.6 2.1.5 diff --git a/mayan/apps/motd/api_views.py b/mayan/apps/motd/api_views.py new file mode 100644 index 0000000000..4799ab357d --- /dev/null +++ b/mayan/apps/motd/api_views.py @@ -0,0 +1,76 @@ +from __future__ import absolute_import, unicode_literals + +from rest_framework import generics + +from rest_api.filters import MayanObjectPermissionsFilter +from rest_api.permissions import MayanPermission + +from .models import Message +from .permissions import ( + permission_message_create, permission_message_delete, + permission_message_edit, permission_message_view +) +from .serializers import MessageSerializer + + +class APIMessageListView(generics.ListCreateAPIView): + filter_backends = (MayanObjectPermissionsFilter,) + mayan_object_permissions = {'GET': (permission_message_view,)} + mayan_view_permissions = {'POST': (permission_message_create,)} + permission_classes = (MayanPermission,) + queryset = Message.objects.all() + serializer_class = MessageSerializer + + def get(self, *args, **kwargs): + """ + Returns a list of all the messages. + """ + + return super(APIMessageListView, self).get(*args, **kwargs) + + def post(self, *args, **kwargs): + """ + Create a new message. + """ + + return super(APIMessageListView, self).post(*args, **kwargs) + + +class APIMessageView(generics.RetrieveUpdateDestroyAPIView): + filter_backends = (MayanObjectPermissionsFilter,) + mayan_object_permissions = { + 'DELETE': (permission_message_delete,), + 'GET': (permission_message_view,), + 'PATCH': (permission_message_edit,), + 'PUT': (permission_message_edit,) + } + queryset = Message.objects.all() + serializer_class = MessageSerializer + + def delete(self, *args, **kwargs): + """ + Delete the selected message. + """ + + return super(APIMessageView, self).delete(*args, **kwargs) + + def get(self, *args, **kwargs): + """ + Return the details of the selected message. + """ + + return super(APIMessageView, self).get(*args, **kwargs) + + def patch(self, *args, **kwargs): + """ + Edit the selected message. + """ + + return super(APIMessageView, self).patch(*args, **kwargs) + + def put(self, *args, **kwargs): + """ + Edit the selected message. + """ + + return super(APIMessageView, self).put(*args, **kwargs) diff --git a/mayan/apps/motd/apps.py b/mayan/apps/motd/apps.py index 0258d066da..afb6c8f132 100644 --- a/mayan/apps/motd/apps.py +++ b/mayan/apps/motd/apps.py @@ -6,6 +6,7 @@ from django.utils.translation import ugettext_lazy as _ from common import MayanAppConfig, menu_object, menu_secondary, menu_setup from navigation import SourceColumn +from rest_api.classes import APIEndPoint from .links import ( link_message_create, link_message_delete, link_message_edit, @@ -23,6 +24,8 @@ class MOTDApp(MayanAppConfig): def ready(self): super(MOTDApp, self).ready() + APIEndPoint(app=self, version_string='1') + Message = self.get_model('Message') SourceColumn( diff --git a/mayan/apps/motd/migrations/0001_initial.py b/mayan/apps/motd/migrations/0001_initial.py index 545f550f5d..f83b25ed46 100644 --- a/mayan/apps/motd/migrations/0001_initial.py +++ b/mayan/apps/motd/migrations/0001_initial.py @@ -13,12 +13,37 @@ class Migration(migrations.Migration): migrations.CreateModel( name='MessageOfTheDay', fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('label', models.CharField(max_length=32, verbose_name='Label')), - ('message', models.TextField(verbose_name='Message', blank=True)), - ('enabled', models.BooleanField(default=True, verbose_name='Enabled')), - ('start_datetime', models.DateTimeField(verbose_name='Start date time', blank=True)), - ('end_datetime', models.DateTimeField(verbose_name='End date time', blank=True)), + ( + 'id', models.AutoField( + verbose_name='ID', serialize=False, + auto_created=True, primary_key=True + ) + ), + ( + 'label', models.CharField( + max_length=32, verbose_name='Label' + ) + ), + ( + 'message', models.TextField( + verbose_name='Message', blank=True + ) + ), + ( + 'enabled', models.BooleanField( + default=True, verbose_name='Enabled' + ) + ), + ( + 'start_datetime', models.DateTimeField( + verbose_name='Start date time', blank=True + ) + ), + ( + 'end_datetime', models.DateTimeField( + verbose_name='End date time', blank=True + ) + ), ], options={ 'verbose_name': 'Message of the day', diff --git a/mayan/apps/motd/migrations/0005_auto_20160510_0025.py b/mayan/apps/motd/migrations/0005_auto_20160510_0025.py index 2f87651d4c..469fa0c76d 100644 --- a/mayan/apps/motd/migrations/0005_auto_20160510_0025.py +++ b/mayan/apps/motd/migrations/0005_auto_20160510_0025.py @@ -14,21 +14,33 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='message', name='end_datetime', - field=models.DateTimeField(help_text='Date and time until when this message is to be displayed.', null=True, verbose_name='End date time', blank=True), + field=models.DateTimeField( + help_text='Date and time until when this message is to be displayed.', + null=True, verbose_name='End date time', blank=True + ), ), migrations.AlterField( model_name='message', name='label', - field=models.CharField(help_text='Short description of this message.', max_length=32, verbose_name='Label'), + field=models.CharField( + help_text='Short description of this message.', max_length=32, + verbose_name='Label' + ), ), migrations.AlterField( model_name='message', name='message', - field=models.TextField(help_text='The actual message to be displayed.', verbose_name='Message'), + field=models.TextField( + help_text='The actual message to be displayed.', + verbose_name='Message' + ), ), migrations.AlterField( model_name='message', name='start_datetime', - field=models.DateTimeField(help_text='Date and time after which this message will be displayed.', null=True, verbose_name='Start date time', blank=True), + field=models.DateTimeField( + help_text='Date and time after which this message will be displayed.', + null=True, verbose_name='Start date time', blank=True + ), ), ] diff --git a/mayan/apps/motd/serializers.py b/mayan/apps/motd/serializers.py new file mode 100644 index 0000000000..1637ac91b6 --- /dev/null +++ b/mayan/apps/motd/serializers.py @@ -0,0 +1,17 @@ +from __future__ import absolute_import, unicode_literals + +from rest_framework import serializers + +from .models import Message + + +class MessageSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + extra_kwargs = { + 'url': {'view_name': 'rest_api:message-detail'}, + } + fields = ( + 'end_datetime', 'enabled', 'label', 'message', 'start_datetime', + 'id', 'url' + ) + model = Message diff --git a/mayan/apps/motd/tests/literals.py b/mayan/apps/motd/tests/literals.py new file mode 100644 index 0000000000..431ef052bb --- /dev/null +++ b/mayan/apps/motd/tests/literals.py @@ -0,0 +1,6 @@ +from __future__ import unicode_literals + +TEST_LABEL = 'test label' +TEST_LABEL_EDITED = 'test label edited' +TEST_MESSAGE = 'test message' +TEST_MESSAGE_EDITED = 'test message edited' diff --git a/mayan/apps/motd/tests/test_api.py b/mayan/apps/motd/tests/test_api.py new file mode 100644 index 0000000000..6cb6e8bdfa --- /dev/null +++ b/mayan/apps/motd/tests/test_api.py @@ -0,0 +1,103 @@ +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 user_management.tests.literals import ( + TEST_ADMIN_EMAIL, TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME +) + +from ..models import Message + +from .literals import ( + TEST_LABEL, TEST_LABEL_EDITED, TEST_MESSAGE, TEST_MESSAGE_EDITED +) + + +@override_settings(OCR_AUTO_OCR=False) +class MOTDAPITestCase(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 + ) + + def _create_message(self): + return Message.objects.create( + label=TEST_LABEL, message=TEST_MESSAGE + ) + + def test_message_create_view(self): + response = self.client.post( + reverse('rest_api:message-list'), { + 'label': TEST_LABEL, 'message': TEST_MESSAGE + } + ) + + message = Message.objects.first() + self.assertEqual(response.data['id'], message.pk) + self.assertEqual(response.data['label'], TEST_LABEL) + self.assertEqual(response.data['message'], TEST_MESSAGE) + + self.assertEqual(Message.objects.count(), 1) + self.assertEqual(message.label, TEST_LABEL) + self.assertEqual(message.message, TEST_MESSAGE) + + def test_message_delete_view(self): + message = self._create_message() + + self.client.delete( + reverse('rest_api:message-detail', args=(message.pk,)) + ) + + self.assertEqual(Message.objects.count(), 0) + + def test_message_detail_view(self): + message = self._create_message() + + response = self.client.get( + reverse('rest_api:message-detail', args=(message.pk,)) + ) + + self.assertEqual( + response.data['label'], TEST_LABEL + ) + + def test_message_path_view(self): + message = self._create_message() + + self.client.patch( + reverse('rest_api:message-detail', args=(message.pk,)), + { + 'label': TEST_LABEL_EDITED, + 'message': TEST_MESSAGE_EDITED + } + ) + + message.refresh_from_db() + + self.assertEqual(message.label, TEST_LABEL_EDITED) + self.assertEqual(message.message, TEST_MESSAGE_EDITED) + + def test_message_put_view(self): + message = self._create_message() + + self.client.put( + reverse('rest_api:message-detail', args=(message.pk,)), + { + 'label': TEST_LABEL_EDITED, + 'message': TEST_MESSAGE_EDITED + } + ) + + message.refresh_from_db() + + self.assertEqual(message.label, TEST_LABEL_EDITED) + self.assertEqual(message.message, TEST_MESSAGE_EDITED) diff --git a/mayan/apps/motd/tests/test_models.py b/mayan/apps/motd/tests/test_models.py index bb250982fa..457cacedac 100644 --- a/mayan/apps/motd/tests/test_models.py +++ b/mayan/apps/motd/tests/test_models.py @@ -7,8 +7,7 @@ from django.utils import timezone from ..models import Message -TEST_LABEL = 'test label' -TEST_MESSAGE = 'test message' +from .literals import TEST_LABEL, TEST_MESSAGE class MOTDTestCase(TestCase): diff --git a/mayan/apps/motd/urls.py b/mayan/apps/motd/urls.py index 709e8d7488..e639db5f57 100644 --- a/mayan/apps/motd/urls.py +++ b/mayan/apps/motd/urls.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from django.conf.urls import patterns, url +from .api_views import APIMessageListView, APIMessageView from .views import ( MessageCreateView, MessageDeleteView, MessageEditView, MessageListView ) @@ -10,9 +11,19 @@ urlpatterns = patterns( '', url(r'^list/$', MessageListView.as_view(), name='message_list'), url(r'^create/$', MessageCreateView.as_view(), name='message_create'), - url(r'^(?P\d+)/edit/$', MessageEditView.as_view(), name='message_edit'), + url( + r'^(?P\d+)/edit/$', MessageEditView.as_view(), name='message_edit' + ), url( r'^(?P\d+)/delete/$', MessageDeleteView.as_view(), name='message_delete' ), ) + +api_urls = [ + url(r'^messages/$', APIMessageListView.as_view(), name='message-list'), + url( + r'^messages/(?P[0-9]+)/$', APIMessageView.as_view(), + name='message-detail' + ), +] From 651f659a05f69044fcb9226bdcf19c2713b2a9f6 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 11 Feb 2017 02:12:25 -0400 Subject: [PATCH 19/32] Add linking app setup API views and tests. --- mayan/apps/linking/api_views.py | 196 ++++++++++++++++++ mayan/apps/linking/apps.py | 3 + mayan/apps/linking/serializers.py | 50 +++++ mayan/apps/linking/tests/literals.py | 6 +- mayan/apps/linking/tests/test_api.py | 265 +++++++++++++++++++++++++ mayan/apps/linking/tests/test_views.py | 8 +- mayan/apps/linking/urls.py | 23 +++ 7 files changed, 546 insertions(+), 5 deletions(-) create mode 100644 mayan/apps/linking/api_views.py create mode 100644 mayan/apps/linking/serializers.py create mode 100644 mayan/apps/linking/tests/test_api.py diff --git a/mayan/apps/linking/api_views.py b/mayan/apps/linking/api_views.py new file mode 100644 index 0000000000..931ea56119 --- /dev/null +++ b/mayan/apps/linking/api_views.py @@ -0,0 +1,196 @@ +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 permissions import Permission +from rest_api.filters import MayanObjectPermissionsFilter +from rest_api.permissions import MayanPermission + +from .models import SmartLink +from .permissions import ( + permission_smart_link_create, permission_smart_link_delete, + permission_smart_link_edit, permission_smart_link_view +) +from .serializers import SmartLinkConditionSerializer, SmartLinkSerializer + + +class APISmartLinkConditionListView(generics.ListCreateAPIView): + serializer_class = SmartLinkConditionSerializer + + def get(self, *args, **kwargs): + """ + Returns a list of all the smart link conditions. + """ + return super(APISmartLinkConditionListView, self).get(*args, **kwargs) + + def get_queryset(self): + return self.get_smart_link().conditions.all() + + def get_serializer_context(self): + """ + Extra context provided to the serializer class. + """ + return { + 'format': self.format_kwarg, + 'request': self.request, + 'smart_link': self.get_smart_link(), + 'view': self + } + + def get_smart_link(self): + if self.request.method == 'GET': + permission_required = permission_smart_link_view + else: + permission_required = permission_smart_link_edit + + smart_link = get_object_or_404(SmartLink, pk=self.kwargs['pk']) + + try: + Permission.check_permissions( + self.request.user, (permission_required,) + ) + except PermissionDenied: + AccessControlList.objects.check_access( + permission_required, self.request.user, smart_link + ) + + return smart_link + + def post(self, *args, **kwargs): + """ + Create a new smart link condition. + """ + return super(APISmartLinkConditionListView, self).post(*args, **kwargs) + + +class APISmartLinkConditionView(generics.RetrieveUpdateDestroyAPIView): + lookup_url_kwarg = 'condition_pk' + serializer_class = SmartLinkConditionSerializer + + def delete(self, *args, **kwargs): + """ + Delete the selected smart link condition. + """ + + return super(APISmartLinkConditionView, self).delete(*args, **kwargs) + + def get(self, *args, **kwargs): + """ + Return the details of the selected smart link condition. + """ + + return super(APISmartLinkConditionView, self).get(*args, **kwargs) + + def get_queryset(self): + return self.get_smart_link().conditions.all() + + def get_serializer_context(self): + """ + Extra context provided to the serializer class. + """ + return { + 'format': self.format_kwarg, + 'request': self.request, + 'smart_link': self.get_smart_link(), + 'view': self + } + + def get_smart_link(self): + if self.request.method == 'GET': + permission_required = permission_smart_link_view + else: + permission_required = permission_smart_link_edit + + smart_link = get_object_or_404(SmartLink, pk=self.kwargs['pk']) + + try: + Permission.check_permissions( + self.request.user, (permission_required,) + ) + except PermissionDenied: + AccessControlList.objects.check_access( + permission_required, self.request.user, smart_link + ) + + return smart_link + + def patch(self, *args, **kwargs): + """ + Edit the selected smart link condition. + """ + + return super(APISmartLinkConditionView, self).patch(*args, **kwargs) + + def put(self, *args, **kwargs): + """ + Edit the selected smart link condition. + """ + + return super(APISmartLinkConditionView, self).put(*args, **kwargs) + + +class APISmartLinkListView(generics.ListCreateAPIView): + filter_backends = (MayanObjectPermissionsFilter,) + mayan_object_permissions = {'GET': (permission_smart_link_view,)} + mayan_view_permissions = {'POST': (permission_smart_link_create,)} + permission_classes = (MayanPermission,) + queryset = SmartLink.objects.all() + serializer_class = SmartLinkSerializer + + def get(self, *args, **kwargs): + """ + Returns a list of all the smart links. + """ + + return super(APISmartLinkListView, self).get(*args, **kwargs) + + def post(self, *args, **kwargs): + """ + Create a new smart link. + """ + + return super(APISmartLinkListView, self).post(*args, **kwargs) + + +class APISmartLinkView(generics.RetrieveUpdateDestroyAPIView): + filter_backends = (MayanObjectPermissionsFilter,) + mayan_object_permissions = { + 'DELETE': (permission_smart_link_delete,), + 'GET': (permission_smart_link_view,), + 'PATCH': (permission_smart_link_edit,), + 'PUT': (permission_smart_link_edit,) + } + queryset = SmartLink.objects.all() + serializer_class = SmartLinkSerializer + + def delete(self, *args, **kwargs): + """ + Delete the selected smart link. + """ + + return super(APISmartLinkView, self).delete(*args, **kwargs) + + def get(self, *args, **kwargs): + """ + Return the details of the selected smart ink. + """ + + return super(APISmartLinkView, self).get(*args, **kwargs) + + def patch(self, *args, **kwargs): + """ + Edit the selected smart link. + """ + + return super(APISmartLinkView, self).patch(*args, **kwargs) + + def put(self, *args, **kwargs): + """ + Edit the selected smart link. + """ + + return super(APISmartLinkView, self).put(*args, **kwargs) diff --git a/mayan/apps/linking/apps.py b/mayan/apps/linking/apps.py index 54250c85e4..41eb9df8cc 100644 --- a/mayan/apps/linking/apps.py +++ b/mayan/apps/linking/apps.py @@ -12,6 +12,7 @@ from common import ( ) from common.widgets import two_state_template from navigation import SourceColumn +from rest_api.classes import APIEndPoint from .links import ( link_smart_link_create, link_smart_link_condition_create, @@ -35,6 +36,8 @@ class LinkingApp(MayanAppConfig): def ready(self): super(LinkingApp, self).ready() + APIEndPoint(app=self, version_string='1') + Document = apps.get_model( app_label='documents', model_name='Document' ) diff --git a/mayan/apps/linking/serializers.py b/mayan/apps/linking/serializers.py new file mode 100644 index 0000000000..4e94a3dc36 --- /dev/null +++ b/mayan/apps/linking/serializers.py @@ -0,0 +1,50 @@ +from __future__ import absolute_import, unicode_literals + +from rest_framework import serializers +from rest_framework.reverse import reverse + +from .models import SmartLink, SmartLinkCondition + + +class SmartLinkConditionSerializer(serializers.HyperlinkedModelSerializer): + smart_link_url = serializers.SerializerMethodField() + url = serializers.SerializerMethodField() + + class Meta: + fields = ( + 'enabled', 'expression', 'foreign_document_data', 'inclusion', + 'id', 'negated', 'operator', 'smart_link_url', 'url' + ) + model = SmartLinkCondition + + def create(self, validated_data): + validated_data['smart_link'] = self.context['smart_link'] + return super(SmartLinkConditionSerializer, self).create(validated_data) + + def get_smart_link_url(self, instance): + return reverse( + 'rest_api:smartlink-detail', args=(instance.smart_link.pk,), + request=self.context['request'], format=self.context['format'] + ) + + def get_url(self, instance): + return reverse( + 'rest_api:smartlinkcondition-detail', args=( + instance.smart_link.pk, instance.pk, + ), request=self.context['request'], format=self.context['format'] + ) + + +class SmartLinkSerializer(serializers.HyperlinkedModelSerializer): + conditions_url = serializers.HyperlinkedIdentityField( + view_name='rest_api:smartlinkcondition-list' + ) + + class Meta: + extra_kwargs = { + 'url': {'view_name': 'rest_api:smartlink-detail'}, + } + fields = ( + 'conditions_url', 'dynamic_label', 'enabled', 'label', 'id', 'url' + ) + model = SmartLink diff --git a/mayan/apps/linking/tests/literals.py b/mayan/apps/linking/tests/literals.py index 279ea65564..e527654e91 100644 --- a/mayan/apps/linking/tests/literals.py +++ b/mayan/apps/linking/tests/literals.py @@ -1,5 +1,9 @@ from __future__ import unicode_literals +TEST_SMART_LINK_CONDITION_FOREIGN_DOCUMENT_DATA = 'label' +TEST_SMART_LINK_CONDITION_EXPRESSION = '\'test\'' +TEST_SMART_LINK_CONDITION_EXPRESSION_EDITED = '\'test edited\'' +TEST_SMART_LINK_CONDITION_OPERATOR = 'icontains' TEST_SMART_LINK_DYNAMIC_LABEL = '{{ document.label }}' -TEST_SMART_LINK_EDITED_LABEL = 'test edited label' +TEST_SMART_LINK_LABEL_EDITED = 'test edited label' TEST_SMART_LINK_LABEL = 'test label' diff --git a/mayan/apps/linking/tests/test_api.py b/mayan/apps/linking/tests/test_api.py new file mode 100644 index 0000000000..8f334b483b --- /dev/null +++ b/mayan/apps/linking/tests/test_api.py @@ -0,0 +1,265 @@ +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.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 SmartLink, SmartLinkCondition + +from .literals import ( + TEST_SMART_LINK_CONDITION_FOREIGN_DOCUMENT_DATA, + TEST_SMART_LINK_CONDITION_EXPRESSION, + TEST_SMART_LINK_CONDITION_EXPRESSION_EDITED, + TEST_SMART_LINK_CONDITION_OPERATOR, TEST_SMART_LINK_DYNAMIC_LABEL, + TEST_SMART_LINK_LABEL_EDITED, TEST_SMART_LINK_LABEL +) + + +@override_settings(OCR_AUTO_OCR=False) +class SmartLinkAPITestCase(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 + ) + + def tearDown(self): + if hasattr(self, 'document_type'): + self.document_type.delete() + + def _create_document_type(self): + self.document_type = DocumentType.objects.create( + label=TEST_DOCUMENT_TYPE + ) + + def _create_document(self): + with open(TEST_SMALL_DOCUMENT_PATH) as file_object: + self.document = self.document_type.new_document( + file_object=file_object + ) + + def _create_smart_link(self): + return SmartLink.objects.create( + label=TEST_SMART_LINK_LABEL, + dynamic_label=TEST_SMART_LINK_DYNAMIC_LABEL + ) + + def test_smart_link_create_view(self): + response = self.client.post( + reverse('rest_api:smartlink-list'), { + 'label': TEST_SMART_LINK_LABEL + } + ) + + smart_link = SmartLink.objects.first() + self.assertEqual(response.data['id'], smart_link.pk) + self.assertEqual(response.data['label'], TEST_SMART_LINK_LABEL) + + self.assertEqual(SmartLink.objects.count(), 1) + self.assertEqual(smart_link.label, TEST_SMART_LINK_LABEL) + + def test_smart_link_delete_view(self): + smart_link = self._create_smart_link() + + self.client.delete( + reverse('rest_api:smartlink-detail', args=(smart_link.pk,)) + ) + + self.assertEqual(SmartLink.objects.count(), 0) + + def test_smart_link_detail_view(self): + smart_link = self._create_smart_link() + + response = self.client.get( + reverse('rest_api:smartlink-detail', args=(smart_link.pk,)) + ) + + self.assertEqual( + response.data['label'], TEST_SMART_LINK_LABEL + ) + + def test_smart_link_patch_view(self): + smart_link = self._create_smart_link() + + self.client.patch( + reverse('rest_api:smartlink-detail', args=(smart_link.pk,)), + data={ + 'label': TEST_SMART_LINK_LABEL_EDITED, + } + ) + + smart_link.refresh_from_db() + + self.assertEqual(smart_link.label, TEST_SMART_LINK_LABEL_EDITED) + + def test_smart_link_put_view(self): + smart_link = self._create_smart_link() + + self.client.put( + reverse('rest_api:smartlink-detail', args=(smart_link.pk,)), + data={ + 'label': TEST_SMART_LINK_LABEL_EDITED, + } + ) + + smart_link.refresh_from_db() + + self.assertEqual(smart_link.label, TEST_SMART_LINK_LABEL_EDITED) + + +@override_settings(OCR_AUTO_OCR=False) +class SmartLinkConditionAPITestCase(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 + ) + + def tearDown(self): + if hasattr(self, 'document_type'): + self.document_type.delete() + + def _create_document_type(self): + self.document_type = DocumentType.objects.create( + label=TEST_DOCUMENT_TYPE + ) + + def _create_document(self): + with open(TEST_SMALL_DOCUMENT_PATH) as file_object: + self.document = self.document_type.new_document( + file_object=file_object + ) + + def _create_smart_link(self): + self.smart_link = SmartLink.objects.create( + label=TEST_SMART_LINK_LABEL, + dynamic_label=TEST_SMART_LINK_DYNAMIC_LABEL + ) + self.smart_link.document_types.add(self.document_type) + + def _create_smart_link_condition(self): + self.smart_link_condition = SmartLinkCondition.objects.create( + smart_link=self.smart_link, + foreign_document_data=TEST_SMART_LINK_CONDITION_FOREIGN_DOCUMENT_DATA, + expression=TEST_SMART_LINK_CONDITION_EXPRESSION, + operator=TEST_SMART_LINK_CONDITION_OPERATOR + ) + + def test_smart_link_condition_create_view(self): + self._create_document_type() + self._create_smart_link() + + response = self.client.post( + reverse( + 'rest_api:smartlinkcondition-list', args=(self.smart_link.pk,) + ), { + 'foreign_document_data': TEST_SMART_LINK_CONDITION_FOREIGN_DOCUMENT_DATA, + 'expression': TEST_SMART_LINK_CONDITION_EXPRESSION, + 'operator': TEST_SMART_LINK_CONDITION_OPERATOR + } + ) + + smart_link_condition = SmartLinkCondition.objects.first() + self.assertEqual(response.data['id'], smart_link_condition.pk) + self.assertEqual( + response.data['operator'], TEST_SMART_LINK_CONDITION_OPERATOR + ) + + self.assertEqual(SmartLinkCondition.objects.count(), 1) + self.assertEqual( + smart_link_condition.operator, TEST_SMART_LINK_CONDITION_OPERATOR + ) + + def test_smart_link_condition_delete_view(self): + self._create_document_type() + self._create_smart_link() + self._create_smart_link_condition() + + self.client.delete( + reverse( + 'rest_api:smartlinkcondition-detail', + args=(self.smart_link.pk, self.smart_link_condition.pk) + ) + ) + + self.assertEqual(SmartLinkCondition.objects.count(), 0) + + def test_smart_link_condition_detail_view(self): + self._create_document_type() + self._create_smart_link() + self._create_smart_link_condition() + + response = self.client.get( + reverse( + 'rest_api:smartlinkcondition-detail', + args=(self.smart_link.pk, self.smart_link_condition.pk) + ) + ) + + self.assertEqual( + response.data['operator'], TEST_SMART_LINK_CONDITION_OPERATOR + ) + + def test_smart_link_condition_patch_view(self): + self._create_document_type() + self._create_smart_link() + self._create_smart_link_condition() + + self.client.patch( + reverse( + 'rest_api:smartlinkcondition-detail', + args=(self.smart_link.pk, self.smart_link_condition.pk) + ), + data={ + 'expression': TEST_SMART_LINK_CONDITION_EXPRESSION_EDITED, + } + ) + + self.smart_link_condition.refresh_from_db() + + self.assertEqual( + self.smart_link_condition.expression, + TEST_SMART_LINK_CONDITION_EXPRESSION_EDITED + ) + + def test_smart_link_condition_put_view(self): + self._create_document_type() + self._create_smart_link() + self._create_smart_link_condition() + + self.client.put( + reverse( + 'rest_api:smartlinkcondition-detail', + args=(self.smart_link.pk, self.smart_link_condition.pk) + ), + data={ + 'expression': TEST_SMART_LINK_CONDITION_EXPRESSION_EDITED, + 'foreign_document_data': TEST_SMART_LINK_CONDITION_FOREIGN_DOCUMENT_DATA, + 'operator': TEST_SMART_LINK_CONDITION_OPERATOR, + } + ) + + self.smart_link_condition.refresh_from_db() + + self.assertEqual( + self.smart_link_condition.expression, + TEST_SMART_LINK_CONDITION_EXPRESSION_EDITED + ) diff --git a/mayan/apps/linking/tests/test_views.py b/mayan/apps/linking/tests/test_views.py index 317e2450b5..d57eed6620 100644 --- a/mayan/apps/linking/tests/test_views.py +++ b/mayan/apps/linking/tests/test_views.py @@ -13,7 +13,7 @@ from ..permissions import ( ) from .literals import ( - TEST_SMART_LINK_DYNAMIC_LABEL, TEST_SMART_LINK_EDITED_LABEL, + TEST_SMART_LINK_DYNAMIC_LABEL, TEST_SMART_LINK_LABEL_EDITED, TEST_SMART_LINK_LABEL ) @@ -83,7 +83,7 @@ class SmartLinkViewTestCase(GenericDocumentViewTestCase): response = self.post( 'linking:smart_link_edit', args=(smart_link.pk,), data={ - 'label': TEST_SMART_LINK_EDITED_LABEL + 'label': TEST_SMART_LINK_LABEL_EDITED } ) self.assertEqual(response.status_code, 403) @@ -101,13 +101,13 @@ class SmartLinkViewTestCase(GenericDocumentViewTestCase): response = self.post( 'linking:smart_link_edit', args=(smart_link.pk,), data={ - 'label': TEST_SMART_LINK_EDITED_LABEL + 'label': TEST_SMART_LINK_LABEL_EDITED }, follow=True ) smart_link = SmartLink.objects.get(pk=smart_link.pk) self.assertContains(response, text='update', status_code=200) - self.assertEqual(smart_link.label, TEST_SMART_LINK_EDITED_LABEL) + self.assertEqual(smart_link.label, TEST_SMART_LINK_LABEL_EDITED) def setup_smart_links(self): smart_link = SmartLink.objects.create( diff --git a/mayan/apps/linking/urls.py b/mayan/apps/linking/urls.py index c9a1c01a1a..3876fb73aa 100644 --- a/mayan/apps/linking/urls.py +++ b/mayan/apps/linking/urls.py @@ -2,6 +2,10 @@ from __future__ import unicode_literals from django.conf.urls import patterns, url +from .api_views import ( + APISmartLinkListView, APISmartLinkView, APISmartLinkConditionListView, + APISmartLinkConditionView +) from .views import ( DocumentSmartLinkListView, ResolvedSmartLinkView, SetupSmartLinkDocumentTypesView, SmartLinkConditionListView, @@ -61,3 +65,22 @@ urlpatterns = patterns( name='smart_link_condition_delete' ), ) + +api_urls = [ + url( + r'^smart_links/$', APISmartLinkListView.as_view(), name='smartlink-list' + ), + url( + r'^smart_links/(?P[0-9]+)/$', APISmartLinkView.as_view(), + name='smartlink-detail' + ), + url( + r'^smart_links/(?P[0-9]+)/conditions/$', + APISmartLinkConditionListView.as_view(), name='smartlinkcondition-list' + ), + url( + r'^smart_links/(?P[0-9]+)/conditions/(?P[0-9]+)/$', + APISmartLinkConditionView.as_view(), + name='smartlinkcondition-detail' + ), +] From dbd614f5049e49d66c477d598b185baf38d936ab Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sat, 11 Feb 2017 02:13:06 -0400 Subject: [PATCH 20/32] Fix typo --- mayan/apps/motd/tests/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mayan/apps/motd/tests/test_api.py b/mayan/apps/motd/tests/test_api.py index 6cb6e8bdfa..84efa6c422 100644 --- a/mayan/apps/motd/tests/test_api.py +++ b/mayan/apps/motd/tests/test_api.py @@ -70,7 +70,7 @@ class MOTDAPITestCase(APITestCase): response.data['label'], TEST_LABEL ) - def test_message_path_view(self): + def test_message_patch_view(self): message = self._create_message() self.client.patch( From a3959aaf79a8c0e6da6b3746e95f796bfafd20eb Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 12 Feb 2017 01:16:58 -0400 Subject: [PATCH 21/32] Add resolved smart link API views. Add Smart link manager method .get_for(document). --- docs/releases/2.1.8.rst | 2 +- mayan/apps/linking/api_views.py | 170 ++++++++++++++++++++++++++- mayan/apps/linking/managers.py | 8 ++ mayan/apps/linking/models.py | 3 + mayan/apps/linking/serializers.py | 65 ++++++++++ mayan/apps/linking/tests/literals.py | 2 +- mayan/apps/linking/tests/test_api.py | 50 ++++++++ mayan/apps/linking/urls.py | 23 +++- mayan/apps/linking/views.py | 4 +- 9 files changed, 316 insertions(+), 11 deletions(-) create mode 100644 mayan/apps/linking/managers.py diff --git a/docs/releases/2.1.8.rst b/docs/releases/2.1.8.rst index 196d21b395..b315527f7e 100644 --- a/docs/releases/2.1.8.rst +++ b/docs/releases/2.1.8.rst @@ -20,7 +20,7 @@ Changes - Add Django GPG API endpoints for singing keys. - Add API endpoints for the document states app. - Add API endpoints for the messsage of the day (MOTD) app. - +- Add Smart link API endpoints. Removals -------- diff --git a/mayan/apps/linking/api_views.py b/mayan/apps/linking/api_views.py index 931ea56119..dd1ca25476 100644 --- a/mayan/apps/linking/api_views.py +++ b/mayan/apps/linking/api_views.py @@ -6,6 +6,8 @@ from django.shortcuts import get_object_or_404 from rest_framework import generics from acls.models import AccessControlList +from documents.models import Document +from documents.permissions import permission_document_view from permissions import Permission from rest_api.filters import MayanObjectPermissionsFilter from rest_api.permissions import MayanPermission @@ -15,7 +17,159 @@ from .permissions import ( permission_smart_link_create, permission_smart_link_delete, permission_smart_link_edit, permission_smart_link_view ) -from .serializers import SmartLinkConditionSerializer, SmartLinkSerializer +from .serializers import ( + ResolvedSmartLinkDocumentSerializer, ResolvedSmartLinkSerializer, + SmartLinkConditionSerializer, SmartLinkSerializer, + WritableSmartLinkSerializer +) + + +class APIResolvedSmartLinkDocumentListView(generics.ListAPIView): + filter_backends = (MayanObjectPermissionsFilter,) + mayan_object_permissions = {'GET': (permission_document_view,)} + permission_classes = (MayanPermission,) + serializer_class = ResolvedSmartLinkDocumentSerializer + + def get(self, *args, **kwargs): + """ + Returns a list of the smart link documents that apply to the document. + """ + return super(APIResolvedSmartLinkDocumentListView, self).get( + *args, **kwargs + ) + + def get_document(self): + document = get_object_or_404(Document, pk=self.kwargs['pk']) + + try: + Permission.check_permissions( + self.request.user, (permission_document_view,) + ) + except PermissionDenied: + AccessControlList.objects.check_access( + permission_document_view, self.request.user, document + ) + + return document + + def get_smart_link(self): + smart_link = get_object_or_404( + SmartLink.objects.get_for(document=self.get_document()), + pk=self.kwargs['smart_link_pk'] + ) + + try: + Permission.check_permissions( + self.request.user, (permission_smart_link_view,) + ) + except PermissionDenied: + AccessControlList.objects.check_access( + permission_smart_link_view, self.request.user, smart_link + ) + + return smart_link + + def get_serializer_context(self): + """ + Extra context provided to the serializer class. + """ + return { + 'document': self.get_document(), + 'format': self.format_kwarg, + 'request': self.request, + 'smart_link': self.get_smart_link(), + 'view': self + } + + def get_queryset(self): + return self.get_smart_link().get_linked_document_for( + document=self.get_document() + ) + + +class APIResolvedSmartLinkView(generics.RetrieveAPIView): + filter_backends = (MayanObjectPermissionsFilter,) + lookup_url_kwarg = 'smart_link_pk' + mayan_object_permissions = {'GET': (permission_smart_link_view,)} + permission_classes = (MayanPermission,) + serializer_class = ResolvedSmartLinkSerializer + + def get(self, *args, **kwargs): + """ + Return the details of the selected resolved smart link. + """ + return super(APIResolvedSmartLinkView, self).get(*args, **kwargs) + + def get_document(self): + document = get_object_or_404(Document, pk=self.kwargs['pk']) + + try: + Permission.check_permissions( + self.request.user, (permission_document_view,) + ) + except PermissionDenied: + AccessControlList.objects.check_access( + permission_document_view, self.request.user, document + ) + + return document + + def get_serializer_context(self): + """ + Extra context provided to the serializer class. + """ + return { + 'document': self.get_document(), + 'format': self.format_kwarg, + 'request': self.request, + 'view': self + } + + def get_queryset(self): + return SmartLink.objects.get_for(document=self.get_document()) + + +class APIResolvedSmartLinkListView(generics.ListAPIView): + filter_backends = (MayanObjectPermissionsFilter,) + mayan_object_permissions = {'GET': (permission_smart_link_view,)} + permission_classes = (MayanPermission,) + serializer_class = ResolvedSmartLinkSerializer + + def get(self, *args, **kwargs): + """ + Returns a list of the smart links that apply to the document. + """ + return super(APIResolvedSmartLinkListView, self).get(*args, **kwargs) + + def get_document(self): + document = get_object_or_404(Document, pk=self.kwargs['pk']) + + try: + Permission.check_permissions( + self.request.user, (permission_document_view,) + ) + except PermissionDenied: + AccessControlList.objects.check_access( + permission_document_view, self.request.user, document + ) + + return document + + def get_serializer_context(self): + """ + Extra context provided to the serializer class. + """ + return { + 'document': self.get_document(), + 'format': self.format_kwarg, + 'request': self.request, + 'view': self + } + + def get_queryset(self): + return SmartLink.objects.filter( + document_types=self.get_document().document_type + ) class APISmartLinkConditionListView(generics.ListCreateAPIView): @@ -139,7 +293,6 @@ class APISmartLinkListView(generics.ListCreateAPIView): mayan_view_permissions = {'POST': (permission_smart_link_create,)} permission_classes = (MayanPermission,) queryset = SmartLink.objects.all() - serializer_class = SmartLinkSerializer def get(self, *args, **kwargs): """ @@ -148,6 +301,12 @@ class APISmartLinkListView(generics.ListCreateAPIView): return super(APISmartLinkListView, self).get(*args, **kwargs) + def get_serializer_class(self): + if self.request.method == 'GET': + return SmartLinkSerializer + else: + return WritableSmartLinkSerializer + def post(self, *args, **kwargs): """ Create a new smart link. @@ -165,7 +324,6 @@ class APISmartLinkView(generics.RetrieveUpdateDestroyAPIView): 'PUT': (permission_smart_link_edit,) } queryset = SmartLink.objects.all() - serializer_class = SmartLinkSerializer def delete(self, *args, **kwargs): """ @@ -181,6 +339,12 @@ class APISmartLinkView(generics.RetrieveUpdateDestroyAPIView): return super(APISmartLinkView, self).get(*args, **kwargs) + def get_serializer_class(self): + if self.request.method == 'GET': + return SmartLinkSerializer + else: + return WritableSmartLinkSerializer + def patch(self, *args, **kwargs): """ Edit the selected smart link. diff --git a/mayan/apps/linking/managers.py b/mayan/apps/linking/managers.py new file mode 100644 index 0000000000..e21ce0364c --- /dev/null +++ b/mayan/apps/linking/managers.py @@ -0,0 +1,8 @@ +from django.db import models + + +class SmartLinkManager(models.Manager): + def get_for(self, document): + return self.filter( + document_types=document.document_type, enabled=True + ) diff --git a/mayan/apps/linking/models.py b/mayan/apps/linking/models.py index 776f84fbe6..22e599d50d 100644 --- a/mayan/apps/linking/models.py +++ b/mayan/apps/linking/models.py @@ -11,6 +11,7 @@ from documents.models import Document, DocumentType from .literals import ( INCLUSION_AND, INCLUSION_CHOICES, INCLUSION_OR, OPERATOR_CHOICES ) +from .managers import SmartLinkManager @python_2_unicode_compatible @@ -29,6 +30,8 @@ class SmartLink(models.Model): DocumentType, verbose_name=_('Document types') ) + objects = SmartLinkManager() + def __str__(self): return self.label diff --git a/mayan/apps/linking/serializers.py b/mayan/apps/linking/serializers.py index 4e94a3dc36..23a0db21ff 100644 --- a/mayan/apps/linking/serializers.py +++ b/mayan/apps/linking/serializers.py @@ -3,6 +3,8 @@ from __future__ import absolute_import, unicode_literals from rest_framework import serializers from rest_framework.reverse import reverse +from documents.serializers import DocumentSerializer + from .models import SmartLink, SmartLinkCondition @@ -48,3 +50,66 @@ class SmartLinkSerializer(serializers.HyperlinkedModelSerializer): 'conditions_url', 'dynamic_label', 'enabled', 'label', 'id', 'url' ) model = SmartLink + + +class ResolvedSmartLinkDocumentSerializer(DocumentSerializer): + resolved_smart_link_url = serializers.SerializerMethodField() + + class Meta(DocumentSerializer.Meta): + fields = DocumentSerializer.Meta.fields + ( + 'resolved_smart_link_url', + ) + read_only_fields = DocumentSerializer.Meta.fields + + def get_resolved_smart_link_url(self, instance): + return reverse( + 'rest_api:resolvedsmartlink-detail', args=( + self.context['document'].pk, self.context['smart_link'].pk + ), request=self.context['request'], + format=self.context['format'] + ) + + +class ResolvedSmartLinkSerializer(SmartLinkSerializer): + resolved_dynamic_label = serializers.SerializerMethodField() + resolved_smart_link_url = serializers.SerializerMethodField() + resolved_documents_url = serializers.SerializerMethodField() + + class Meta(SmartLinkSerializer.Meta): + fields = SmartLinkSerializer.Meta.fields + ( + 'resolved_dynamic_label', 'resolved_smart_link_url', + 'resolved_documents_url' + ) + read_only_fields = SmartLinkSerializer.Meta.fields + + def get_resolved_documents_url(self, instance): + return reverse( + 'rest_api:resolvedsmartlinkdocument-list', + args=(self.context['document'].pk, instance.pk,), + request=self.context['request'], format=self.context['format'] + ) + + def get_resolved_dynamic_label(self, instance): + return instance.get_dynamic_label(document=self.context['document']) + + def get_resolved_smart_link_url(self, instance): + return reverse( + 'rest_api:resolvedsmartlink-detail', + args=(self.context['document'].pk, instance.pk,), + request=self.context['request'], format=self.context['format'] + ) + + +class WritableSmartLinkSerializer(serializers.ModelSerializer): + conditions_url = serializers.HyperlinkedIdentityField( + view_name='rest_api:smartlinkcondition-list' + ) + + class Meta: + extra_kwargs = { + 'url': {'view_name': 'rest_api:smartlink-detail'}, + } + fields = ( + 'conditions_url', 'dynamic_label', 'enabled', 'label', 'id', 'url' + ) + model = SmartLink diff --git a/mayan/apps/linking/tests/literals.py b/mayan/apps/linking/tests/literals.py index e527654e91..6c00f08366 100644 --- a/mayan/apps/linking/tests/literals.py +++ b/mayan/apps/linking/tests/literals.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals TEST_SMART_LINK_CONDITION_FOREIGN_DOCUMENT_DATA = 'label' -TEST_SMART_LINK_CONDITION_EXPRESSION = '\'test\'' +TEST_SMART_LINK_CONDITION_EXPRESSION = 'sample' TEST_SMART_LINK_CONDITION_EXPRESSION_EDITED = '\'test edited\'' TEST_SMART_LINK_CONDITION_OPERATOR = 'icontains' TEST_SMART_LINK_DYNAMIC_LABEL = '{{ document.label }}' diff --git a/mayan/apps/linking/tests/test_api.py b/mayan/apps/linking/tests/test_api.py index 8f334b483b..4e795f830e 100644 --- a/mayan/apps/linking/tests/test_api.py +++ b/mayan/apps/linking/tests/test_api.py @@ -163,6 +163,56 @@ class SmartLinkConditionAPITestCase(APITestCase): operator=TEST_SMART_LINK_CONDITION_OPERATOR ) + def test_resolved_smart_link_detail_view(self): + self._create_document_type() + self._create_smart_link() + self._create_smart_link_condition() + self._create_document() + + response = self.client.get( + reverse( + 'rest_api:resolvedsmartlink-detail', + args=(self.document.pk, self.smart_link.pk) + ) + ) + + self.assertEqual( + response.data['label'], TEST_SMART_LINK_LABEL + ) + + def test_resolved_smart_link_list_view(self): + self._create_document_type() + self._create_smart_link() + self._create_smart_link_condition() + self._create_document() + + response = self.client.get( + reverse( + 'rest_api:resolvedsmartlink-list', args=(self.document.pk,) + ) + ) + + self.assertEqual( + response.data['results'][0]['label'], TEST_SMART_LINK_LABEL + ) + + def test_resolved_smart_link_document_list_view(self): + self._create_document_type() + self._create_smart_link() + self._create_smart_link_condition() + self._create_document() + + response = self.client.get( + reverse( + 'rest_api:resolvedsmartlinkdocument-list', + args=(self.document.pk, self.smart_link.pk) + ) + ) + + self.assertEqual( + response.data['results'][0]['label'], self.document.label + ) + def test_smart_link_condition_create_view(self): self._create_document_type() self._create_smart_link() diff --git a/mayan/apps/linking/urls.py b/mayan/apps/linking/urls.py index 3876fb73aa..272a433e87 100644 --- a/mayan/apps/linking/urls.py +++ b/mayan/apps/linking/urls.py @@ -3,8 +3,9 @@ from __future__ import unicode_literals from django.conf.urls import patterns, url from .api_views import ( - APISmartLinkListView, APISmartLinkView, APISmartLinkConditionListView, - APISmartLinkConditionView + APIResolvedSmartLinkView, APIResolvedSmartLinkDocumentListView, + APIResolvedSmartLinkListView, APISmartLinkListView, APISmartLinkView, + APISmartLinkConditionListView, APISmartLinkConditionView ) from .views import ( DocumentSmartLinkListView, ResolvedSmartLinkView, @@ -68,7 +69,8 @@ urlpatterns = patterns( api_urls = [ url( - r'^smart_links/$', APISmartLinkListView.as_view(), name='smartlink-list' + r'^smart_links/$', APISmartLinkListView.as_view(), + name='smartlink-list' ), url( r'^smart_links/(?P[0-9]+)/$', APISmartLinkView.as_view(), @@ -83,4 +85,19 @@ api_urls = [ APISmartLinkConditionView.as_view(), name='smartlinkcondition-detail' ), + url( + r'^documents/(?P[0-9]+)/resolved_smart_links/$', + APIResolvedSmartLinkListView.as_view(), + name='resolvedsmartlink-list' + ), + url( + r'^documents/(?P[0-9]+)/resolved_smart_links/(?P[0-9]+)/$', + APIResolvedSmartLinkView.as_view(), + name='resolvedsmartlink-detail' + ), + url( + r'^documents/(?P[0-9]+)/resolved_smart_links/(?P[0-9]+)/documents/$', + APIResolvedSmartLinkDocumentListView.as_view(), + name='resolvedsmartlinkdocument-list' + ), ] diff --git a/mayan/apps/linking/views.py b/mayan/apps/linking/views.py index c7e03b0482..a04fbc3efa 100644 --- a/mayan/apps/linking/views.py +++ b/mayan/apps/linking/views.py @@ -174,9 +174,7 @@ class DocumentSmartLinkListView(SmartLinkListView): } def get_smart_link_queryset(self): - return ResolvedSmartLink.objects.filter( - document_types=self.document.document_type, enabled=True - ) + return ResolvedSmartLink.objects.get_for(document=self.document) class SmartLinkCreateView(SingleObjectCreateView): From 92ac4dc2f77a8391fc73780856cfa3a694ac9fe9 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 12 Feb 2017 03:11:24 -0400 Subject: [PATCH 22/32] Add writable versions of the Document and Document Type serializers (GitLab issues #348 and #349). --- docs/releases/2.1.8.rst | 4 +- mayan/apps/documents/api_views.py | 31 ++++++++++-- mayan/apps/documents/serializers.py | 70 ++++++++++++++++++++++++-- mayan/apps/documents/tests/test_api.py | 3 +- 4 files changed, 99 insertions(+), 9 deletions(-) diff --git a/docs/releases/2.1.8.rst b/docs/releases/2.1.8.rst index b315527f7e..f3b89980fa 100644 --- a/docs/releases/2.1.8.rst +++ b/docs/releases/2.1.8.rst @@ -21,6 +21,7 @@ Changes - Add API endpoints for the document states app. - Add API endpoints for the messsage of the day (MOTD) app. - Add Smart link API endpoints. +- Add writable versions of the Document and Document Type serializers (GitLab issues #348 and #349). Removals -------- @@ -75,6 +76,7 @@ Backward incompatible changes Bugs fixed or issues closed =========================== -* None +* `GitLab issue #348 `_ REST API: Document version comments are not getting updated +* `GitLab issue #349 `_ REST API: Document Label, Description are not able to update .. _PyPI: https://pypi.python.org/pypi/mayan-edms/ diff --git a/mayan/apps/documents/api_views.py b/mayan/apps/documents/api_views.py index c33ecfd8e7..a21d20278e 100644 --- a/mayan/apps/documents/api_views.py +++ b/mayan/apps/documents/api_views.py @@ -31,7 +31,9 @@ from .serializers import ( DocumentPageSerializer, DocumentSerializer, DocumentTypeSerializer, DocumentVersionSerializer, DocumentVersionRevertSerializer, NewDocumentSerializer, - NewDocumentVersionSerializer, RecentDocumentSerializer + NewDocumentVersionSerializer, RecentDocumentSerializer, + WritableDocumentSerializer, WritableDocumentTypeSerializer, + WritableDocumentVersionSerializer ) logger = logging.getLogger(__name__) @@ -190,7 +192,6 @@ class APIDocumentView(generics.RetrieveUpdateDestroyAPIView): } permission_classes = (MayanPermission,) queryset = Document.objects.all() - serializer_class = DocumentSerializer def delete(self, *args, **kwargs): """ @@ -206,6 +207,12 @@ class APIDocumentView(generics.RetrieveUpdateDestroyAPIView): return super(APIDocumentView, self).get(*args, **kwargs) + def get_serializer_class(self): + if self.request.method == 'GET': + return DocumentSerializer + else: + return WritableDocumentSerializer + def patch(self, *args, **kwargs): """ Edit the properties of the selected document. @@ -289,6 +296,12 @@ class APIDocumentTypeListView(generics.ListCreateAPIView): return super(APIDocumentTypeListView, self).get(*args, **kwargs) + def get_serializer_class(self): + if self.request.method == 'GET': + return DocumentTypeSerializer + else: + return WritableDocumentTypeSerializer + def post(self, *args, **kwargs): """ Create a new document type. @@ -310,7 +323,6 @@ class APIDocumentTypeView(generics.RetrieveUpdateDestroyAPIView): } permission_classes = (MayanPermission,) queryset = DocumentType.objects.all() - serializer_class = DocumentTypeSerializer def delete(self, *args, **kwargs): """ @@ -326,6 +338,12 @@ class APIDocumentTypeView(generics.RetrieveUpdateDestroyAPIView): return super(APIDocumentTypeView, self).get(*args, **kwargs) + def get_serializer_class(self): + if self.request.method == 'GET': + return DocumentTypeSerializer + else: + return WritableDocumentTypeSerializer + def patch(self, *args, **kwargs): """ Edit the properties of the selected document type. @@ -436,7 +454,12 @@ class APIDocumentVersionView(generics.RetrieveUpdateAPIView): mayan_permission_attribute_check = 'document' permission_classes = (MayanPermission,) queryset = DocumentVersion.objects.all() - serializer_class = DocumentVersionSerializer + + def get_serializer_class(self): + if self.request.method == 'GET': + return DocumentVersionSerializer + else: + return WritableDocumentVersionSerializer def patch(self, *args, **kwargs): """ diff --git a/mayan/apps/documents/serializers.py b/mayan/apps/documents/serializers.py index 4a4a729e46..fd3a80e022 100644 --- a/mayan/apps/documents/serializers.py +++ b/mayan/apps/documents/serializers.py @@ -51,9 +51,27 @@ class DocumentTypeSerializer(serializers.HyperlinkedModelSerializer): ) documents_count = serializers.SerializerMethodField() + class Meta: + extra_kwargs = { + 'url': {'view_name': 'rest_api:documenttype-detail'}, + } + fields = ( + 'delete_time_period', 'delete_time_unit', 'documents_url', + 'documents_count', 'id', 'label', 'trash_time_period', + 'trash_time_unit', 'url' + ) + model = DocumentType + def get_documents_count(self, obj): return obj.documents.count() + +class WritableDocumentTypeSerializer(serializers.ModelSerializer): + documents_url = serializers.HyperlinkedIdentityField( + view_name='rest_api:documenttype-document-list', + ) + documents_count = serializers.SerializerMethodField() + class Meta: extra_kwargs = { 'url': {'view_name': 'rest_api:documenttype-detail'}, @@ -65,6 +83,9 @@ class DocumentTypeSerializer(serializers.HyperlinkedModelSerializer): ) model = DocumentType + def get_documents_count(self, obj): + return obj.documents.count() + class DocumentVersionSerializer(serializers.HyperlinkedModelSerializer): pages = DocumentPageSerializer(many=True, required=False, read_only=True) @@ -82,6 +103,26 @@ class DocumentVersionSerializer(serializers.HyperlinkedModelSerializer): read_only_fields = ('document', 'file') +class WritableDocumentVersionSerializer(serializers.ModelSerializer): + document = serializers.HyperlinkedIdentityField( + view_name='rest_api:document-detail' + ) + pages = DocumentPageSerializer(many=True, required=False, read_only=True) + revert = serializers.HyperlinkedIdentityField( + view_name='rest_api:documentversion-revert' + ) + url = serializers.HyperlinkedIdentityField( + view_name='rest_api:documentversion-detail' + ) + + class Meta: + extra_kwargs = { + 'file': {'use_url': False}, + } + model = DocumentVersion + read_only_fields = ('document', 'file') + + class DocumentVersionRevertSerializer(DocumentVersionSerializer): class Meta(DocumentVersionSerializer.Meta): read_only_fields = ('comment', 'document',) @@ -136,9 +177,6 @@ class DocumentSerializer(serializers.HyperlinkedModelSerializer): view_name='rest_api:document-version-list', ) - def get_document_type_label(self, instance): - return instance.document_type.label - class Meta: extra_kwargs = { 'document_type': {'view_name': 'rest_api:documenttype-detail'}, @@ -152,6 +190,32 @@ class DocumentSerializer(serializers.HyperlinkedModelSerializer): model = Document read_only_fields = ('document_type',) + def get_document_type_label(self, instance): + return instance.document_type.label + + +class WritableDocumentSerializer(serializers.ModelSerializer): + document_type_label = serializers.SerializerMethodField() + latest_version = DocumentVersionSerializer(many=False, read_only=True) + versions = serializers.HyperlinkedIdentityField( + view_name='rest_api:document-version-list', + ) + url = serializers.HyperlinkedIdentityField( + view_name='rest_api:document-detail', + ) + + class Meta: + fields = ( + 'date_added', 'description', 'document_type', + 'document_type_label', 'id', 'label', 'language', + 'latest_version', 'url', 'uuid', 'versions', + ) + model = Document + read_only_fields = ('document_type',) + + def get_document_type_label(self, instance): + return instance.document_type.label + class NewDocumentSerializer(serializers.ModelSerializer): file = serializers.FileField(write_only=True) diff --git a/mayan/apps/documents/tests/test_api.py b/mayan/apps/documents/tests/test_api.py index fc1a8225c0..ce77b8a6ae 100644 --- a/mayan/apps/documents/tests/test_api.py +++ b/mayan/apps/documents/tests/test_api.py @@ -49,12 +49,13 @@ class DocumentTypeAPITestCase(APITestCase): def test_document_type_create(self): self.assertEqual(DocumentType.objects.all().count(), 0) - self.client.post( + response = self.client.post( reverse('rest_api:documenttype-list'), data={ 'label': TEST_DOCUMENT_TYPE } ) + self.assertEqual(response.status_code, 201) self.assertEqual(DocumentType.objects.all().count(), 1) self.assertEqual( DocumentType.objects.all().first().label, TEST_DOCUMENT_TYPE From 36db1f4e063ff60edddec1320c72c9190738da27 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Fri, 3 Feb 2017 16:18:58 -0400 Subject: [PATCH 23/32] Return metadata type lookup values as list of unicode not list of strings. Fixed GitLab issue #310, thank for @fordguo for the find and fix suggestion. --- docs/releases/2.1.8.rst | 1 + mayan/apps/metadata/models.py | 5 +++-- mayan/apps/metadata/tests/test_models.py | 13 +++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/releases/2.1.8.rst b/docs/releases/2.1.8.rst index f3b89980fa..82aa846878 100644 --- a/docs/releases/2.1.8.rst +++ b/docs/releases/2.1.8.rst @@ -76,6 +76,7 @@ Backward incompatible changes Bugs fixed or issues closed =========================== +* `GitLab issue #310 `_ Metadata's lookup with chinese messages when new document * `GitLab issue #348 `_ REST API: Document version comments are not getting updated * `GitLab issue #349 `_ REST API: Document Label, Description are not able to update diff --git a/mayan/apps/metadata/models.py b/mayan/apps/metadata/models.py index 8ff7609517..b6aaeab76e 100644 --- a/mayan/apps/metadata/models.py +++ b/mayan/apps/metadata/models.py @@ -5,7 +5,7 @@ import shlex from django.core.exceptions import ValidationError from django.db import models from django.template import Context, Template -from django.utils.encoding import python_2_unicode_compatible +from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils.module_loading import import_string from django.utils.translation import ugettext_lazy as _ @@ -97,7 +97,7 @@ class MetadataType(models.Model): splitter.whitespace = ','.encode('utf-8') splitter.whitespace_split = True splitter.commenters = ''.encode('utf-8') - return list(splitter) + return [force_text(e) for e in splitter] def get_default_value(self): template = Template(self.default) @@ -126,6 +126,7 @@ class MetadataType(models.Model): if self.lookup: lookup_options = self.get_lookup_values() + if value and value not in lookup_options: raise ValidationError( _('Value is not one of the provided options.') diff --git a/mayan/apps/metadata/tests/test_models.py b/mayan/apps/metadata/tests/test_models.py index 3d96286a64..91990bd168 100644 --- a/mayan/apps/metadata/tests/test_models.py +++ b/mayan/apps/metadata/tests/test_models.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.core.files.base import File @@ -175,3 +176,15 @@ class MetadataTestCase(TestCase): self.assertTrue( self.metadata_type.get_required_for(self.document_type) ) + + def test_unicode_lookup(self): + # Should NOT return a ValidationError, otherwise test fails + self.metadata_type.lookup = '测试1,测试2,test1,test2' + self.metadata_type.save() + self.metadata_type.validate_value(document_type=None, value='测试1') + + def test_non_unicode_lookup(self): + # Should NOT return a ValidationError, otherwise test fails + self.metadata_type.lookup = 'test1,test2' + self.metadata_type.save() + self.metadata_type.validate_value(document_type=None, value='test1') From 00a2fce71d12073f9294816d245acc17b58529b9 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 12 Feb 2017 13:03:01 -0400 Subject: [PATCH 24/32] Add changelog. Bump version to 2.1.8. --- HISTORY.rst | 13 +++++++++++++ mayan/__init__.py | 4 ++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index 3573e665ac..70103fdc22 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,3 +1,16 @@ +2.1.8 (2017-02-12) +================== +- Fixes in the trashed document API endpoints. +- Improved tags API PUT and PATCH endpoints. +- Bulk document adding when creating and editing tags. +- The version of django-mptt is preserved in case mayan-cabinets is installed. +- Add Django GPG API endpoints for singing keys. +- Add API endpoints for the document states (workflows) app. +- Add API endpoints for the messsage of the day (MOTD) app. +- Add Smart link API endpoints. +- Add writable versions of the Document and Document Type serializers (GitLab issues #348 and #349). +- Close GitLab issue #310 "Metadata's lookup with chinese messages when new document" + 2.1.7 (2017-02-01) ================== - Improved user management API endpoints. diff --git a/mayan/__init__.py b/mayan/__init__.py index 43c74b02a1..2e0898e7c8 100644 --- a/mayan/__init__.py +++ b/mayan/__init__.py @@ -1,8 +1,8 @@ from __future__ import unicode_literals __title__ = 'Mayan EDMS' -__version__ = '2.1.7' -__build__ = 0x020107 +__version__ = '2.1.8' +__build__ = 0x020108 __author__ = 'Roberto Rosario' __author_email__ = 'roberto.rosario@mayan-edms.com' __description__ = 'Free Open Source Electronic Document Management System' From 55a905236b1fe9c3bfd2c92e018d81f37d4b69b6 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 12 Feb 2017 15:27:49 -0400 Subject: [PATCH 25/32] Update README file. Switch format to markdown. --- README.md | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ README.rst | 65 ------------------------------------------- 2 files changed, 82 insertions(+), 65 deletions(-) create mode 100644 README.md delete mode 100644 README.rst diff --git a/README.md b/README.md new file mode 100644 index 0000000000..ebc381f84d --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +[![pypi][pypi]][pypi-url] +[![builds][builds]][builds-url] +[![coverage][cover]][cover-url] +![python][python] +![license][license] + +[pypi]: http://img.shields.io/pypi/v/mayan-edms.svg +[pypi-url]: http://badge.fury.io/py/mayan-edms + +[builds]: https://gitlab.com/mayan-edms/mayan-edms/badges/master/build.svg +[builds-url]: https://gitlab.com/mayan-edms/mayan-edms/pipelines + +[cover]: https://codecov.io/gitlab/mayan-edms/mayan-edms/coverage.svg?branch=master +[cover-url]: https://codecov.io/gitlab/mayan-edms/mayan-edms?branch=master + +[python]: https://img.shields.io/pypi/pyversions/mayan-edms.svg +[python-url]: https://img.shields.io/pypi/l/mayan-edms.svg?style=flat + +[license]: https://img.shields.io/pypi/l/mayan-edms.svg?style=flat +[license-url]: https://img.shields.io/pypi/l/mayan-edms.svg?style=flat + + +
+ + + +
+
+

+ Mayan EDMS is a document management system. Its main purpose is to store, + introspect, and categorize files, with a strong emphasis on preserving the + contextual and business information of documents. It can also OCR, preview, + label, sign, send, and receive thoses files. Other features of interest + are its workflow system, role based access control, and REST API. +

+ +

+ +

+ +
+ +

Installation

+ +The installation procedure uses the Docker container manager (docker.com). Make sure Docker is properly installed and working before attempting to install Mayan EDMS. + +Step 1- Initialize the installation + +```bash +docker run --rm -v mayan_media:/var/lib/mayan \ +-v mayan_settings:/etc/mayan mayanedms/mayanedms mayan:init +``` + +Step 2- Deploy a container + +```bash +docker run -d --name mayan-edms --restart=always -p 80:80 \ +-v mayan_media:/var/lib/mayan -v mayan_settings:/etc/mayan mayanedms/mayanedms +``` + +Step 3- Open a browser and go to http://localhost + + +

Important links

+ +Homepage + +Videos + +Documentation + +Paid support + +Community forum + +Community forum archive + +Source code, issues, bugs + +Plug-ins, other related projects + +Translations diff --git a/README.rst b/README.rst deleted file mode 100644 index 426c881519..0000000000 --- a/README.rst +++ /dev/null @@ -1,65 +0,0 @@ -|PyPI badge| |Build Status| |Coverage badge| |Documentation| |License badge| |Python version| - -|Logo| - -Description ------------ - -Free Open Source Electronic Document Management System. - -`Website`_ - -`Video demostration`_ - -`Documentation`_ - -`Translations`_ - -`Mailing list (via Google Groups)`_ - -|Animation| - -License -------- - -This project is open sourced under `Apache 2.0 License`_. - -Installation ------------- - -To install Mayan EDMS, simply do: - -.. code-block:: bash - - $ virtualenv venv - $ source venv/bin/activate - (venv) $ pip install mayan-edms - (venv) $ mayan-edms.py initialsetup - (venv) $ mayan-edms.py runserver - -Point your browser to 127.0.0.1:8000 and use the automatically created admin -account. - - -.. _Website: http://www.mayan-edms.com -.. _Video demostration: http://bit.ly/pADNXv -.. _Documentation: http://readthedocs.org/docs/mayan/en/latest/ -.. _Translations: https://www.transifex.com/projects/p/mayan-edms/ -.. _Mailing list (via Google Groups): http://groups.google.com/group/mayan-edms -.. _Apache 2.0 License: https://www.apache.org/licenses/LICENSE-2.0.txt - -.. |Build Status| image:: https://gitlab.com/mayan-edms/mayan-edms/badges/master/build.svg - :target: https://gitlab.com/mayan-edms/mayan-edms/commits/master -.. |Logo| image:: https://gitlab.com/mayan-edms/mayan-edms/raw/master/docs/_static/mayan_logo.png -.. |Animation| image:: https://gitlab.com/mayan-edms/mayan-edms/raw/master/docs/_static/overview.gif -.. |PyPI badge| image:: http://img.shields.io/pypi/v/mayan-edms.svg?style=flat - :target: http://badge.fury.io/py/mayan-edms -.. |License badge| image:: https://img.shields.io/pypi/l/mayan-edms.svg?style=flat -.. |Analytics| image:: https://ga-beacon.appspot.com/UA-52965619-2/mayan-edms/readme?pixel -.. |Coverage badge| image:: https://codecov.io/gitlab/mayan-edms/mayan-edms/coverage.svg?branch=master - :target: https://codecov.io/gitlab/mayan-edms/mayan-edms?branch=master -.. |Documentation| image:: https://readthedocs.org/projects/mayan/badge/?version=latest - :target: http://mayan.readthedocs.io/en/latest -.. |Python version| image:: https://img.shields.io/pypi/pyversions/mayan-edms.svg - -|Analytics| From 9c7ba66d1f6570044eeaf4da3349f28345ae4596 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 12 Feb 2017 22:34:12 -0400 Subject: [PATCH 26/32] Update MANIFEST to include the markdown version of the README. Convert the markdown README to .rst for PyPI. Add pypandoc to the development requirements. --- MANIFEST.in | 2 +- README.md | 24 +++++++++--------------- requirements/development.txt | 2 ++ setup.py | 9 +++++++-- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 4c59f9ed98..c8891a699f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ -include README.rst LICENSE HISTORY.rst +include README.md LICENSE HISTORY.rst recursive-include mayan *.txt *.html *.css *.ico *.png *.jpg *.js *.po *.mo *.ttf *.woff *.woff2 LICENSE global-exclude mayan/settings/local.py mayan/settings/travis/* mayan/media/* diff --git a/README.md b/README.md index ebc381f84d..d75efe7909 100644 --- a/README.md +++ b/README.md @@ -63,20 +63,14 @@ Step 3- Open a browser and go to http://localhost

Important links

-Homepage -Videos +- [Homepage](http://www.mayan-edms.com) +- [Videos](https://www.youtube.com/channel/UCJOOXHP1MJ9lVA7d8ZTlHPw) +- [Documentation](http://mayan.readthedocs.io/en/stable/) +- [Paid support](http://www.mayan-edms.com/providers/) +- [Community forum](https://groups.google.com/forum/#!forum/mayan-edms) +- [Community forum archive](http://mayan-edms.1003.x6.nabble.com/) +- [Source code, issues, bugs](https://gitlab.com/mayan-edms/mayan-edms) +- [Plug-ins, other related projects](https://gitlab.com/mayan-edms/) +- [Translations](https://www.transifex.com/rosarior/mayan-edms/) -Documentation - -Paid support - -Community forum - -Community forum archive - -Source code, issues, bugs - -Plug-ins, other related projects - -Translations diff --git a/requirements/development.txt b/requirements/development.txt index 46a154d3c8..8fd957cd99 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -8,6 +8,8 @@ django-rosetta==0.7.8 ipython==4.0.3 +pypandoc==1.3.3 + transifex-client==0.12.2 wheel==0.26.0 diff --git a/setup.py b/setup.py index 29aff914e9..76bdb6880c 100644 --- a/setup.py +++ b/setup.py @@ -91,8 +91,13 @@ pytz==2015.4 sh==1.11 """.split() -with open('README.rst') as f: - readme = f.read() +try: + import pypandoc + readme = pypandoc.convert_file('README.md', 'rst') +except (IOError, ImportError): + with open('README.md') as f: + readme = f.read() + with open('HISTORY.rst') as f: history = f.read() From 26f17b6ede3823163558f4c16983bdb6a5bccad6 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 12 Feb 2017 23:08:11 -0400 Subject: [PATCH 27/32] Workaround long standing pypa wheel bug #99 https://bitbucket.org/pypa/wheel/issues/99/cannot-exclude-directory --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index c9dc8bc30e..43e43fda7d 100644 --- a/Makefile +++ b/Makefile @@ -94,8 +94,8 @@ sdist: clean python setup.py sdist ls -l dist -wheel: clean - python setup.py bdist_wheel +wheel: clean sdist + pip wheel --no-index --no-deps --wheel-dir dist dist/*.tar.gz ls -l dist From e65b453bc1e3d2e35a18ac05607ae50b2e548a33 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Sun, 12 Feb 2017 23:18:00 -0400 Subject: [PATCH 28/32] Bump version to v.2.1.9. Add changelog and release notes. This is a micro version release to due to the Python Package Index not allowing re-uploads. --- HISTORY.rst | 4 +++ docs/releases/2.1.8.rst | 2 +- docs/releases/2.1.9.rst | 74 +++++++++++++++++++++++++++++++++++++++++ docs/releases/index.rst | 1 + mayan/__init__.py | 4 +-- 5 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 docs/releases/2.1.9.rst diff --git a/HISTORY.rst b/HISTORY.rst index 70103fdc22..19876fd1a9 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,3 +1,7 @@ +2.1.9 (2017-02-13) +================== +- Update make file to Workaround long standing pypa wheel bug #99 + 2.1.8 (2017-02-12) ================== - Fixes in the trashed document API endpoints. diff --git a/docs/releases/2.1.8.rst b/docs/releases/2.1.8.rst index 82aa846878..ebe1ae8596 100644 --- a/docs/releases/2.1.8.rst +++ b/docs/releases/2.1.8.rst @@ -2,7 +2,7 @@ Mayan EDMS v2.1.8 release notes =============================== -Released: February XX, 2017 +Released: February 12, 2017 What's new ========== diff --git a/docs/releases/2.1.9.rst b/docs/releases/2.1.9.rst new file mode 100644 index 0000000000..f9c3333e86 --- /dev/null +++ b/docs/releases/2.1.9.rst @@ -0,0 +1,74 @@ +=============================== +Mayan EDMS v2.1.9 release notes +=============================== + +Released: February 13, 2017 + +What's new +========== + +This is a micro release equal to the previews version from the user's point of view. +The version number was increase to workaround some issues with the Python +Package Index not allowing re-uploads. + +Changes +------------- + +- Update make file to Workaround long standing pypa wheel bug #99 + +Removals +-------- +* None + +Upgrading from a previous version +--------------------------------- + +Using PIP +~~~~~~~~~ + +Type in the console:: + + $ pip install -U mayan-edms + +the requirements will also be updated automatically. + +Using Git +~~~~~~~~~ + +If you installed Mayan EDMS by cloning the Git repository issue the commands:: + + $ git reset --hard HEAD + $ git pull + +otherwise download the compressed archived and uncompress it overriding the +existing installation. + +Next upgrade/add the new requirements:: + + $ pip install --upgrade -r requirements.txt + +Common steps +~~~~~~~~~~~~ + +Migrate existing database schema with:: + + $ mayan-edms.py performupgrade + +Add new static media:: + + $ mayan-edms.py collectstatic --noinput + +The upgrade procedure is now complete. + + +Backward incompatible changes +============================= + +* None + +Bugs fixed or issues closed +=========================== + +* None + +.. _PyPI: https://pypi.python.org/pypi/mayan-edms/ diff --git a/docs/releases/index.rst b/docs/releases/index.rst index 84cb057583..20f0924247 100644 --- a/docs/releases/index.rst +++ b/docs/releases/index.rst @@ -22,6 +22,7 @@ versions of the documentation contain the release notes for any later releases. .. toctree:: :maxdepth: 1 + 2.1.9 2.1.8 2.1.7 2.1.6 diff --git a/mayan/__init__.py b/mayan/__init__.py index 2e0898e7c8..5b002feecb 100644 --- a/mayan/__init__.py +++ b/mayan/__init__.py @@ -1,8 +1,8 @@ from __future__ import unicode_literals __title__ = 'Mayan EDMS' -__version__ = '2.1.8' -__build__ = 0x020108 +__version__ = '2.1.9' +__build__ = 0x020109 __author__ = 'Roberto Rosario' __author_email__ = 'roberto.rosario@mayan-edms.com' __description__ = 'Free Open Source Electronic Document Management System' From 1f230c843a1c9cd1441ec8936b0260e38ec235e0 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 13 Feb 2017 02:04:49 -0400 Subject: [PATCH 29/32] Call the wheel target instead of executing setup.py --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 43e43fda7d..37a377306d 100644 --- a/Makefile +++ b/Makefile @@ -87,8 +87,8 @@ requirements_testing: # Releases -release: clean - python setup.py sdist bdist_wheel upload +release: clean wheel + python setup.py upload sdist: clean python setup.py sdist From 4469f020a6b7f8bd4e35963e9201709d0d083d29 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 13 Feb 2017 02:42:01 -0400 Subject: [PATCH 30/32] Update Makefile to use twine for releases. Add target to make test releases. --- Makefile | 9 ++++++--- requirements/development.txt | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 37a377306d..c35edc14fc 100644 --- a/Makefile +++ b/Makefile @@ -87,8 +87,13 @@ requirements_testing: # Releases + +test_release: clean wheel + twine upload dist/* -r testpypi + @echo "Test with: pip install -i https://testpypi.python.org/pypi mayan-edms" + release: clean wheel - python setup.py upload + twine upload dist/* -r pypi sdist: clean python setup.py sdist @@ -106,5 +111,3 @@ runserver: shell_plus: ./manage.py shell_plus --settings=mayan.settings.development - - diff --git a/requirements/development.txt b/requirements/development.txt index 8fd957cd99..2169026ca8 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -11,6 +11,7 @@ ipython==4.0.3 pypandoc==1.3.3 transifex-client==0.12.2 +twine==1.8.1 wheel==0.26.0 From fa38b5b1357cbb8b8501cb21993edde5359804dd Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 13 Feb 2017 02:45:28 -0400 Subject: [PATCH 31/32] Add version 2.1.10 changelog and release notes. --- HISTORY.rst | 5 +++ docs/releases/2.1.10.rst | 75 ++++++++++++++++++++++++++++++++++++++++ docs/releases/index.rst | 1 + 3 files changed, 81 insertions(+) create mode 100644 docs/releases/2.1.10.rst diff --git a/HISTORY.rst b/HISTORY.rst index 19876fd1a9..2e7b617934 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,3 +1,8 @@ +2.1.10 (2017-02-13) +================== +- Update Makefile to use twine for releases. +- Add Makefile target to make test releases. + 2.1.9 (2017-02-13) ================== - Update make file to Workaround long standing pypa wheel bug #99 diff --git a/docs/releases/2.1.10.rst b/docs/releases/2.1.10.rst new file mode 100644 index 0000000000..8445af2a09 --- /dev/null +++ b/docs/releases/2.1.10.rst @@ -0,0 +1,75 @@ +=============================== +Mayan EDMS v2.1.10 release notes +=============================== + +Released: February 13, 2017 + +What's new +========== + +This is a micro release equal to the previews version from the user's point of view. +The version number was increase to workaround some issues with the Python +Package Index not allowing re-uploads. + +Changes +------------- + +- Update Makefile to use twine for releases. +- Add Makefile target to make test releases. + +Removals +-------- +* None + +Upgrading from a previous version +--------------------------------- + +Using PIP +~~~~~~~~~ + +Type in the console:: + + $ pip install -U mayan-edms + +the requirements will also be updated automatically. + +Using Git +~~~~~~~~~ + +If you installed Mayan EDMS by cloning the Git repository issue the commands:: + + $ git reset --hard HEAD + $ git pull + +otherwise download the compressed archived and uncompress it overriding the +existing installation. + +Next upgrade/add the new requirements:: + + $ pip install --upgrade -r requirements.txt + +Common steps +~~~~~~~~~~~~ + +Migrate existing database schema with:: + + $ mayan-edms.py performupgrade + +Add new static media:: + + $ mayan-edms.py collectstatic --noinput + +The upgrade procedure is now complete. + + +Backward incompatible changes +============================= + +* None + +Bugs fixed or issues closed +=========================== + +* None + +.. _PyPI: https://pypi.python.org/pypi/mayan-edms/ diff --git a/docs/releases/index.rst b/docs/releases/index.rst index 20f0924247..896a3f1948 100644 --- a/docs/releases/index.rst +++ b/docs/releases/index.rst @@ -22,6 +22,7 @@ versions of the documentation contain the release notes for any later releases. .. toctree:: :maxdepth: 1 + 2.1.10 2.1.9 2.1.8 2.1.7 From cd910e8ae996e725f6653a0b66a5576c20492989 Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 13 Feb 2017 02:46:08 -0400 Subject: [PATCH 32/32] Bump version to 2.1.10 --- mayan/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mayan/__init__.py b/mayan/__init__.py index 5b002feecb..bf64ac09c6 100644 --- a/mayan/__init__.py +++ b/mayan/__init__.py @@ -1,8 +1,8 @@ from __future__ import unicode_literals __title__ = 'Mayan EDMS' -__version__ = '2.1.9' -__build__ = 0x020109 +__version__ = '2.1.10' +__build__ = 0x020110 __author__ = 'Roberto Rosario' __author_email__ = 'roberto.rosario@mayan-edms.com' __description__ = 'Free Open Source Electronic Document Management System'