diff --git a/HISTORY.rst b/HISTORY.rst index 3573e665ac..2e7b617934 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,3 +1,25 @@ +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 + +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/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/Makefile b/Makefile index c9dc8bc30e..c35edc14fc 100644 --- a/Makefile +++ b/Makefile @@ -87,15 +87,20 @@ requirements_testing: # Releases -release: clean - python setup.py sdist bdist_wheel upload + +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 + twine upload dist/* -r pypi 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 @@ -106,5 +111,3 @@ runserver: shell_plus: ./manage.py shell_plus --settings=mayan.settings.development - - diff --git a/README.md b/README.md new file mode 100644 index 0000000000..d75efe7909 --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +[![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](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/) + 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| 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/2.1.8.rst b/docs/releases/2.1.8.rst index 196d21b395..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 ========== @@ -20,7 +20,8 @@ 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. +- Add writable versions of the Document and Document Type serializers (GitLab issues #348 and #349). Removals -------- @@ -75,6 +76,8 @@ Backward incompatible changes Bugs fixed or issues closed =========================== -* None +* `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 .. _PyPI: https://pypi.python.org/pypi/mayan-edms/ 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..896a3f1948 100644 --- a/docs/releases/index.rst +++ b/docs/releases/index.rst @@ -22,6 +22,8 @@ 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 2.1.6 diff --git a/mayan/__init__.py b/mayan/__init__.py index 43c74b02a1..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.7' -__build__ = 0x020107 +__version__ = '2.1.10' +__build__ = 0x020110 __author__ = 'Roberto Rosario' __author_email__ = 'roberto.rosario@mayan-edms.com' __description__ = 'Free Open Source Electronic Document Management System' 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..0dde168a52 100644 --- a/mayan/apps/documents/serializers.py +++ b/mayan/apps/documents/serializers.py @@ -6,7 +6,8 @@ from common.models import SharedUploadedFile from .literals import DOCUMENT_IMAGE_TASK_TIMEOUT from .models import ( - Document, DocumentVersion, DocumentPage, DocumentType, RecentDocument + Document, DocumentVersion, DocumentPage, DocumentType, + DocumentTypeFilename, RecentDocument ) from .settings import setting_language from .tasks import task_get_document_page_image, task_upload_new_version @@ -45,15 +46,40 @@ class DocumentPageSerializer(serializers.HyperlinkedModelSerializer): model = DocumentPage +class DocumentTypeFilenameSerializer(serializers.ModelSerializer): + class Meta: + model = DocumentTypeFilename + fields = ('filename',) + + class DocumentTypeSerializer(serializers.HyperlinkedModelSerializer): documents_url = serializers.HyperlinkedIdentityField( view_name='rest_api:documenttype-document-list', ) documents_count = serializers.SerializerMethodField() + filenames = DocumentTypeFilenameSerializer(many=True, read_only=True) + + class Meta: + extra_kwargs = { + 'url': {'view_name': 'rest_api:documenttype-detail'}, + } + fields = ( + 'delete_time_period', 'delete_time_unit', 'documents_url', + 'documents_count', 'id', 'label', 'filenames', '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 +91,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 +111,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 +185,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 +198,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 diff --git a/mayan/apps/linking/api_views.py b/mayan/apps/linking/api_views.py new file mode 100644 index 0000000000..dd1ca25476 --- /dev/null +++ b/mayan/apps/linking/api_views.py @@ -0,0 +1,360 @@ +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.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 + +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 ( + 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): + 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() + + def get(self, *args, **kwargs): + """ + Returns a list of all the smart links. + """ + + 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. + """ + + 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() + + 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 get_serializer_class(self): + if self.request.method == 'GET': + return SmartLinkSerializer + else: + return WritableSmartLinkSerializer + + 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/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 new file mode 100644 index 0000000000..23a0db21ff --- /dev/null +++ b/mayan/apps/linking/serializers.py @@ -0,0 +1,115 @@ +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 + + +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 + + +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 279ea65564..6c00f08366 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 = 'sample' +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..4e795f830e --- /dev/null +++ b/mayan/apps/linking/tests/test_api.py @@ -0,0 +1,315 @@ +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_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() + + 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..272a433e87 100644 --- a/mayan/apps/linking/urls.py +++ b/mayan/apps/linking/urls.py @@ -2,6 +2,11 @@ from __future__ import unicode_literals from django.conf.urls import patterns, url +from .api_views import ( + APIResolvedSmartLinkView, APIResolvedSmartLinkDocumentListView, + APIResolvedSmartLinkListView, APISmartLinkListView, APISmartLinkView, + APISmartLinkConditionListView, APISmartLinkConditionView +) from .views import ( DocumentSmartLinkListView, ResolvedSmartLinkView, SetupSmartLinkDocumentTypesView, SmartLinkConditionListView, @@ -61,3 +66,38 @@ 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' + ), + 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): 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( 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/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): 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') 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( diff --git a/requirements/development.txt b/requirements/development.txt index 46a154d3c8..2169026ca8 100644 --- a/requirements/development.txt +++ b/requirements/development.txt @@ -8,7 +8,10 @@ django-rosetta==0.7.8 ipython==4.0.3 +pypandoc==1.3.3 + transifex-client==0.12.2 +twine==1.8.1 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()