Merge remote-tracking branch 'origin/master' into feature/merge_master

This commit is contained in:
Roberto Rosario
2017-02-13 21:00:56 -04:00
53 changed files with 3665 additions and 268 deletions

View File

@@ -16,6 +16,28 @@ the user links
- Stop loading theme fonts from the web (GitLab #343). - Stop loading theme fonts from the web (GitLab #343).
- Add support for attaching multiple tags (GitLab #307). - Add support for attaching multiple tags (GitLab #307).
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) 2.1.7 (2017-02-01)
================== ==================
- Improved user management API endpoints. - Improved user management API endpoints.

View File

@@ -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 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/* global-exclude mayan/settings/local.py mayan/settings/travis/* mayan/media/*

View File

@@ -90,15 +90,20 @@ requirements_testing:
# Releases # 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 sdist: clean
python setup.py sdist python setup.py sdist
ls -l dist ls -l dist
wheel: clean wheel: clean sdist
python setup.py bdist_wheel pip wheel --no-index --no-deps --wheel-dir dist dist/*.tar.gz
ls -l dist ls -l dist
@@ -118,4 +123,3 @@ shell_plus:
safety_check: safety_check:
safety check safety check

76
README.md Normal file
View File

@@ -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
<div align="center">
<a href="http://www.mayan-edms.com">
<img width="200" heigth="200" src="https://gitlab.com/mayan-edms/mayan-edms/raw/master/docs/_static/mayan_logo.png">
</a>
<br>
<br>
<p>
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.
<p>
<p align="center">
<img src="https://gitlab.com/mayan-edms/mayan-edms/raw/master/docs/_static/overview.gif">
</p>
</div>
<h2 align="center">Installation</h2>
The installation procedure uses the <a href="https://www.docker.com">Docker container manager (docker.com)</a>. 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
<h2 align="center">Important links</h2>
- [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/)

View File

@@ -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| images:: https://img.shields.io/pypi/pyversions/mayan-edms.svg
|Analytics|

75
docs/releases/2.1.10.rst Normal file
View File

@@ -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/

83
docs/releases/2.1.8.rst Normal file
View File

@@ -0,0 +1,83 @@
===============================
Mayan EDMS v2.1.8 release notes
===============================
Released: February 12, 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.
- Add Smart link API endpoints.
- Add writable versions of the Document and Document Type serializers (GitLab issues #348 and #349).
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
===========================
* `GitLab issue #310 <https://gitlab.com/mayan-edms/mayan-edms/issues/310>`_ Metadata's lookup with chinese messages when new document
* `GitLab issue #348 <https://gitlab.com/mayan-edms/mayan-edms/issues/348>`_ REST API: Document version comments are not getting updated
* `GitLab issue #349 <https://gitlab.com/mayan-edms/mayan-edms/issues/349>`_ REST API: Document Label, Description are not able to update
.. _PyPI: https://pypi.python.org/pypi/mayan-edms/

74
docs/releases/2.1.9.rst Normal file
View File

@@ -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/

View File

@@ -23,6 +23,9 @@ versions of the documentation contain the release notes for any later releases.
:maxdepth: 1 :maxdepth: 1
2.2 2.2
2.1.10
2.1.9
2.1.8
2.1.7 2.1.7
2.1.6 2.1.6
2.1.5 2.1.5

View File

@@ -1,8 +1,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
__title__ = 'Mayan EDMS' __title__ = 'Mayan EDMS'
__version__ = '2.1.7' __version__ = '2.1.10'
__build__ = 0x020107 __build__ = 0x020110
__author__ = 'Roberto Rosario' __author__ = 'Roberto Rosario'
__author_email__ = 'roberto.rosario@mayan-edms.com' __author_email__ = 'roberto.rosario@mayan-edms.com'
__description__ = 'Free Open Source Electronic Document Management System' __description__ = 'Free Open Source Electronic Document Management System'

View File

@@ -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)

View File

@@ -9,6 +9,7 @@ from common import (
MayanAppConfig, menu_facet, menu_object, menu_setup, menu_sidebar MayanAppConfig, menu_facet, menu_object, menu_setup, menu_sidebar
) )
from navigation import SourceColumn from navigation import SourceColumn
from rest_api.classes import APIEndPoint
from .classes import KeyStub from .classes import KeyStub
from .links import ( from .links import (
@@ -32,6 +33,7 @@ class DjangoGPGApp(MayanAppConfig):
def ready(self): def ready(self):
super(DjangoGPGApp, self).ready() super(DjangoGPGApp, self).ready()
APIEndPoint(app=self, version_string='1')
Key = self.get_model('Key') Key = self.get_model('Key')
ModelPermission.register( ModelPermission.register(

View File

@@ -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

View File

@@ -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)

View File

@@ -2,6 +2,7 @@ from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
from .api_views import APIKeyListView, APIKeyView
from .views import ( from .views import (
KeyDeleteView, KeyDetailView, KeyDownloadView, KeyQueryView, KeyDeleteView, KeyDetailView, KeyDownloadView, KeyQueryView,
KeyQueryResultView, KeyReceive, KeyUploadView, PrivateKeyListView, KeyQueryResultView, KeyReceive, KeyUploadView, PrivateKeyListView,
@@ -38,3 +39,11 @@ urlpatterns = [
r'^receive/(?P<key_id>.+)/$', KeyReceive.as_view(), name='key_receive' r'^receive/(?P<key_id>.+)/$', KeyReceive.as_view(), name='key_receive'
), ),
] ]
api_urls = [
url(
r'^keys/(?P<pk>[0-9]+)/$', APIKeyView.as_view(),
name='key-detail'
),
url(r'^keys/$', APIKeyListView.as_view(), name='key-list'),
]

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@ from common import (
) )
from common.widgets import two_state_template from common.widgets import two_state_template
from navigation import SourceColumn from navigation import SourceColumn
from rest_api.classes import APIEndPoint
from .handlers import launch_workflow from .handlers import launch_workflow
from .links import ( from .links import (
@@ -33,6 +34,8 @@ class DocumentStatesApp(MayanAppConfig):
def ready(self): def ready(self):
super(DocumentStatesApp, self).ready() super(DocumentStatesApp, self).ready()
APIEndPoint(app=self, version_string='1')
Document = apps.get_model( Document = apps.get_model(
app_label='documents', model_name='Document' app_label='documents', model_name='Document'
) )

View File

@@ -3,6 +3,7 @@ from __future__ import unicode_literals
import logging import logging
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import IntegrityError, models from django.db import IntegrityError, models
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
@@ -166,7 +167,18 @@ class WorkflowInstance(models.Model):
return None return None
def get_transition_choices(self): 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: class Meta:
unique_together = ('document', 'workflow') unique_together = ('document', 'workflow')
@@ -195,3 +207,7 @@ class WorkflowInstanceLogEntry(models.Model):
class Meta: class Meta:
verbose_name = _('Workflow instance log entry') verbose_name = _('Workflow instance log entry')
verbose_name_plural = _('Workflow instance log entries') 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.'))

View File

@@ -22,6 +22,5 @@ permission_workflow_view = namespace.add_permission(
# 'transition workflows' from one state to another, to move the workflow # 'transition workflows' from one state to another, to move the workflow
# forwards # forwards
permission_workflow_transition = namespace.add_permission( permission_workflow_transition = namespace.add_permission(
name='workflow_transition', name='workflow_transition', label=_('Transition workflows')
label=_('Transition workflows')
) )

View File

@@ -0,0 +1,355 @@
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, WorkflowInstance, WorkflowInstanceLogEntry, WorkflowState,
WorkflowTransition
)
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 WorkflowStateSerializer(serializers.HyperlinkedModelSerializer):
url = serializers.SerializerMethodField()
workflow_url = serializers.SerializerMethodField()
class Meta:
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 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', 'transitions',
'url'
)
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=_(
'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
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']
)

View File

@@ -1,8 +1,12 @@
from __future__ import unicode_literals 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_LABEL = 'test initial state'
TEST_WORKFLOW_INITIAL_STATE_COMPLETION = 33 TEST_WORKFLOW_INITIAL_STATE_COMPLETION = 33
TEST_WORKFLOW_STATE_LABEL = 'test state' 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 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'

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,13 @@ from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
from .api_views import (
APIWorkflowDocumentTypeList, APIWorkflowDocumentTypeView,
APIWorkflowInstanceListView, APIWorkflowInstanceView,
APIWorkflowInstanceLogEntryListView, APIWorkflowListView,
APIWorkflowStateListView, APIWorkflowStateView,
APIWorkflowTransitionListView, APIWorkflowTransitionView, APIWorkflowView
)
from .views import ( from .views import (
DocumentWorkflowInstanceListView, SetupWorkflowCreateView, DocumentWorkflowInstanceListView, SetupWorkflowCreateView,
SetupWorkflowDeleteView, SetupWorkflowDocumentTypesView, SetupWorkflowDeleteView, SetupWorkflowDocumentTypesView,
@@ -96,3 +103,50 @@ urlpatterns = [
name='setup_workflow_transition_edit' name='setup_workflow_transition_edit'
), ),
] ]
api_urls = [
url(r'^workflows/$', APIWorkflowListView.as_view(), name='workflow-list'),
url(
r'^workflows/(?P<pk>[0-9]+)/$', APIWorkflowView.as_view(),
name='workflow-detail'
),
url(
r'^workflows/(?P<pk>[0-9]+)/document_types/$',
APIWorkflowDocumentTypeList.as_view(),
name='workflow-document-type-list'
),
url(
r'^workflows/(?P<pk>[0-9]+)/document_types/(?P<document_type_pk>[0-9]+)/$',
APIWorkflowDocumentTypeView.as_view(),
name='workflow-document-type-detail'
),
url(
r'^workflows/(?P<pk>[0-9]+)/states/$',
APIWorkflowStateListView.as_view(), name='workflowstate-list'
),
url(
r'^workflows/(?P<pk>[0-9]+)/states/(?P<state_pk>[0-9]+)/$',
APIWorkflowStateView.as_view(), name='workflowstate-detail'
),
url(
r'^workflows/(?P<pk>[0-9]+)/transitions/$',
APIWorkflowTransitionListView.as_view(), name='workflowtransition-list'
),
url(
r'^workflows/(?P<pk>[0-9]+)/transitions/(?P<transition_pk>[0-9]+)/$',
APIWorkflowTransitionView.as_view(), name='workflowtransition-detail'
),
url(
r'^document/(?P<pk>[0-9]+)/workflows/$',
APIWorkflowInstanceListView.as_view(), name='workflowinstance-list'
),
url(
r'^document/(?P<pk>[0-9]+)/workflows/(?P<workflow_pk>[0-9]+)/$',
APIWorkflowInstanceView.as_view(), name='workflowinstance-detail'
),
url(
r'^document/(?P<pk>[0-9]+)/workflows/(?P<workflow_pk>[0-9]+)/log_entries/$',
APIWorkflowInstanceLogEntryListView.as_view(),
name='workflowinstancelogentry-list'
),
]

View File

@@ -31,7 +31,9 @@ from .serializers import (
DeletedDocumentSerializer, DocumentPageSerializer, DocumentSerializer, DeletedDocumentSerializer, DocumentPageSerializer, DocumentSerializer,
DocumentTypeSerializer, DocumentVersionSerializer, DocumentTypeSerializer, DocumentVersionSerializer,
DocumentVersionRevertSerializer, NewDocumentSerializer, DocumentVersionRevertSerializer, NewDocumentSerializer,
NewDocumentVersionSerializer, RecentDocumentSerializer NewDocumentVersionSerializer, RecentDocumentSerializer,
WritableDocumentSerializer, WritableDocumentTypeSerializer,
WritableDocumentVersionSerializer
) )
from .tasks import task_generate_document_page_image from .tasks import task_generate_document_page_image
@@ -187,7 +189,6 @@ class APIDocumentView(generics.RetrieveUpdateDestroyAPIView):
} }
permission_classes = (MayanPermission,) permission_classes = (MayanPermission,)
queryset = Document.objects.all() queryset = Document.objects.all()
serializer_class = DocumentSerializer
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
""" """
@@ -203,6 +204,12 @@ class APIDocumentView(generics.RetrieveUpdateDestroyAPIView):
return super(APIDocumentView, self).get(*args, **kwargs) 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): def patch(self, *args, **kwargs):
""" """
Edit the properties of the selected document. Edit the properties of the selected document.
@@ -321,6 +328,12 @@ class APIDocumentTypeListView(generics.ListCreateAPIView):
return super(APIDocumentTypeListView, self).get(*args, **kwargs) 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): def post(self, *args, **kwargs):
""" """
Create a new document type. Create a new document type.
@@ -342,7 +355,6 @@ class APIDocumentTypeView(generics.RetrieveUpdateDestroyAPIView):
} }
permission_classes = (MayanPermission,) permission_classes = (MayanPermission,)
queryset = DocumentType.objects.all() queryset = DocumentType.objects.all()
serializer_class = DocumentTypeSerializer
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
""" """
@@ -358,6 +370,12 @@ class APIDocumentTypeView(generics.RetrieveUpdateDestroyAPIView):
return super(APIDocumentTypeView, self).get(*args, **kwargs) 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): def patch(self, *args, **kwargs):
""" """
Edit the properties of the selected document type. Edit the properties of the selected document type.
@@ -463,7 +481,12 @@ class APIDocumentVersionView(generics.RetrieveUpdateAPIView):
mayan_permission_attribute_check = 'document' mayan_permission_attribute_check = 'document'
permission_classes = (MayanPermission,) permission_classes = (MayanPermission,)
queryset = DocumentVersion.objects.all() 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): def patch(self, *args, **kwargs):
""" """

View File

@@ -27,25 +27,46 @@ class DocumentPageSerializer(serializers.HyperlinkedModelSerializer):
class DocumentTypeSerializer(serializers.HyperlinkedModelSerializer): class DocumentTypeSerializer(serializers.HyperlinkedModelSerializer):
documents = serializers.HyperlinkedIdentityField( documents_url = serializers.HyperlinkedIdentityField(
view_name='rest_api:documenttype-document-list', view_name='rest_api:documenttype-document-list',
) )
documents_count = serializers.SerializerMethodField() documents_count = serializers.SerializerMethodField()
def get_documents_count(self, obj):
return obj.documents.count()
class Meta: class Meta:
extra_kwargs = { extra_kwargs = {
'url': {'view_name': 'rest_api:documenttype-detail'}, 'url': {'view_name': 'rest_api:documenttype-detail'},
} }
fields = ( fields = (
'delete_time_period', 'delete_time_unit', 'documents', 'delete_time_period', 'delete_time_unit', 'documents_url',
'documents_count', 'id', 'label', 'trash_time_period', 'documents_count', 'id', 'label', 'trash_time_period',
'trash_time_unit', 'url' 'trash_time_unit', 'url'
) )
model = DocumentType 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'},
}
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 DocumentVersionSerializer(serializers.HyperlinkedModelSerializer): class DocumentVersionSerializer(serializers.HyperlinkedModelSerializer):
pages = DocumentPageSerializer(many=True, required=False, read_only=True) pages = DocumentPageSerializer(many=True, required=False, read_only=True)
@@ -63,6 +84,26 @@ class DocumentVersionSerializer(serializers.HyperlinkedModelSerializer):
read_only_fields = ('document', 'file') 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 DocumentVersionRevertSerializer(DocumentVersionSerializer):
class Meta(DocumentVersionSerializer.Meta): class Meta(DocumentVersionSerializer.Meta):
read_only_fields = ('comment', 'document',) read_only_fields = ('comment', 'document',)
@@ -87,7 +128,7 @@ class NewDocumentVersionSerializer(serializers.Serializer):
class DeletedDocumentSerializer(serializers.HyperlinkedModelSerializer): class DeletedDocumentSerializer(serializers.HyperlinkedModelSerializer):
document_type_label = serializers.SerializerMethodField() document_type_label = serializers.SerializerMethodField()
restore = serializers.HyperlinkedIdentityField( restore = serializers.HyperlinkedIdentityField(
view_name='rest_api:deleteddocument-restore' view_name='rest_api:trasheddocument-restore'
) )
def get_document_type_label(self, instance): def get_document_type_label(self, instance):
@@ -96,7 +137,7 @@ class DeletedDocumentSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
extra_kwargs = { extra_kwargs = {
'document_type': {'view_name': 'rest_api:documenttype-detail'}, 'document_type': {'view_name': 'rest_api:documenttype-detail'},
'url': {'view_name': 'rest_api:deleteddocument-detail'} 'url': {'view_name': 'rest_api:trasheddocument-detail'}
} }
fields = ( fields = (
'date_added', 'deleted_date_time', 'description', 'document_type', 'date_added', 'deleted_date_time', 'description', 'document_type',
@@ -117,9 +158,6 @@ class DocumentSerializer(serializers.HyperlinkedModelSerializer):
view_name='rest_api:document-version-list', view_name='rest_api:document-version-list',
) )
def get_document_type_label(self, instance):
return instance.document_type.label
class Meta: class Meta:
extra_kwargs = { extra_kwargs = {
'document_type': {'view_name': 'rest_api:documenttype-detail'}, 'document_type': {'view_name': 'rest_api:documenttype-detail'},
@@ -133,6 +171,32 @@ class DocumentSerializer(serializers.HyperlinkedModelSerializer):
model = Document model = Document
read_only_fields = ('document_type',) 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): class NewDocumentSerializer(serializers.ModelSerializer):
file = serializers.FileField(write_only=True) file = serializers.FileField(write_only=True)

View File

@@ -9,6 +9,8 @@ from json import loads
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import override_settings from django.test import override_settings
from django.utils.encoding import force_text
from django.utils.six import BytesIO
from django_downloadview import assert_download_response from django_downloadview import assert_download_response
from rest_framework import status from rest_framework import status
@@ -48,12 +50,13 @@ class DocumentTypeAPITestCase(APITestCase):
def test_document_type_create(self): def test_document_type_create(self):
self.assertEqual(DocumentType.objects.all().count(), 0) self.assertEqual(DocumentType.objects.all().count(), 0)
self.client.post( response = self.client.post(
reverse('rest_api:documenttype-list'), data={ reverse('rest_api:documenttype-list'), data={
'label': TEST_DOCUMENT_TYPE 'label': TEST_DOCUMENT_TYPE
} }
) )
self.assertEqual(response.status_code, 201)
self.assertEqual(DocumentType.objects.all().count(), 1) self.assertEqual(DocumentType.objects.all().count(), 1)
self.assertEqual( self.assertEqual(
DocumentType.objects.all().first().label, TEST_DOCUMENT_TYPE DocumentType.objects.all().first().label, TEST_DOCUMENT_TYPE
@@ -93,10 +96,6 @@ class DocumentTypeAPITestCase(APITestCase):
@override_settings(OCR_AUTO_OCR=False) @override_settings(OCR_AUTO_OCR=False)
class DocumentAPITestCase(APITestCase): class DocumentAPITestCase(APITestCase):
"""
Test document API endpoints
"""
def setUp(self): def setUp(self):
self.admin_user = get_user_model().objects.create_superuser( self.admin_user = get_user_model().objects.create_superuser(
username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL, username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL,
@@ -155,51 +154,6 @@ class DocumentAPITestCase(APITestCase):
) )
self.assertEqual(document.page_count, 47) 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): def test_document_new_version_upload(self):
with open(TEST_SMALL_DOCUMENT_PATH) as file_object: with open(TEST_SMALL_DOCUMENT_PATH) as file_object:
document = self.document_type.new_document( document = self.document_type.new_document(
@@ -367,5 +321,88 @@ class DocumentAPITestCase(APITestCase):
TEST_DOCUMENT_DESCRIPTION_EDITED 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): # TODO: def test_document_set_document_type(self):
# pass # pass

View File

@@ -297,7 +297,8 @@ api_urls = [
), ),
url( url(
r'^document_version/(?P<pk>[0-9]+)/download/$', r'^document_version/(?P<pk>[0-9]+)/download/$',
APIDocumentVersionDownloadView.as_view(), name='documentversion-download' APIDocumentVersionDownloadView.as_view(),
name='documentversion-download'
), ),
url( url(
r'^document_page/(?P<pk>[0-9]+)/$', APIDocumentPageView.as_view(), r'^document_page/(?P<pk>[0-9]+)/$', APIDocumentPageView.as_view(),

View File

@@ -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)

View File

@@ -12,6 +12,7 @@ from common import (
) )
from common.widgets import two_state_template from common.widgets import two_state_template
from navigation import SourceColumn from navigation import SourceColumn
from rest_api.classes import APIEndPoint
from .links import ( from .links import (
link_smart_link_create, link_smart_link_condition_create, link_smart_link_create, link_smart_link_condition_create,
@@ -35,6 +36,8 @@ class LinkingApp(MayanAppConfig):
def ready(self): def ready(self):
super(LinkingApp, self).ready() super(LinkingApp, self).ready()
APIEndPoint(app=self, version_string='1')
Document = apps.get_model( Document = apps.get_model(
app_label='documents', model_name='Document' app_label='documents', model_name='Document'
) )

View File

@@ -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
)

View File

@@ -11,6 +11,7 @@ from documents.models import Document, DocumentType
from .literals import ( from .literals import (
INCLUSION_AND, INCLUSION_CHOICES, INCLUSION_OR, OPERATOR_CHOICES INCLUSION_AND, INCLUSION_CHOICES, INCLUSION_OR, OPERATOR_CHOICES
) )
from .managers import SmartLinkManager
@python_2_unicode_compatible @python_2_unicode_compatible
@@ -29,6 +30,8 @@ class SmartLink(models.Model):
DocumentType, verbose_name=_('Document types') DocumentType, verbose_name=_('Document types')
) )
objects = SmartLinkManager()
def __str__(self): def __str__(self):
return self.label return self.label

View File

@@ -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

View File

@@ -1,5 +1,9 @@
from __future__ import unicode_literals 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_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' TEST_SMART_LINK_LABEL = 'test label'

View File

@@ -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
)

View File

@@ -10,7 +10,7 @@ from ..permissions import (
) )
from .literals 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 TEST_SMART_LINK_LABEL
) )
@@ -80,7 +80,7 @@ class SmartLinkViewTestCase(GenericDocumentViewTestCase):
response = self.post( response = self.post(
'linking:smart_link_edit', args=(smart_link.pk,), data={ '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) self.assertEqual(response.status_code, 403)
@@ -98,13 +98,13 @@ class SmartLinkViewTestCase(GenericDocumentViewTestCase):
response = self.post( response = self.post(
'linking:smart_link_edit', args=(smart_link.pk,), data={ 'linking:smart_link_edit', args=(smart_link.pk,), data={
'label': TEST_SMART_LINK_EDITED_LABEL 'label': TEST_SMART_LINK_LABEL_EDITED
}, follow=True }, follow=True
) )
smart_link = SmartLink.objects.get(pk=smart_link.pk) smart_link = SmartLink.objects.get(pk=smart_link.pk)
self.assertContains(response, text='update', status_code=200) 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): def setup_smart_links(self):
smart_link = SmartLink.objects.create( smart_link = SmartLink.objects.create(

View File

@@ -2,6 +2,11 @@ from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
from .api_views import (
APIResolvedSmartLinkView, APIResolvedSmartLinkDocumentListView,
APIResolvedSmartLinkListView, APISmartLinkListView, APISmartLinkView,
APISmartLinkConditionListView, APISmartLinkConditionView
)
from .views import ( from .views import (
DocumentSmartLinkListView, ResolvedSmartLinkView, DocumentSmartLinkListView, ResolvedSmartLinkView,
SetupSmartLinkDocumentTypesView, SmartLinkConditionListView, SetupSmartLinkDocumentTypesView, SmartLinkConditionListView,
@@ -60,3 +65,38 @@ urlpatterns = [
name='smart_link_condition_delete' name='smart_link_condition_delete'
), ),
] ]
api_urls = [
url(
r'^smart_links/$', APISmartLinkListView.as_view(),
name='smartlink-list'
),
url(
r'^smart_links/(?P<pk>[0-9]+)/$', APISmartLinkView.as_view(),
name='smartlink-detail'
),
url(
r'^smart_links/(?P<pk>[0-9]+)/conditions/$',
APISmartLinkConditionListView.as_view(), name='smartlinkcondition-list'
),
url(
r'^smart_links/(?P<pk>[0-9]+)/conditions/(?P<condition_pk>[0-9]+)/$',
APISmartLinkConditionView.as_view(),
name='smartlinkcondition-detail'
),
url(
r'^documents/(?P<pk>[0-9]+)/resolved_smart_links/$',
APIResolvedSmartLinkListView.as_view(),
name='resolvedsmartlink-list'
),
url(
r'^documents/(?P<pk>[0-9]+)/resolved_smart_links/(?P<smart_link_pk>[0-9]+)/$',
APIResolvedSmartLinkView.as_view(),
name='resolvedsmartlink-detail'
),
url(
r'^documents/(?P<pk>[0-9]+)/resolved_smart_links/(?P<smart_link_pk>[0-9]+)/documents/$',
APIResolvedSmartLinkDocumentListView.as_view(),
name='resolvedsmartlinkdocument-list'
),
]

View File

@@ -160,9 +160,7 @@ class DocumentSmartLinkListView(SmartLinkListView):
} }
def get_smart_link_queryset(self): def get_smart_link_queryset(self):
return ResolvedSmartLink.objects.filter( return ResolvedSmartLink.objects.get_for(document=self.document)
document_types=self.document.document_type, enabled=True
)
class SmartLinkCreateView(SingleObjectCreateView): class SmartLinkCreateView(SingleObjectCreateView):

View File

@@ -244,7 +244,7 @@ class APIDocumentTypeMetadataTypeOptionalListView(generics.ListCreateAPIView):
obj=document_type obj=document_type
) )
serializer = self.get_serializer(data=self.request.POST) serializer = self.get_serializer(data=self.request.data)
if serializer.is_valid(): if serializer.is_valid():
metadata_type = get_object_or_404( metadata_type = get_object_or_404(

View File

@@ -1,8 +1,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import IntegrityError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from .models import DocumentMetadata, MetadataType, DocumentTypeMetadataType from .models import DocumentMetadata, MetadataType, DocumentTypeMetadataType
@@ -26,7 +28,7 @@ class DocumentMetadataSerializer(serializers.ModelSerializer):
class DocumentTypeMetadataTypeSerializer(serializers.ModelSerializer): class DocumentTypeMetadataTypeSerializer(serializers.ModelSerializer):
class Meta: class Meta:
fields = ('metadata_type', ) fields = ('metadata_type',)
model = DocumentTypeMetadataType model = DocumentTypeMetadataType
@@ -52,10 +54,15 @@ class DocumentNewMetadataSerializer(serializers.Serializer):
metadata_type = MetadataType.objects.get( metadata_type = MetadataType.objects.get(
pk=validated_data['metadata_type_pk'] pk=validated_data['metadata_type_pk']
) )
instance = self.document.metadata.create( try:
metadata_type=metadata_type, value=validated_data['value'] instance = self.document.metadata.create(
) metadata_type=metadata_type, value=validated_data['value']
return instance )
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): class DocumentTypeNewMetadataTypeSerializer(serializers.Serializer):

View File

@@ -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)

View File

@@ -6,6 +6,7 @@ from django.utils.translation import ugettext_lazy as _
from common import MayanAppConfig, menu_object, menu_secondary, menu_setup from common import MayanAppConfig, menu_object, menu_secondary, menu_setup
from navigation import SourceColumn from navigation import SourceColumn
from rest_api.classes import APIEndPoint
from .links import ( from .links import (
link_message_create, link_message_delete, link_message_edit, link_message_create, link_message_delete, link_message_edit,
@@ -23,6 +24,8 @@ class MOTDApp(MayanAppConfig):
def ready(self): def ready(self):
super(MOTDApp, self).ready() super(MOTDApp, self).ready()
APIEndPoint(app=self, version_string='1')
Message = self.get_model('Message') Message = self.get_model('Message')
SourceColumn( SourceColumn(

View File

@@ -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

View File

@@ -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'

View File

@@ -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_patch_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)

View File

@@ -7,8 +7,7 @@ from django.utils import timezone
from ..models import Message from ..models import Message
TEST_LABEL = 'test label' from .literals import TEST_LABEL, TEST_MESSAGE
TEST_MESSAGE = 'test message'
class MOTDTestCase(TestCase): class MOTDTestCase(TestCase):

View File

@@ -2,6 +2,7 @@ from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
from .api_views import APIMessageListView, APIMessageView
from .views import ( from .views import (
MessageCreateView, MessageDeleteView, MessageEditView, MessageListView MessageCreateView, MessageDeleteView, MessageEditView, MessageListView
) )
@@ -17,3 +18,11 @@ urlpatterns = [
name='message_delete' name='message_delete'
), ),
] ]
api_urls = [
url(r'^messages/$', APIMessageListView.as_view(), name='message-list'),
url(
r'^messages/(?P<pk>[0-9]+)/$', APIMessageView.as_view(),
name='message-detail'
),
]

View File

@@ -15,15 +15,43 @@ from rest_api.permissions import MayanPermission
from .models import Tag from .models import Tag
from .permissions import ( from .permissions import (
permission_tag_create, permission_tag_delete, permission_tag_edit, permission_tag_attach, permission_tag_create, permission_tag_delete,
permission_tag_remove, permission_tag_view permission_tag_edit, permission_tag_remove, permission_tag_view
) )
from .serializers import ( from .serializers import (
DocumentTagSerializer, NewDocumentTagSerializer, NewTagSerializer, DocumentTagSerializer, NewDocumentTagSerializer, TagSerializer,
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): class APITagView(generics.RetrieveUpdateDestroyAPIView):
filter_backends = (MayanObjectPermissionsFilter,) filter_backends = (MayanObjectPermissionsFilter,)
mayan_object_permissions = { mayan_object_permissions = {
@@ -33,7 +61,6 @@ class APITagView(generics.RetrieveUpdateDestroyAPIView):
'PUT': (permission_tag_edit,) 'PUT': (permission_tag_edit,)
} }
queryset = Tag.objects.all() queryset = Tag.objects.all()
serializer_class = TagSerializer
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
""" """
@@ -49,6 +76,12 @@ class APITagView(generics.RetrieveUpdateDestroyAPIView):
return super(APITagView, self).get(*args, **kwargs) 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): def patch(self, *args, **kwargs):
""" """
Edit the selected tag. Edit the selected tag.
@@ -64,34 +97,6 @@ class APITagView(generics.RetrieveUpdateDestroyAPIView):
return super(APITagView, self).put(*args, **kwargs) 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): class APITagDocumentListView(generics.ListAPIView):
""" """
Returns a list of all the documents tagged by a particular tag. Returns a list of all the documents tagged by a particular tag.
@@ -112,15 +117,21 @@ class APITagDocumentListView(generics.ListAPIView):
class APIDocumentTagListView(generics.ListCreateAPIView): class APIDocumentTagListView(generics.ListCreateAPIView):
"""
Returns a list of all the tags attached to a document.
"""
filter_backends = (MayanObjectPermissionsFilter,) 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): 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): def get_queryset(self):
document = self.get_document() document = self.get_document()
@@ -132,6 +143,12 @@ class APIDocumentTagListView(generics.ListCreateAPIView):
return document.attached_tags().all() 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): def get_serializer_context(self):
""" """
Extra context provided to the serializer class. Extra context provided to the serializer class.
@@ -143,12 +160,6 @@ class APIDocumentTagListView(generics.ListCreateAPIView):
'view': self '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): def perform_create(self, serializer):
serializer.save(document=self.get_document()) serializer.save(document=self.get_document())

View File

@@ -7,13 +7,15 @@ from rest_framework.exceptions import ValidationError
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
from acls.models import AccessControlList from acls.models import AccessControlList
from documents.models import Document
from permissions import Permission
from .models import Tag from .models import Tag
from .permissions import permission_tag_attach from .permissions import permission_tag_attach
class TagSerializer(serializers.HyperlinkedModelSerializer): class TagSerializer(serializers.HyperlinkedModelSerializer):
documents = serializers.HyperlinkedIdentityField( documents_url = serializers.HyperlinkedIdentityField(
view_name='rest_api:tag-document-list' view_name='rest_api:tag-document-list'
) )
documents_count = serializers.SerializerMethodField() documents_count = serializers.SerializerMethodField()
@@ -23,7 +25,7 @@ class TagSerializer(serializers.HyperlinkedModelSerializer):
'url': {'view_name': 'rest_api:tag-detail'}, 'url': {'view_name': 'rest_api:tag-detail'},
} }
fields = ( fields = (
'color', 'documents', 'documents_count', 'id', 'label', 'url' 'color', 'documents_count', 'documents_url', 'id', 'label', 'url'
) )
model = Tag model = Tag
@@ -31,22 +33,82 @@ class TagSerializer(serializers.HyperlinkedModelSerializer):
return instance.documents.count() 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: class Meta:
fields = ( fields = (
'color', 'label', 'id' 'color', 'documents_pk_list', 'id', 'label',
) )
model = Tag 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 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): class NewDocumentTagSerializer(serializers.Serializer):
tag = serializers.IntegerField( tag_pk = serializers.IntegerField(
help_text=_('Primary key of the tag to be added.') help_text=_('Primary key of the tag to be added.')
) )
def create(self, validated_data): def create(self, validated_data):
try: try:
tag = Tag.objects.get(pk=validated_data['tag']) tag = Tag.objects.get(pk=validated_data['tag_pk'])
AccessControlList.objects.check_access( AccessControlList.objects.check_access(
permissions=permission_tag_attach, permissions=permission_tag_attach,
@@ -57,19 +119,4 @@ class NewDocumentTagSerializer(serializers.Serializer):
except Exception as exception: except Exception as exception:
raise ValidationError(exception) raise ValidationError(exception)
return {'tag': tag.pk} return {'tag_pk': 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

View File

@@ -3,6 +3,7 @@ from __future__ import unicode_literals
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import override_settings from django.test import override_settings
from django.utils.encoding import force_text
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
@@ -20,6 +21,7 @@ from .literals import (
) )
@override_settings(OCR_AUTO_OCR=False)
class TagAPITestCase(APITestCase): class TagAPITestCase(APITestCase):
""" """
Test the tag API endpoints Test the tag API endpoints
@@ -36,9 +38,25 @@ class TagAPITestCase(APITestCase):
) )
def tearDown(self): def tearDown(self):
self.admin_user.delete() if hasattr(self, 'document_type'):
self.document_type.delete()
def test_tag_create(self): 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
)
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_view(self):
response = self.client.post( response = self.client.post(
reverse('rest_api:tag-list'), { reverse('rest_api:tag-list'), {
'label': TEST_TAG_LABEL, 'color': TEST_TAG_COLOR 'label': TEST_TAG_LABEL, 'color': TEST_TAG_COLOR
@@ -46,7 +64,6 @@ class TagAPITestCase(APITestCase):
) )
tag = Tag.objects.first() tag = Tag.objects.first()
self.assertEqual(response.data['id'], tag.pk) self.assertEqual(response.data['id'], tag.pk)
self.assertEqual(response.data['label'], TEST_TAG_LABEL) self.assertEqual(response.data['label'], TEST_TAG_LABEL)
self.assertEqual(response.data['color'], TEST_TAG_COLOR) self.assertEqual(response.data['color'], TEST_TAG_COLOR)
@@ -55,15 +72,60 @@ class TagAPITestCase(APITestCase):
self.assertEqual(tag.label, TEST_TAG_LABEL) self.assertEqual(tag.label, TEST_TAG_LABEL)
self.assertEqual(tag.color, TEST_TAG_COLOR) self.assertEqual(tag.color, TEST_TAG_COLOR)
def test_tag_delete(self): def test_tag_create_with_documents_view(self):
tag = Tag.objects.create(color=TEST_TAG_COLOR, label=TEST_TAG_LABEL) 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)
self.assertEqual(Tag.objects.count(), 1)
self.assertEqual(tag.label, TEST_TAG_LABEL)
self.assertEqual(tag.color, TEST_TAG_COLOR)
def test_tag_delete_view(self):
tag = self._create_tag()
self.client.delete(reverse('rest_api:tag-detail', args=(tag.pk,))) self.client.delete(reverse('rest_api:tag-detail', args=(tag.pk,)))
self.assertEqual(Tag.objects.count(), 0) self.assertEqual(Tag.objects.count(), 0)
def test_tag_edit(self): 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)
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 = self._create_tag()
self.client.patch(
reverse('rest_api:tag-detail', args=(tag.pk,)),
{
'label': TEST_TAG_LABEL_EDITED,
'color': TEST_TAG_COLOR_EDITED
}
)
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 = self._create_tag()
self.client.put( self.client.put(
reverse('rest_api:tag-detail', args=(tag.pk,)), reverse('rest_api:tag-detail', args=(tag.pk,)),
@@ -73,48 +135,51 @@ class TagAPITestCase(APITestCase):
} }
) )
tag = Tag.objects.first() tag.refresh_from_db()
self.assertEqual(tag.label, TEST_TAG_LABEL_EDITED) self.assertEqual(tag.label, TEST_TAG_LABEL_EDITED)
self.assertEqual(tag.color, TEST_TAG_COLOR_EDITED) self.assertEqual(tag.color, TEST_TAG_COLOR_EDITED)
@override_settings(OCR_AUTO_OCR=False) def test_document_attach_tag_view(self):
def test_tag_add_document(self): tag = self._create_tag()
tag = Tag.objects.create(color=TEST_TAG_COLOR, label=TEST_TAG_LABEL) document = self._document_create()
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,
)
self.client.post( self.client.post(
reverse('rest_api:document-tag-list', args=(document.pk,)), 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 = self._create_tag()
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)
@override_settings(OCR_AUTO_OCR=False) def test_document_tag_list_view(self):
def test_tag_remove_document(self): tag = self._create_tag()
tag = Tag.objects.create(color=TEST_TAG_COLOR, label=TEST_TAG_LABEL) document = self._document_create()
tag.documents.add(document)
document_type = DocumentType.objects.create( response = self.client.get(
label=TEST_DOCUMENT_TYPE reverse('rest_api:document-tag-list', args=(document.pk,))
) )
self.assertEqual(response.data['results'][0]['label'], tag.label)
with open(TEST_SMALL_DOCUMENT_PATH) as file_object: def test_document_tag_remove_view(self):
document = document_type.new_document( tag = self._create_tag()
file_object=file_object, document = self._document_create()
)
tag.documents.add(document) tag.documents.add(document)
self.client.delete( 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) self.assertEqual(tag.documents.count(), 0)

View File

@@ -63,11 +63,11 @@ api_urls = [
url(r'^tags/(?P<pk>[0-9]+)/$', APITagView.as_view(), name='tag-detail'), url(r'^tags/(?P<pk>[0-9]+)/$', APITagView.as_view(), name='tag-detail'),
url(r'^tags/$', APITagListView.as_view(), name='tag-list'), url(r'^tags/$', APITagListView.as_view(), name='tag-list'),
url( url(
r'^document/(?P<pk>[0-9]+)/tags/$', APIDocumentTagListView.as_view(), r'^documents/(?P<document_pk>[0-9]+)/tags/$',
name='document-tag-list' APIDocumentTagListView.as_view(), name='document-tag-list'
), ),
url( url(
r'^document/(?P<document_pk>[0-9]+)/tags/(?P<pk>[0-9]+)/$', r'^documents/(?P<document_pk>[0-9]+)/tags/(?P<pk>[0-9]+)/$',
APIDocumentTagView.as_view(), name='document-tag' APIDocumentTagView.as_view(), name='document-tag-detail'
), ),
] ]

View File

@@ -14,7 +14,7 @@ django-downloadview==1.9
django-formtools==2.0 django-formtools==2.0
django-pure-pagination==0.3.0 django-pure-pagination==0.3.0
django-model-utils==2.6.1 django-model-utils==2.6.1
django-mptt==0.8.7 django-mptt>=0.8.7
django-qsstats-magic==0.7.2 django-qsstats-magic==0.7.2
django-rest-swagger==0.3.10 django-rest-swagger==0.3.10
django-stronghold==0.2.8 django-stronghold==0.2.8

View File

@@ -12,7 +12,10 @@ ipython==5.1.0
safety==0.5.1 safety==0.5.1
pypandoc==1.3.3
transifex-client==0.12.2 transifex-client==0.12.2
twine==1.8.1
wheel==0.29.0 wheel==0.29.0

View File

@@ -72,7 +72,7 @@ django-filetransfers==0.1.0
django-formtools==1.0 django-formtools==1.0
django-pure-pagination==0.3.0 django-pure-pagination==0.3.0
django-model-utils==2.4 django-model-utils==2.4
django-mptt==0.8.0 django-mptt>=0.8.0
django-qsstats-magic==0.7.2 django-qsstats-magic==0.7.2
django-rest-swagger==0.3.4 django-rest-swagger==0.3.4
django-stronghold==0.2.7 django-stronghold==0.2.7
@@ -91,8 +91,13 @@ pytz==2015.4
sh==1.11 sh==1.11
""".split() """.split()
with open('README.rst') as f: try:
readme = f.read() 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: with open('HISTORY.rst') as f:
history = f.read() history = f.read()