Merge remote-tracking branch 'origin/master' into feature/master_merge
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
This commit is contained in:
20
HISTORY.rst
20
HISTORY.rst
@@ -1,4 +1,4 @@
|
|||||||
2.2 (2016-XX-XX)
|
2.2 (2017-04-XX)
|
||||||
================
|
================
|
||||||
- Remove the installation app (GitLab #301).
|
- Remove the installation app (GitLab #301).
|
||||||
- Add support for document page search
|
- Add support for document page search
|
||||||
@@ -17,6 +17,24 @@ the user links
|
|||||||
- Add support for attaching multiple tags (GitLab #307).
|
- Add support for attaching multiple tags (GitLab #307).
|
||||||
- Integrate the Cabinets app.
|
- Integrate the Cabinets app.
|
||||||
|
|
||||||
|
2.1.11 (2017-03-14)
|
||||||
|
===================
|
||||||
|
- Added a quick rename serializer to the document type API serializer.
|
||||||
|
- Added per document type, workflow list API view.
|
||||||
|
- Mayan EDMS was adopted a version 1.1 of the Linux Foundation Developer Certificate of Origin.
|
||||||
|
- Added the detail url of a permission in the permission serializer.
|
||||||
|
- Added endpoints for the ACL app API.
|
||||||
|
- Implemented document workflows transition ACLs. GitLab issue #321.
|
||||||
|
- Add document comments API endpoints. GitHub issue #249.
|
||||||
|
- Add support for overriding the Celery class.
|
||||||
|
- Changed the document upload view in source app to not use the HTTP referer
|
||||||
|
URL blindly, but instead recompose the URL using known view name. Needed
|
||||||
|
when integrating Mayan EDMS into other app via using iframes.
|
||||||
|
- Addes size field to the document version serializer.
|
||||||
|
- Removed the serializer from the deleted document restore API endpoint.
|
||||||
|
- Added support for adding or editing document types to smart links via the
|
||||||
|
API.
|
||||||
|
|
||||||
2.1.10 (2017-02-13)
|
2.1.10 (2017-02-13)
|
||||||
===================
|
===================
|
||||||
- Update Makefile to use twine for releases.
|
- Update Makefile to use twine for releases.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
===============================
|
================================
|
||||||
Mayan EDMS v2.1.10 release notes
|
Mayan EDMS v2.1.10 release notes
|
||||||
===============================
|
================================
|
||||||
|
|
||||||
Released: February 13, 2017
|
Released: February 13, 2017
|
||||||
|
|
||||||
|
|||||||
94
docs/releases/2.1.11.rst
Normal file
94
docs/releases/2.1.11.rst
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
================================
|
||||||
|
Mayan EDMS v2.1.11 release notes
|
||||||
|
================================
|
||||||
|
|
||||||
|
Released: March 14, 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
|
||||||
|
-------------
|
||||||
|
|
||||||
|
- Added a quick rename serializer to the document type API serializer.
|
||||||
|
- Added per document type, workflow list API view. The URL for this endpoint is
|
||||||
|
GET /api/document_states/document_type/{pk}/workflows/
|
||||||
|
- Added Developer Certificate of Origin. Mayan EDMS was adopted a version 1.1 of
|
||||||
|
the Linux Foundation Developer Certificate of Origin. All commits must be
|
||||||
|
signed (`git commit -s`) in order to be merged.
|
||||||
|
- Added the detail url of a permission in the permission serializer.
|
||||||
|
- Added endpoints for the ACL app API.
|
||||||
|
- Implemented document workflows transition ACLs. GitLab issue #321.
|
||||||
|
- Add document comments API endpoints. GitHub issue #249.
|
||||||
|
- Add support for overriding the Celery class. The setting is named
|
||||||
|
MAYAN_CELERY_CLASS and expects a dotted python path to the class to use.
|
||||||
|
- Changed the document upload view in source app to not use the HTTP referer
|
||||||
|
URL blindly, but instead recompose the URL using known view name. Needed
|
||||||
|
when integrating Mayan EDMS into other app via using iframes.
|
||||||
|
- Addes size field to the document version serializer.
|
||||||
|
- Removed the serializer from the deleted document restore API endpoint
|
||||||
|
it doesn't need a serializer being just an action POST endpoint.
|
||||||
|
- Added support for adding or editing document types to smart links via the
|
||||||
|
API.
|
||||||
|
|
||||||
|
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
|
||||||
|
===========================
|
||||||
|
|
||||||
|
* `Github issue #249 <https://github.com/mayan-edms/mayan-edms/issues/249>`_ Add document comments API [$50 US]
|
||||||
|
* `GitLab issue #321 <https://gitlab.com/mayan-edms/mayan-edms/issues/321>`_ Transition ACLS
|
||||||
|
* `GitLab issue #357 <https://gitlab.com/mayan-edms/mayan-edms/issues/357>`_ It should be possible to retrieve all workflows for a given DocumentType from the API
|
||||||
|
|
||||||
|
.. _PyPI: https://pypi.python.org/pypi/mayan-edms/
|
||||||
@@ -23,6 +23,7 @@ versions of the documentation contain the release notes for any later releases.
|
|||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
|
|
||||||
2.2
|
2.2
|
||||||
|
2.1.11
|
||||||
2.1.10
|
2.1.10
|
||||||
2.1.9
|
2.1.9
|
||||||
2.1.8
|
2.1.8
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
__title__ = 'Mayan EDMS'
|
__title__ = 'Mayan EDMS'
|
||||||
__version__ = '2.2b1'
|
__version__ = '2.2b2'
|
||||||
__build__ = 0x020200
|
__build__ = 0x020200
|
||||||
__author__ = 'Roberto Rosario'
|
__author__ = 'Roberto Rosario'
|
||||||
__author_email__ = 'roberto.rosario@mayan-edms.com'
|
__author_email__ = 'roberto.rosario@mayan-edms.com'
|
||||||
|
|||||||
250
mayan/apps/acls/api_views.py
Normal file
250
mayan/apps/acls/api_views.py
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
|
from rest_framework import generics
|
||||||
|
|
||||||
|
from permissions import Permission
|
||||||
|
|
||||||
|
from .models import AccessControlList
|
||||||
|
from .permissions import permission_acl_edit, permission_acl_view
|
||||||
|
from .serializers import (
|
||||||
|
AccessControlListPermissionSerializer, AccessControlListSerializer,
|
||||||
|
WritableAccessControlListPermissionSerializer,
|
||||||
|
WritableAccessControlListSerializer
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class APIObjectACLListView(generics.ListCreateAPIView):
|
||||||
|
def get(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns a list of all the object's access control lists
|
||||||
|
"""
|
||||||
|
|
||||||
|
return super(APIObjectACLListView, self).get(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_content_object(self):
|
||||||
|
content_type = get_object_or_404(
|
||||||
|
ContentType, app_label=self.kwargs['app_label'],
|
||||||
|
model=self.kwargs['model']
|
||||||
|
)
|
||||||
|
|
||||||
|
content_object = get_object_or_404(
|
||||||
|
content_type.model_class(), pk=self.kwargs['object_pk']
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.request.method == 'GET':
|
||||||
|
permission_required = permission_acl_view
|
||||||
|
else:
|
||||||
|
permission_required = permission_acl_edit
|
||||||
|
|
||||||
|
try:
|
||||||
|
Permission.check_permissions(
|
||||||
|
self.request.user, permissions=(permission_required,)
|
||||||
|
)
|
||||||
|
except PermissionDenied:
|
||||||
|
AccessControlList.objects.check_access(
|
||||||
|
permission_required, self.request.user, content_object
|
||||||
|
)
|
||||||
|
|
||||||
|
return content_object
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.get_content_object().acls.all()
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
"""
|
||||||
|
Extra context provided to the serializer class.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return {
|
||||||
|
'content_object': self.get_content_object(),
|
||||||
|
'format': self.format_kwarg,
|
||||||
|
'request': self.request,
|
||||||
|
'view': self
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.request.method == 'GET':
|
||||||
|
return AccessControlListSerializer
|
||||||
|
else:
|
||||||
|
return WritableAccessControlListSerializer
|
||||||
|
|
||||||
|
def post(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Create a new access control list for the selected object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return super(APIObjectACLListView, self).post(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class APIObjectACLView(generics.RetrieveDestroyAPIView):
|
||||||
|
serializer_class = AccessControlListSerializer
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Delete the selected access control list.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return super(APIObjectACLView, self).delete(*args, **kwargs)
|
||||||
|
|
||||||
|
def get(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the details of the selected access control list.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return super(APIObjectACLView, self).get(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_content_object(self):
|
||||||
|
if self.request.method == 'GET':
|
||||||
|
permission_required = permission_acl_view
|
||||||
|
else:
|
||||||
|
permission_required = permission_acl_edit
|
||||||
|
|
||||||
|
content_type = get_object_or_404(
|
||||||
|
ContentType, app_label=self.kwargs['app_label'],
|
||||||
|
model=self.kwargs['model']
|
||||||
|
)
|
||||||
|
|
||||||
|
content_object = get_object_or_404(
|
||||||
|
content_type.model_class(), pk=self.kwargs['object_pk']
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
Permission.check_permissions(
|
||||||
|
self.request.user, permissions=(permission_required,)
|
||||||
|
)
|
||||||
|
except PermissionDenied:
|
||||||
|
AccessControlList.objects.check_access(
|
||||||
|
permission_required, self.request.user, content_object
|
||||||
|
)
|
||||||
|
|
||||||
|
return content_object
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.get_content_object().acls.all()
|
||||||
|
|
||||||
|
|
||||||
|
class APIObjectACLPermissionListView(generics.ListCreateAPIView):
|
||||||
|
def get(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the access control list permission list.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return super(
|
||||||
|
APIObjectACLPermissionListView, self
|
||||||
|
).get(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_acl(self):
|
||||||
|
return get_object_or_404(
|
||||||
|
self.get_content_object().acls, pk=self.kwargs['pk']
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_content_object(self):
|
||||||
|
content_type = get_object_or_404(
|
||||||
|
ContentType, app_label=self.kwargs['app_label'],
|
||||||
|
model=self.kwargs['model']
|
||||||
|
)
|
||||||
|
|
||||||
|
content_object = get_object_or_404(
|
||||||
|
content_type.model_class(), pk=self.kwargs['object_pk']
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
Permission.check_permissions(
|
||||||
|
self.request.user, permissions=(permission_acl_view,)
|
||||||
|
)
|
||||||
|
except PermissionDenied:
|
||||||
|
AccessControlList.objects.check_access(
|
||||||
|
permission_acl_view, self.request.user, content_object
|
||||||
|
)
|
||||||
|
|
||||||
|
return content_object
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.get_acl().permissions.all()
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.request.method == 'GET':
|
||||||
|
return AccessControlListPermissionSerializer
|
||||||
|
else:
|
||||||
|
return WritableAccessControlListPermissionSerializer
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
return {
|
||||||
|
'acl': self.get_acl(),
|
||||||
|
'format': self.format_kwarg,
|
||||||
|
'request': self.request,
|
||||||
|
'view': self
|
||||||
|
}
|
||||||
|
|
||||||
|
def post(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Add a new permission to the selected access control list.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return super(
|
||||||
|
APIObjectACLPermissionListView, self
|
||||||
|
).post(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class APIObjectACLPermissionView(generics.RetrieveDestroyAPIView):
|
||||||
|
lookup_url_kwarg = 'permission_pk'
|
||||||
|
serializer_class = AccessControlListPermissionSerializer
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Remove the permission from the selected access control list.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return super(
|
||||||
|
APIObjectACLPermissionView, self
|
||||||
|
).delete(*args, **kwargs)
|
||||||
|
|
||||||
|
def get(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the details of the selected access control list permission.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return super(
|
||||||
|
APIObjectACLPermissionView, self
|
||||||
|
).get(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_acl(self):
|
||||||
|
return get_object_or_404(
|
||||||
|
self.get_content_object().acls, pk=self.kwargs['pk']
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_content_object(self):
|
||||||
|
content_type = get_object_or_404(
|
||||||
|
ContentType, app_label=self.kwargs['app_label'],
|
||||||
|
model=self.kwargs['model']
|
||||||
|
)
|
||||||
|
|
||||||
|
content_object = get_object_or_404(
|
||||||
|
content_type.model_class(), pk=self.kwargs['object_pk']
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
Permission.check_permissions(
|
||||||
|
self.request.user, permissions=(permission_acl_view,)
|
||||||
|
)
|
||||||
|
except PermissionDenied:
|
||||||
|
AccessControlList.objects.check_access(
|
||||||
|
permission_acl_view, self.request.user, content_object
|
||||||
|
)
|
||||||
|
|
||||||
|
return content_object
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.get_acl().permissions.all()
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
return {
|
||||||
|
'acl': self.get_acl(),
|
||||||
|
'format': self.format_kwarg,
|
||||||
|
'request': self.request,
|
||||||
|
'view': self
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
|
|
||||||
from common import MayanAppConfig, menu_object, menu_sidebar
|
from common import MayanAppConfig, menu_object, menu_sidebar
|
||||||
from navigation import SourceColumn
|
from navigation import SourceColumn
|
||||||
|
from rest_api.classes import APIEndPoint
|
||||||
|
|
||||||
from .links import link_acl_create, link_acl_delete, link_acl_permissions
|
from .links import link_acl_create, link_acl_delete, link_acl_permissions
|
||||||
|
|
||||||
@@ -16,6 +17,8 @@ class ACLsApp(MayanAppConfig):
|
|||||||
def ready(self):
|
def ready(self):
|
||||||
super(ACLsApp, self).ready()
|
super(ACLsApp, self).ready()
|
||||||
|
|
||||||
|
APIEndPoint(app=self, version_string='1')
|
||||||
|
|
||||||
AccessControlList = self.get_model('AccessControlList')
|
AccessControlList = self.get_model('AccessControlList')
|
||||||
|
|
||||||
SourceColumn(
|
SourceColumn(
|
||||||
|
|||||||
@@ -14,10 +14,18 @@ class ModelPermission(object):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def register(cls, model, permissions):
|
def register(cls, model, permissions):
|
||||||
|
from django.contrib.contenttypes.fields import GenericRelation
|
||||||
|
|
||||||
cls._registry.setdefault(model, [])
|
cls._registry.setdefault(model, [])
|
||||||
for permission in permissions:
|
for permission in permissions:
|
||||||
cls._registry[model].append(permission)
|
cls._registry[model].append(permission)
|
||||||
|
|
||||||
|
AccessControlList = apps.get_model(
|
||||||
|
app_label='acls', model_name='AccessControlList'
|
||||||
|
)
|
||||||
|
|
||||||
|
model.add_to_class('acls', GenericRelation(AccessControlList))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_for_instance(cls, instance):
|
def get_for_instance(cls, instance):
|
||||||
StoredPermission = apps.get_model(
|
StoredPermission = apps.get_model(
|
||||||
@@ -36,7 +44,9 @@ class ModelPermission(object):
|
|||||||
if proxy:
|
if proxy:
|
||||||
permissions.extend(cls._registry.get(proxy))
|
permissions.extend(cls._registry.get(proxy))
|
||||||
|
|
||||||
pks = [permission.stored_permission.pk for permission in set(permissions)]
|
pks = [
|
||||||
|
permission.stored_permission.pk for permission in set(permissions)
|
||||||
|
]
|
||||||
return StoredPermission.objects.filter(pk__in=pks)
|
return StoredPermission.objects.filter(pk__in=pks)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -49,6 +49,10 @@ class AccessControlListManager(models.Manager):
|
|||||||
|
|
||||||
def check_access(self, permissions, user, obj, related=None):
|
def check_access(self, permissions, user, obj, related=None):
|
||||||
if user.is_superuser or user.is_staff:
|
if user.is_superuser or user.is_staff:
|
||||||
|
logger.debug(
|
||||||
|
'Permissions "%s" on "%s" granted to user "%s" as superuser or staff',
|
||||||
|
permissions, obj, user
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -89,15 +93,30 @@ class AccessControlListManager(models.Manager):
|
|||||||
for group in user.groups.all():
|
for group in user.groups.all():
|
||||||
for role in group.roles.all():
|
for role in group.roles.all():
|
||||||
if set(stored_permissions).intersection(set(self.get_inherited_permissions(role=role, obj=obj))):
|
if set(stored_permissions).intersection(set(self.get_inherited_permissions(role=role, obj=obj))):
|
||||||
|
logger.debug(
|
||||||
|
'Permissions "%s" on "%s" granted to user "%s" through role "%s" via inherited ACL',
|
||||||
|
permissions, obj, user, role
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
user_roles.append(role)
|
user_roles.append(role)
|
||||||
|
|
||||||
if not self.filter(content_type=ContentType.objects.get_for_model(obj), object_id=obj.pk, permissions__in=stored_permissions, role__in=user_roles).exists():
|
if not self.filter(content_type=ContentType.objects.get_for_model(obj), object_id=obj.pk, permissions__in=stored_permissions, role__in=user_roles).exists():
|
||||||
|
logger.debug(
|
||||||
|
'Permissions "%s" on "%s" denied for user "%s"',
|
||||||
|
permissions, obj, user
|
||||||
|
)
|
||||||
raise PermissionDenied(ugettext('Insufficient access.'))
|
raise PermissionDenied(ugettext('Insufficient access.'))
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
'Permissions "%s" on "%s" granted to user "%s" through roles "%s" by direct ACL',
|
||||||
|
permissions, obj, user, user_roles
|
||||||
|
)
|
||||||
|
|
||||||
def filter_by_access(self, permission, user, queryset):
|
def filter_by_access(self, permission, user, queryset):
|
||||||
if user.is_superuser or user.is_staff:
|
if user.is_superuser or user.is_staff:
|
||||||
|
logger.debug('Unfiltered queryset returned to user "%s" as superuser or staff',
|
||||||
|
user)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -145,6 +164,10 @@ class AccessControlListManager(models.Manager):
|
|||||||
content_type=content_type, role__in=user_roles,
|
content_type=content_type, role__in=user_roles,
|
||||||
permissions=permission.stored_permission
|
permissions=permission.stored_permission
|
||||||
).values_list('object_id', flat=True))
|
).values_list('object_id', flat=True))
|
||||||
|
logger.debug(
|
||||||
|
'Filtered queryset returned to user "%s" based on roles "%s"',
|
||||||
|
user, user_roles
|
||||||
|
)
|
||||||
|
|
||||||
return queryset.filter(parent_acl_query | acl_query)
|
return queryset.filter(parent_acl_query | acl_query)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ class AccessControlList(models.Model):
|
|||||||
verbose_name_plural = _('Access entries')
|
verbose_name_plural = _('Access entries')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return _('Permissions "%(permissions)s" to role "%(role)s" for "%(object)s"') % {
|
return _(
|
||||||
|
'Permissions "%(permissions)s" to role "%(role)s" for "%(object)s"'
|
||||||
|
) % {
|
||||||
'permissions': self.get_permission_titles(),
|
'permissions': self.get_permission_titles(),
|
||||||
'object': self.content_object,
|
'object': self.content_object,
|
||||||
'role': self.role
|
'role': self.role
|
||||||
|
|||||||
204
mayan/apps/acls/serializers.py
Normal file
204
mayan/apps/acls/serializers.py
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
|
from django.utils.encoding import force_text
|
||||||
|
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 common.serializers import ContentTypeSerializer
|
||||||
|
from permissions import Permission
|
||||||
|
from permissions.models import Role, StoredPermission
|
||||||
|
from permissions.serializers import PermissionSerializer, RoleSerializer
|
||||||
|
|
||||||
|
from .models import AccessControlList
|
||||||
|
|
||||||
|
|
||||||
|
class AccessControlListSerializer(serializers.ModelSerializer):
|
||||||
|
content_type = ContentTypeSerializer(read_only=True)
|
||||||
|
permissions_url = serializers.SerializerMethodField(
|
||||||
|
help_text=_(
|
||||||
|
'API URL pointing to the list of permissions for this access '
|
||||||
|
'control list.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
role = RoleSerializer(read_only=True)
|
||||||
|
url = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
fields = (
|
||||||
|
'content_type', 'id', 'object_id', 'permissions_url', 'role', 'url'
|
||||||
|
)
|
||||||
|
model = AccessControlList
|
||||||
|
|
||||||
|
def get_permissions_url(self, instance):
|
||||||
|
return reverse(
|
||||||
|
'rest_api:accesscontrollist-permission-list', args=(
|
||||||
|
instance.content_type.app_label, instance.content_type.model,
|
||||||
|
instance.object_id, instance.pk
|
||||||
|
), request=self.context['request'], format=self.context['format']
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_url(self, instance):
|
||||||
|
return reverse(
|
||||||
|
'rest_api:accesscontrollist-detail', args=(
|
||||||
|
instance.content_type.app_label, instance.content_type.model,
|
||||||
|
instance.object_id, instance.pk
|
||||||
|
), request=self.context['request'], format=self.context['format']
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AccessControlListPermissionSerializer(PermissionSerializer):
|
||||||
|
acl_permission_url = serializers.SerializerMethodField(
|
||||||
|
help_text=_(
|
||||||
|
'API URL pointing to a permission in relation to the '
|
||||||
|
'access control list to which it is attached. This URL is '
|
||||||
|
'different than the canonical workflow URL.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
acl_url = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def get_acl_permission_url(self, instance):
|
||||||
|
return reverse(
|
||||||
|
'rest_api:accesscontrollist-permission-detail', args=(
|
||||||
|
self.context['acl'].content_type.app_label,
|
||||||
|
self.context['acl'].content_type.model,
|
||||||
|
self.context['acl'].object_id, self.context['acl'].pk,
|
||||||
|
instance.stored_permission.pk
|
||||||
|
), request=self.context['request'], format=self.context['format']
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_acl_url(self, instance):
|
||||||
|
return reverse(
|
||||||
|
'rest_api:accesscontrollist-detail', args=(
|
||||||
|
self.context['acl'].content_type.app_label,
|
||||||
|
self.context['acl'].content_type.model,
|
||||||
|
self.context['acl'].object_id, self.context['acl'].pk
|
||||||
|
), request=self.context['request'], format=self.context['format']
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WritableAccessControlListPermissionSerializer(AccessControlListPermissionSerializer):
|
||||||
|
permission_pk = serializers.CharField(
|
||||||
|
help_text=_(
|
||||||
|
'Primary key of the new permission to grant to the access control '
|
||||||
|
'list.'
|
||||||
|
), write_only=True
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
fields = ('namespace',)
|
||||||
|
read_only_fields = ('namespace',)
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
for permission in validated_data['permissions']:
|
||||||
|
self.context['acl'].permissions.add(permission)
|
||||||
|
|
||||||
|
return validated_data['permissions'][0]
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
permissions_pk_list = attrs.pop('permission_pk', None)
|
||||||
|
permissions_result = []
|
||||||
|
|
||||||
|
if permissions_pk_list:
|
||||||
|
for pk in permissions_pk_list.split(','):
|
||||||
|
try:
|
||||||
|
permission = Permission.get(get_dict={'pk': pk})
|
||||||
|
except KeyError:
|
||||||
|
raise ValidationError(_('No such permission: %s') % pk)
|
||||||
|
else:
|
||||||
|
# Accumulate valid stored permission pks
|
||||||
|
permissions_result.append(permission.pk)
|
||||||
|
|
||||||
|
attrs['permissions'] = StoredPermission.objects.filter(
|
||||||
|
pk__in=permissions_result
|
||||||
|
)
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
class WritableAccessControlListSerializer(serializers.ModelSerializer):
|
||||||
|
content_type = ContentTypeSerializer(read_only=True)
|
||||||
|
permissions_pk_list = serializers.CharField(
|
||||||
|
help_text=_(
|
||||||
|
'Comma separated list of permission primary keys to grant to this '
|
||||||
|
'access control list.'
|
||||||
|
), required=False
|
||||||
|
)
|
||||||
|
permissions_url = serializers.SerializerMethodField(
|
||||||
|
help_text=_(
|
||||||
|
'API URL pointing to the list of permissions for this access '
|
||||||
|
'control list.'
|
||||||
|
), read_only=True
|
||||||
|
)
|
||||||
|
role_pk = serializers.IntegerField(
|
||||||
|
help_text=_(
|
||||||
|
'Primary keys of the role to which this access control list '
|
||||||
|
'binds to.'
|
||||||
|
), write_only=True
|
||||||
|
)
|
||||||
|
url = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
fields = (
|
||||||
|
'content_type', 'id', 'object_id', 'permissions_pk_list',
|
||||||
|
'permissions_url', 'role_pk', 'url'
|
||||||
|
)
|
||||||
|
model = AccessControlList
|
||||||
|
read_only_fields = ('content_type', 'object_id')
|
||||||
|
|
||||||
|
def get_permissions_url(self, instance):
|
||||||
|
return reverse(
|
||||||
|
'rest_api:accesscontrollist-permission-list', args=(
|
||||||
|
instance.content_type.app_label, instance.content_type.model,
|
||||||
|
instance.object_id, instance.pk
|
||||||
|
), request=self.context['request'], format=self.context['format']
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_url(self, instance):
|
||||||
|
return reverse(
|
||||||
|
'rest_api:accesscontrollist-detail', args=(
|
||||||
|
instance.content_type.app_label, instance.content_type.model,
|
||||||
|
instance.object_id, instance.pk
|
||||||
|
), request=self.context['request'], format=self.context['format']
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
attrs['content_type'] = ContentType.objects.get_for_model(
|
||||||
|
self.context['content_object']
|
||||||
|
)
|
||||||
|
attrs['object_id'] = self.context['content_object'].pk
|
||||||
|
|
||||||
|
try:
|
||||||
|
attrs['role'] = Role.objects.get(pk=attrs.pop('role_pk'))
|
||||||
|
except Role.DoesNotExist as exception:
|
||||||
|
raise ValidationError(force_text(exception))
|
||||||
|
|
||||||
|
permissions_pk_list = attrs.pop('permissions_pk_list', None)
|
||||||
|
permissions_result = []
|
||||||
|
|
||||||
|
if permissions_pk_list:
|
||||||
|
for pk in permissions_pk_list.split(','):
|
||||||
|
try:
|
||||||
|
permission = Permission.get(get_dict={'pk': pk})
|
||||||
|
except KeyError:
|
||||||
|
raise ValidationError(_('No such permission: %s') % pk)
|
||||||
|
else:
|
||||||
|
# Accumulate valid stored permission pks
|
||||||
|
permissions_result.append(permission.pk)
|
||||||
|
|
||||||
|
instance = AccessControlList(**attrs)
|
||||||
|
|
||||||
|
try:
|
||||||
|
instance.full_clean()
|
||||||
|
except DjangoValidationError as exception:
|
||||||
|
raise ValidationError(exception)
|
||||||
|
|
||||||
|
# Add a queryset of valid stored permissions so that they get added
|
||||||
|
# after the ACL gets created.
|
||||||
|
attrs['permissions'] = StoredPermission.objects.filter(
|
||||||
|
pk__in=permissions_result
|
||||||
|
)
|
||||||
|
return attrs
|
||||||
254
mayan/apps/acls/tests/test_api.py
Normal file
254
mayan/apps/acls/tests/test_api.py
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.test import override_settings
|
||||||
|
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from documents.models import DocumentType
|
||||||
|
from documents.permissions import permission_document_view
|
||||||
|
from documents.tests.literals import (
|
||||||
|
TEST_DOCUMENT_TYPE, TEST_SMALL_DOCUMENT_PATH
|
||||||
|
)
|
||||||
|
from permissions.classes import Permission
|
||||||
|
from permissions.models import Role
|
||||||
|
from permissions.tests.literals import TEST_ROLE_LABEL
|
||||||
|
from user_management.tests.literals import (
|
||||||
|
TEST_ADMIN_EMAIL, TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME
|
||||||
|
)
|
||||||
|
|
||||||
|
from ..models import AccessControlList
|
||||||
|
from ..permissions import permission_acl_view
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(OCR_AUTO_OCR=False)
|
||||||
|
class ACLAPITestCase(APITestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.admin_user = get_user_model().objects.create_superuser(
|
||||||
|
username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL,
|
||||||
|
password=TEST_ADMIN_PASSWORD
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.login(
|
||||||
|
username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD
|
||||||
|
)
|
||||||
|
|
||||||
|
self.document_type = DocumentType.objects.create(
|
||||||
|
label=TEST_DOCUMENT_TYPE
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(TEST_SMALL_DOCUMENT_PATH) as file_object:
|
||||||
|
self.document = self.document_type.new_document(
|
||||||
|
file_object=file_object
|
||||||
|
)
|
||||||
|
|
||||||
|
self.role = Role.objects.create(label=TEST_ROLE_LABEL)
|
||||||
|
|
||||||
|
self.document_content_type = ContentType.objects.get_for_model(
|
||||||
|
self.document
|
||||||
|
)
|
||||||
|
Permission.invalidate_cache()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
if hasattr(self, 'document_type'):
|
||||||
|
self.document_type.delete()
|
||||||
|
|
||||||
|
def _create_acl(self):
|
||||||
|
self.acl = AccessControlList.objects.create(
|
||||||
|
content_object=self.document,
|
||||||
|
role=self.role
|
||||||
|
)
|
||||||
|
|
||||||
|
self.acl.permissions.add(permission_document_view.stored_permission)
|
||||||
|
|
||||||
|
def test_object_acl_list_view(self):
|
||||||
|
self._create_acl()
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
'rest_api:accesscontrollist-list',
|
||||||
|
args=(
|
||||||
|
self.document_content_type.app_label,
|
||||||
|
self.document_content_type.model,
|
||||||
|
self.document.pk
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
response.data['results'][0]['content_type']['app_label'],
|
||||||
|
self.document_content_type.app_label
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data['results'][0]['role']['label'], TEST_ROLE_LABEL
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_object_acl_delete_view(self):
|
||||||
|
self._create_acl()
|
||||||
|
|
||||||
|
response = self.client.delete(
|
||||||
|
reverse(
|
||||||
|
'rest_api:accesscontrollist-detail',
|
||||||
|
args=(
|
||||||
|
self.document_content_type.app_label,
|
||||||
|
self.document_content_type.model,
|
||||||
|
self.document.pk, self.acl.pk
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 204)
|
||||||
|
self.assertEqual(AccessControlList.objects.count(), 0)
|
||||||
|
|
||||||
|
def test_object_acl_detail_view(self):
|
||||||
|
self._create_acl()
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
'rest_api:accesscontrollist-detail',
|
||||||
|
args=(
|
||||||
|
self.document_content_type.app_label,
|
||||||
|
self.document_content_type.model,
|
||||||
|
self.document.pk, self.acl.pk
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data['content_type']['app_label'],
|
||||||
|
self.document_content_type.app_label
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
response.data['role']['label'], TEST_ROLE_LABEL
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_object_acl_permission_delete_view(self):
|
||||||
|
self._create_acl()
|
||||||
|
permission = self.acl.permissions.first()
|
||||||
|
|
||||||
|
response = self.client.delete(
|
||||||
|
reverse(
|
||||||
|
'rest_api:accesscontrollist-permission-detail',
|
||||||
|
args=(
|
||||||
|
self.document_content_type.app_label,
|
||||||
|
self.document_content_type.model,
|
||||||
|
self.document.pk, self.acl.pk,
|
||||||
|
permission.pk
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 204)
|
||||||
|
self.assertEqual(self.acl.permissions.count(), 0)
|
||||||
|
|
||||||
|
def test_object_acl_permission_detail_view(self):
|
||||||
|
self._create_acl()
|
||||||
|
permission = self.acl.permissions.first()
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
'rest_api:accesscontrollist-permission-detail',
|
||||||
|
args=(
|
||||||
|
self.document_content_type.app_label,
|
||||||
|
self.document_content_type.model,
|
||||||
|
self.document.pk, self.acl.pk,
|
||||||
|
permission.pk
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
response.data['pk'], permission_document_view.pk
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_object_acl_permission_list_view(self):
|
||||||
|
self._create_acl()
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
'rest_api:accesscontrollist-permission-list',
|
||||||
|
args=(
|
||||||
|
self.document_content_type.app_label,
|
||||||
|
self.document_content_type.model,
|
||||||
|
self.document.pk, self.acl.pk
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
response.data['results'][0]['pk'],
|
||||||
|
permission_document_view.pk
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_object_acl_permission_list_post_view(self):
|
||||||
|
self._create_acl()
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse(
|
||||||
|
'rest_api:accesscontrollist-permission-list',
|
||||||
|
args=(
|
||||||
|
self.document_content_type.app_label,
|
||||||
|
self.document_content_type.model,
|
||||||
|
self.document.pk, self.acl.pk
|
||||||
|
)
|
||||||
|
), data={'permission_pk': permission_acl_view.pk}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
self.assertQuerysetEqual(
|
||||||
|
ordered=False, qs=self.acl.permissions.all(), values=(
|
||||||
|
repr(permission_document_view.stored_permission),
|
||||||
|
repr(permission_acl_view.stored_permission)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_object_acl_post_no_permissions_added_view(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse(
|
||||||
|
'rest_api:accesscontrollist-list',
|
||||||
|
args=(
|
||||||
|
self.document_content_type.app_label,
|
||||||
|
self.document_content_type.model,
|
||||||
|
self.document.pk
|
||||||
|
)
|
||||||
|
), data={'role_pk': self.role.pk}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
self.assertEqual(
|
||||||
|
self.document.acls.first().role, self.role
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.document.acls.first().content_object, self.document
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.document.acls.first().permissions.count(), 0
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_object_acl_post_with_permissions_added_view(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse(
|
||||||
|
'rest_api:accesscontrollist-list',
|
||||||
|
args=(
|
||||||
|
self.document_content_type.app_label,
|
||||||
|
self.document_content_type.model,
|
||||||
|
self.document.pk
|
||||||
|
)
|
||||||
|
), data={
|
||||||
|
'role_pk': self.role.pk,
|
||||||
|
'permissions_pk_list': permission_acl_view.pk
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
self.assertEqual(
|
||||||
|
self.document.acls.first().content_object, self.document
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.document.acls.first().role, self.role
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
self.document.acls.first().permissions.first(),
|
||||||
|
permission_acl_view.stored_permission
|
||||||
|
)
|
||||||
@@ -2,6 +2,10 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from .api_views import (
|
||||||
|
APIObjectACLListView, APIObjectACLPermissionListView,
|
||||||
|
APIObjectACLPermissionView, APIObjectACLView
|
||||||
|
)
|
||||||
from .views import (
|
from .views import (
|
||||||
ACLCreateView, ACLDeleteView, ACLListView, ACLPermissionsView
|
ACLCreateView, ACLDeleteView, ACLListView, ACLPermissionsView
|
||||||
)
|
)
|
||||||
@@ -21,3 +25,22 @@ urlpatterns = [
|
|||||||
name='acl_permissions'
|
name='acl_permissions'
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
api_urls = [
|
||||||
|
url(
|
||||||
|
r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/$',
|
||||||
|
APIObjectACLListView.as_view(), name='accesscontrollist-list'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/(?P<pk>\d+)/$',
|
||||||
|
APIObjectACLView.as_view(), name='accesscontrollist-detail'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/(?P<pk>\d+)/permissions/$',
|
||||||
|
APIObjectACLPermissionListView.as_view(), name='accesscontrollist-permission-list'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/(?P<pk>\d+)/permissions/(?P<permission_pk>\d+)/$',
|
||||||
|
APIObjectACLPermissionView.as_view(), name='accesscontrollist-permission-detail'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|||||||
@@ -140,7 +140,8 @@ class ACLListView(SingleObjectListView):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return AccessControlList.objects.filter(
|
return AccessControlList.objects.filter(
|
||||||
content_type=self.object_content_type, object_id=self.content_object.pk
|
content_type=self.object_content_type,
|
||||||
|
object_id=self.content_object.pk
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -34,22 +34,6 @@ class DeleteExtraDataMixin(object):
|
|||||||
return HttpResponseRedirect(success_url)
|
return HttpResponseRedirect(success_url)
|
||||||
|
|
||||||
|
|
||||||
class FormExtraKwargsMixin(object):
|
|
||||||
"""
|
|
||||||
Mixin that allows a view to pass extra keyword arguments to forms
|
|
||||||
"""
|
|
||||||
|
|
||||||
form_extra_kwargs = {}
|
|
||||||
|
|
||||||
def get_form_extra_kwargs(self):
|
|
||||||
return self.form_extra_kwargs
|
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
|
||||||
result = super(FormExtraKwargsMixin, self).get_form_kwargs()
|
|
||||||
result.update(self.get_form_extra_kwargs())
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class ExtraContextMixin(object):
|
class ExtraContextMixin(object):
|
||||||
"""
|
"""
|
||||||
Mixin that allows views to pass extra context to the template
|
Mixin that allows views to pass extra context to the template
|
||||||
@@ -66,6 +50,22 @@ class ExtraContextMixin(object):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class FormExtraKwargsMixin(object):
|
||||||
|
"""
|
||||||
|
Mixin that allows a view to pass extra keyword arguments to forms
|
||||||
|
"""
|
||||||
|
|
||||||
|
form_extra_kwargs = {}
|
||||||
|
|
||||||
|
def get_form_extra_kwargs(self):
|
||||||
|
return self.form_extra_kwargs
|
||||||
|
|
||||||
|
def get_form_kwargs(self):
|
||||||
|
result = super(FormExtraKwargsMixin, self).get_form_kwargs()
|
||||||
|
result.update(self.get_form_extra_kwargs())
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class MultipleInstanceActionMixin(object):
|
class MultipleInstanceActionMixin(object):
|
||||||
# TODO: Deprecated, replace views using this with
|
# TODO: Deprecated, replace views using this with
|
||||||
# MultipleObjectFormActionView or MultipleObjectConfirmActionView
|
# MultipleObjectFormActionView or MultipleObjectConfirmActionView
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ class OpenFileCheckMixin(object):
|
|||||||
|
|
||||||
class TempfileCheckMixin(object):
|
class TempfileCheckMixin(object):
|
||||||
# Ignore the jvmstat instrumentation and GitLab's CI .config files
|
# Ignore the jvmstat instrumentation and GitLab's CI .config files
|
||||||
|
# Ignore LibreOffice fontconfig cache dir
|
||||||
ignore_globs = ('hsperfdata_*', '.config', '.cache')
|
ignore_globs = ('hsperfdata_*', '.config', '.cache')
|
||||||
|
|
||||||
def _get_temporary_entries(self):
|
def _get_temporary_entries(self):
|
||||||
|
|||||||
120
mayan/apps/document_comments/api_views.py
Normal file
120
mayan/apps/document_comments/api_views.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
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 permissions import Permission
|
||||||
|
|
||||||
|
from .permissions import (
|
||||||
|
permission_comment_create, permission_comment_delete,
|
||||||
|
permission_comment_view
|
||||||
|
)
|
||||||
|
from .serializers import CommentSerializer, WritableCommentSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class APICommentListView(generics.ListCreateAPIView):
|
||||||
|
def get(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns a list of all the document comments.
|
||||||
|
"""
|
||||||
|
return super(APICommentListView, self).get(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_document(self):
|
||||||
|
if self.request.method == 'GET':
|
||||||
|
permission_required = permission_comment_view
|
||||||
|
else:
|
||||||
|
permission_required = permission_comment_create
|
||||||
|
|
||||||
|
document = get_object_or_404(Document, pk=self.kwargs['document_pk'])
|
||||||
|
|
||||||
|
try:
|
||||||
|
Permission.check_permissions(
|
||||||
|
self.request.user, (permission_required,)
|
||||||
|
)
|
||||||
|
except PermissionDenied:
|
||||||
|
AccessControlList.objects.check_access(
|
||||||
|
permission_required, self.request.user, document
|
||||||
|
)
|
||||||
|
|
||||||
|
return document
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.get_document().comments.all()
|
||||||
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
if self.request.method == 'GET':
|
||||||
|
return CommentSerializer
|
||||||
|
else:
|
||||||
|
return WritableCommentSerializer
|
||||||
|
|
||||||
|
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 post(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Create a new document comment.
|
||||||
|
"""
|
||||||
|
return super(APICommentListView, self).post(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class APICommentView(generics.RetrieveDestroyAPIView):
|
||||||
|
lookup_url_kwarg = 'comment_pk'
|
||||||
|
serializer_class = CommentSerializer
|
||||||
|
|
||||||
|
def delete(self, request, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Delete the selected document comment.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return super(APICommentView, self).delete(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Returns the details of the selected document comment.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return super(APICommentView, self).get(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_document(self):
|
||||||
|
if self.request.method == 'GET':
|
||||||
|
permission_required = permission_comment_view
|
||||||
|
else:
|
||||||
|
permission_required = permission_comment_delete
|
||||||
|
|
||||||
|
document = get_object_or_404(Document, pk=self.kwargs['document_pk'])
|
||||||
|
|
||||||
|
try:
|
||||||
|
Permission.check_permissions(
|
||||||
|
self.request.user, (permission_required,)
|
||||||
|
)
|
||||||
|
except PermissionDenied:
|
||||||
|
AccessControlList.objects.check_access(
|
||||||
|
permission_required, self.request.user, document
|
||||||
|
)
|
||||||
|
|
||||||
|
return document
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.get_document().comments.all()
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
"""
|
||||||
|
Extra context provided to the serializer class.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'format': self.format_kwarg,
|
||||||
|
'request': self.request,
|
||||||
|
'view': self
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
from acls import ModelPermission
|
from acls import ModelPermission
|
||||||
from common import MayanAppConfig, menu_facet, menu_object, menu_sidebar
|
from common import MayanAppConfig, menu_facet, menu_object, menu_sidebar
|
||||||
from navigation import SourceColumn
|
from navigation import SourceColumn
|
||||||
|
from rest_api.classes import APIEndPoint
|
||||||
|
|
||||||
from .links import (
|
from .links import (
|
||||||
link_comment_add, link_comment_delete, link_comments_for_document
|
link_comment_add, link_comment_delete, link_comments_for_document
|
||||||
@@ -20,11 +21,14 @@ class DocumentCommentsApp(MayanAppConfig):
|
|||||||
app_namespace = 'comments'
|
app_namespace = 'comments'
|
||||||
app_url = 'comments'
|
app_url = 'comments'
|
||||||
name = 'document_comments'
|
name = 'document_comments'
|
||||||
|
test = True
|
||||||
verbose_name = _('Document comments')
|
verbose_name = _('Document comments')
|
||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
super(DocumentCommentsApp, self).ready()
|
super(DocumentCommentsApp, 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'
|
||||||
)
|
)
|
||||||
|
|||||||
71
mayan/apps/document_comments/serializers.py
Normal file
71
mayan/apps/document_comments/serializers.py
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
|
from documents.serializers import DocumentSerializer
|
||||||
|
from user_management.serializers import UserSerializer
|
||||||
|
|
||||||
|
from .models import Comment
|
||||||
|
|
||||||
|
|
||||||
|
class CommentSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
document = DocumentSerializer(read_only=True)
|
||||||
|
document_comments_url = serializers.SerializerMethodField()
|
||||||
|
url = serializers.SerializerMethodField()
|
||||||
|
user = UserSerializer(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
fields = (
|
||||||
|
'comment', 'document', 'document_comments_url', 'id',
|
||||||
|
'submit_date', 'url', 'user'
|
||||||
|
)
|
||||||
|
model = Comment
|
||||||
|
|
||||||
|
def get_document_comments_url(self, instance):
|
||||||
|
return reverse(
|
||||||
|
'rest_api:comment-list', args=(
|
||||||
|
instance.document.pk,
|
||||||
|
), request=self.context['request'], format=self.context['format']
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_url(self, instance):
|
||||||
|
return reverse(
|
||||||
|
'rest_api:comment-detail', args=(
|
||||||
|
instance.document.pk, instance.pk
|
||||||
|
), request=self.context['request'], format=self.context['format']
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WritableCommentSerializer(serializers.ModelSerializer):
|
||||||
|
document = DocumentSerializer(read_only=True)
|
||||||
|
document_comments_url = serializers.SerializerMethodField()
|
||||||
|
url = serializers.SerializerMethodField()
|
||||||
|
user = UserSerializer(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
fields = (
|
||||||
|
'comment', 'document', 'document_comments_url', 'id',
|
||||||
|
'submit_date', 'url', 'user'
|
||||||
|
)
|
||||||
|
model = Comment
|
||||||
|
read_only_fields = ('document',)
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
validated_data['document'] = self.context['document']
|
||||||
|
validated_data['user'] = self.context['request'].user
|
||||||
|
return super(WritableCommentSerializer, self).create(validated_data)
|
||||||
|
|
||||||
|
def get_document_comments_url(self, instance):
|
||||||
|
return reverse(
|
||||||
|
'rest_api:comment-list', args=(
|
||||||
|
instance.document.pk,
|
||||||
|
), request=self.context['request'], format=self.context['format']
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_url(self, instance):
|
||||||
|
return reverse(
|
||||||
|
'rest_api:comment-detail', args=(
|
||||||
|
instance.document.pk, instance.pk
|
||||||
|
), request=self.context['request'], format=self.context['format']
|
||||||
|
)
|
||||||
0
mayan/apps/document_comments/tests/__init__.py
Normal file
0
mayan/apps/document_comments/tests/__init__.py
Normal file
3
mayan/apps/document_comments/tests/literals.py
Normal file
3
mayan/apps/document_comments/tests/literals.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
TEST_COMMENT_TEXT = 'test comment text'
|
||||||
97
mayan/apps/document_comments/tests/test_api.py
Normal file
97
mayan/apps/document_comments/tests/test_api.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
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 Comment
|
||||||
|
|
||||||
|
from .literals import TEST_COMMENT_TEXT
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(OCR_AUTO_OCR=False)
|
||||||
|
class CommentAPITestCase(APITestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.admin_user = get_user_model().objects.create_superuser(
|
||||||
|
username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL,
|
||||||
|
password=TEST_ADMIN_PASSWORD
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.login(
|
||||||
|
username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD
|
||||||
|
)
|
||||||
|
|
||||||
|
self.document_type = DocumentType.objects.create(
|
||||||
|
label=TEST_DOCUMENT_TYPE
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(TEST_SMALL_DOCUMENT_PATH) as file_object:
|
||||||
|
self.document = self.document_type.new_document(
|
||||||
|
file_object=file_object
|
||||||
|
)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
if hasattr(self, 'document_type'):
|
||||||
|
self.document_type.delete()
|
||||||
|
|
||||||
|
def _create_comment(self):
|
||||||
|
return self.document.comments.create(
|
||||||
|
comment=TEST_COMMENT_TEXT, user=self.admin_user
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_comment_create_view(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse(
|
||||||
|
'rest_api:comment-list', args=(self.document.pk,)
|
||||||
|
), {
|
||||||
|
'comment': TEST_COMMENT_TEXT
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
comment = Comment.objects.first()
|
||||||
|
self.assertEqual(Comment.objects.count(), 1)
|
||||||
|
self.assertEqual(response.data['id'], comment.pk)
|
||||||
|
|
||||||
|
def test_comment_delete_view(self):
|
||||||
|
comment = self._create_comment()
|
||||||
|
|
||||||
|
self.client.delete(
|
||||||
|
reverse(
|
||||||
|
'rest_api:comment-detail', args=(self.document.pk, comment.pk,)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(Comment.objects.count(), 0)
|
||||||
|
|
||||||
|
def test_comment_detail_view(self):
|
||||||
|
comment = self._create_comment()
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse(
|
||||||
|
'rest_api:comment-detail', args=(self.document.pk, comment.pk,)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.data['comment'], comment.comment)
|
||||||
|
|
||||||
|
def test_comment_list_view(self):
|
||||||
|
comment = self._create_comment()
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
reverse('rest_api:comment-list', args=(self.document.pk,))
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
response.data['results'][0]['comment'], comment.comment
|
||||||
|
)
|
||||||
@@ -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 APICommentListView, APICommentView
|
||||||
from .views import (
|
from .views import (
|
||||||
DocumentCommentCreateView, DocumentCommentDeleteView,
|
DocumentCommentCreateView, DocumentCommentDeleteView,
|
||||||
DocumentCommentListView
|
DocumentCommentListView
|
||||||
@@ -21,3 +22,14 @@ urlpatterns = [
|
|||||||
DocumentCommentListView.as_view(), name='comments_for_document'
|
DocumentCommentListView.as_view(), name='comments_for_document'
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
api_urls = [
|
||||||
|
url(
|
||||||
|
r'^document/(?P<document_pk>[0-9]+)/comments/$',
|
||||||
|
APICommentListView.as_view(), name='comment-list'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^document/(?P<document_pk>[0-9]+)/comments/(?P<comment_pk>[0-9]+)/$',
|
||||||
|
APICommentView.as_view(), name='comment-detail'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ from rest_api.permissions import MayanPermission
|
|||||||
from .models import Workflow
|
from .models import Workflow
|
||||||
from .permissions import (
|
from .permissions import (
|
||||||
permission_workflow_create, permission_workflow_delete,
|
permission_workflow_create, permission_workflow_delete,
|
||||||
permission_workflow_edit, permission_workflow_transition,
|
permission_workflow_edit, permission_workflow_view
|
||||||
permission_workflow_view
|
|
||||||
)
|
)
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
NewWorkflowDocumentTypeSerializer, WorkflowDocumentTypeSerializer,
|
NewWorkflowDocumentTypeSerializer, WorkflowDocumentTypeSerializer,
|
||||||
@@ -552,15 +551,21 @@ class APIWorkflowInstanceLogEntryListView(generics.ListCreateAPIView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_document(self):
|
def get_document(self):
|
||||||
if self.request.method == 'GET':
|
|
||||||
permission_required = permission_workflow_view
|
|
||||||
else:
|
|
||||||
permission_required = permission_workflow_transition
|
|
||||||
|
|
||||||
document = get_object_or_404(Document, pk=self.kwargs['pk'])
|
document = get_object_or_404(Document, pk=self.kwargs['pk'])
|
||||||
|
|
||||||
|
if self.request.method == 'GET':
|
||||||
|
"""
|
||||||
|
Only test for permission if reading. If writing, the permission
|
||||||
|
will be checked in the serializer
|
||||||
|
|
||||||
|
IMPROVEMENT:
|
||||||
|
When writing, add check for permission or ACL for the workflow.
|
||||||
|
Failing that, check for ACLs for any of the workflow's transitions.
|
||||||
|
Failing that, then raise PermissionDenied
|
||||||
|
"""
|
||||||
|
|
||||||
AccessControlList.objects.check_access(
|
AccessControlList.objects.check_access(
|
||||||
permissions=permission_required, user=self.request.user,
|
permissions=permission_workflow_view, user=self.request.user,
|
||||||
obj=document
|
obj=document
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
|
|
||||||
from kombu import Exchange, Queue
|
from kombu import Exchange, Queue
|
||||||
|
|
||||||
|
from acls import ModelPermission
|
||||||
|
from acls.links import link_acl_list
|
||||||
from common import (
|
from common import (
|
||||||
MayanAppConfig, menu_facet, menu_main, menu_object, menu_secondary,
|
MayanAppConfig, menu_facet, menu_main, menu_object, menu_secondary,
|
||||||
menu_setup, menu_sidebar, menu_tools
|
menu_setup, menu_sidebar, menu_tools
|
||||||
@@ -29,6 +31,7 @@ from .links import (
|
|||||||
link_workflow_list, link_workflow_state_document_list,
|
link_workflow_list, link_workflow_state_document_list,
|
||||||
link_workflow_state_list
|
link_workflow_state_list
|
||||||
)
|
)
|
||||||
|
from .permissions import permission_workflow_transition
|
||||||
|
|
||||||
|
|
||||||
class DocumentStatesApp(MayanAppConfig):
|
class DocumentStatesApp(MayanAppConfig):
|
||||||
@@ -54,6 +57,15 @@ class DocumentStatesApp(MayanAppConfig):
|
|||||||
WorkflowStateRuntimeProxy = self.get_model('WorkflowStateRuntimeProxy')
|
WorkflowStateRuntimeProxy = self.get_model('WorkflowStateRuntimeProxy')
|
||||||
WorkflowTransition = self.get_model('WorkflowTransition')
|
WorkflowTransition = self.get_model('WorkflowTransition')
|
||||||
|
|
||||||
|
ModelPermission.register(
|
||||||
|
model=Workflow, permissions=(permission_workflow_transition,)
|
||||||
|
)
|
||||||
|
|
||||||
|
ModelPermission.register(
|
||||||
|
model=WorkflowTransition,
|
||||||
|
permissions=(permission_workflow_transition,)
|
||||||
|
)
|
||||||
|
|
||||||
SourceColumn(
|
SourceColumn(
|
||||||
source=Workflow, label=_('Initial state'),
|
source=Workflow, label=_('Initial state'),
|
||||||
func=lambda context: context['object'].get_initial_state() or _('None')
|
func=lambda context: context['object'].get_initial_state() or _('None')
|
||||||
@@ -144,7 +156,7 @@ class DocumentStatesApp(MayanAppConfig):
|
|||||||
links=(
|
links=(
|
||||||
link_setup_workflow_states, link_setup_workflow_transitions,
|
link_setup_workflow_states, link_setup_workflow_transitions,
|
||||||
link_setup_workflow_document_types, link_setup_workflow_edit,
|
link_setup_workflow_document_types, link_setup_workflow_edit,
|
||||||
link_setup_workflow_delete
|
link_acl_list, link_setup_workflow_delete
|
||||||
), sources=(Workflow,)
|
), sources=(Workflow,)
|
||||||
)
|
)
|
||||||
menu_object.bind_links(
|
menu_object.bind_links(
|
||||||
@@ -155,7 +167,7 @@ class DocumentStatesApp(MayanAppConfig):
|
|||||||
)
|
)
|
||||||
menu_object.bind_links(
|
menu_object.bind_links(
|
||||||
links=(
|
links=(
|
||||||
link_setup_workflow_transition_edit,
|
link_setup_workflow_transition_edit, link_acl_list,
|
||||||
link_setup_workflow_transition_delete
|
link_setup_workflow_transition_delete
|
||||||
), sources=(WorkflowTransition,)
|
), sources=(WorkflowTransition,)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
@@ -32,11 +32,16 @@ class WorkflowTransitionForm(forms.ModelForm):
|
|||||||
|
|
||||||
class WorkflowInstanceTransitionForm(forms.Form):
|
class WorkflowInstanceTransitionForm(forms.Form):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
workflow = kwargs.pop('workflow')
|
user = kwargs.pop('user')
|
||||||
|
workflow_instance = kwargs.pop('workflow_instance')
|
||||||
super(WorkflowInstanceTransitionForm, self).__init__(*args, **kwargs)
|
super(WorkflowInstanceTransitionForm, self).__init__(*args, **kwargs)
|
||||||
self.fields['transition'].choices = workflow.get_transition_choices().values_list('pk', 'label')
|
self.fields[
|
||||||
|
'transition'
|
||||||
|
].queryset = workflow_instance.get_transition_choices(_user=user)
|
||||||
|
|
||||||
transition = forms.ChoiceField(label=_('Transition'))
|
transition = forms.ModelChoiceField(
|
||||||
|
label=_('Transition'), queryset=WorkflowTransition.objects.none()
|
||||||
|
)
|
||||||
comment = forms.CharField(
|
comment = forms.CharField(
|
||||||
label=_('Comment'), required=False, widget=forms.widgets.Textarea()
|
label=_('Comment'), required=False, widget=forms.widgets.Textarea()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ from navigation import Link
|
|||||||
|
|
||||||
from .permissions import (
|
from .permissions import (
|
||||||
permission_workflow_create, permission_workflow_delete,
|
permission_workflow_create, permission_workflow_delete,
|
||||||
permission_workflow_edit, permission_workflow_transition,
|
permission_workflow_edit, permission_workflow_tools,
|
||||||
permission_workflow_tools, permission_workflow_view,
|
permission_workflow_view,
|
||||||
)
|
)
|
||||||
|
|
||||||
link_document_workflow_instance_list = Link(
|
link_document_workflow_instance_list = Link(
|
||||||
@@ -82,7 +82,7 @@ link_workflow_instance_detail = Link(
|
|||||||
view='document_states:workflow_instance_detail', args='resolved_object.pk'
|
view='document_states:workflow_instance_detail', args='resolved_object.pk'
|
||||||
)
|
)
|
||||||
link_workflow_instance_transition = Link(
|
link_workflow_instance_transition = Link(
|
||||||
permissions=(permission_workflow_transition,), text=_('Transition'),
|
text=_('Transition'),
|
||||||
view='document_states:workflow_instance_transition',
|
view='document_states:workflow_instance_transition',
|
||||||
args='resolved_object.pk'
|
args='resolved_object.pk'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import absolute_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.exceptions import PermissionDenied, 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
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from acls.models import AccessControlList
|
||||||
from documents.models import Document, DocumentType
|
from documents.models import Document, DocumentType
|
||||||
|
from permissions import Permission
|
||||||
|
|
||||||
from .managers import WorkflowManager
|
from .managers import WorkflowManager
|
||||||
|
from .permissions import permission_workflow_transition
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -169,11 +172,41 @@ class WorkflowInstance(models.Model):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_transition_choices(self):
|
def get_transition_choices(self, _user=None):
|
||||||
current_state = self.get_current_state()
|
current_state = self.get_current_state()
|
||||||
|
|
||||||
if current_state:
|
if current_state:
|
||||||
return current_state.origin_transitions.all()
|
queryset = current_state.origin_transitions.all()
|
||||||
|
|
||||||
|
if _user:
|
||||||
|
try:
|
||||||
|
Permission.check_permissions(
|
||||||
|
requester=_user, permissions=(
|
||||||
|
permission_workflow_transition,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except PermissionDenied:
|
||||||
|
try:
|
||||||
|
"""
|
||||||
|
Check for ACL access to the workflow, if true, allow
|
||||||
|
all transition options.
|
||||||
|
"""
|
||||||
|
|
||||||
|
AccessControlList.objects.check_access(
|
||||||
|
permissions=permission_workflow_transition,
|
||||||
|
user=_user, obj=self.workflow
|
||||||
|
)
|
||||||
|
except PermissionDenied:
|
||||||
|
"""
|
||||||
|
If not ACL access to the workflow, filter transition
|
||||||
|
options by each transition ACL access
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = AccessControlList.objects.filter_by_access(
|
||||||
|
permission=permission_workflow_transition,
|
||||||
|
user=_user, queryset=queryset
|
||||||
|
)
|
||||||
|
return queryset
|
||||||
else:
|
else:
|
||||||
"""
|
"""
|
||||||
This happens when a workflow has no initial state and a document
|
This happens when a workflow has no initial state and a document
|
||||||
@@ -212,7 +245,7 @@ class WorkflowInstanceLogEntry(models.Model):
|
|||||||
verbose_name_plural = _('Workflow instance log entries')
|
verbose_name_plural = _('Workflow instance log entries')
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
if self.transition not in self.workflow_instance.get_transition_choices():
|
if self.transition not in self.workflow_instance.get_transition_choices(_user=self.user):
|
||||||
raise ValidationError(_('Not a valid transition choice.'))
|
raise ValidationError(_('Not a valid transition choice.'))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
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
|
||||||
@@ -328,24 +329,6 @@ class WritableWorkflowInstanceLogEntrySerializer(serializers.ModelSerializer):
|
|||||||
)
|
)
|
||||||
model = WorkflowInstanceLogEntry
|
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):
|
def get_document_workflow_url(self, instance):
|
||||||
return reverse(
|
return reverse(
|
||||||
'rest_api:workflowinstance-detail', args=(
|
'rest_api:workflowinstance-detail', args=(
|
||||||
@@ -353,3 +336,19 @@ class WritableWorkflowInstanceLogEntrySerializer(serializers.ModelSerializer):
|
|||||||
instance.workflow_instance.pk,
|
instance.workflow_instance.pk,
|
||||||
), request=self.context['request'], format=self.context['format']
|
), request=self.context['request'], format=self.context['format']
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
attrs['user'] = self.context['request'].user
|
||||||
|
attrs['workflow_instance'] = self.context['workflow_instance']
|
||||||
|
attrs['transition'] = WorkflowTransition.objects.get(
|
||||||
|
pk=attrs.pop('transition_pk')
|
||||||
|
)
|
||||||
|
|
||||||
|
instance = WorkflowInstanceLogEntry(**attrs)
|
||||||
|
|
||||||
|
try:
|
||||||
|
instance.full_clean()
|
||||||
|
except DjangoValidationError as exception:
|
||||||
|
raise ValidationError(exception)
|
||||||
|
|
||||||
|
return attrs
|
||||||
|
|||||||
@@ -8,5 +8,6 @@ TEST_WORKFLOW_INSTANCE_LOG_ENTRY_COMMENT = 'test workflow instance log entry com
|
|||||||
TEST_WORKFLOW_STATE_LABEL = 'test state label'
|
TEST_WORKFLOW_STATE_LABEL = 'test state label'
|
||||||
TEST_WORKFLOW_STATE_LABEL_EDITED = 'test state label edited'
|
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 label'
|
TEST_WORKFLOW_TRANSITION_LABEL = 'test transition label'
|
||||||
TEST_WORKFLOW_TRANSITION_LABEL_EDITED = 'test transtition label edited'
|
TEST_WORKFLOW_TRANSITION_LABEL_2 = 'test transition label 2'
|
||||||
|
TEST_WORKFLOW_TRANSITION_LABEL_EDITED = 'test transition label edited'
|
||||||
|
|||||||
@@ -1,19 +1,28 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.auth.models import Group
|
||||||
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 rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from acls.models import AccessControlList
|
||||||
from documents.models import DocumentType
|
from documents.models import DocumentType
|
||||||
from documents.tests.literals import (
|
from documents.tests.literals import (
|
||||||
TEST_DOCUMENT_TYPE, TEST_SMALL_DOCUMENT_PATH
|
TEST_DOCUMENT_TYPE, TEST_SMALL_DOCUMENT_PATH
|
||||||
)
|
)
|
||||||
|
from permissions import Permission
|
||||||
|
from permissions.models import Role
|
||||||
|
from permissions.tests.literals import TEST_ROLE_LABEL
|
||||||
from rest_api.tests import BaseAPITestCase
|
from rest_api.tests import BaseAPITestCase
|
||||||
from user_management.tests.literals import (
|
from user_management.tests import (
|
||||||
TEST_ADMIN_EMAIL, TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME
|
TEST_ADMIN_EMAIL, TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, TEST_GROUP,
|
||||||
|
TEST_USER_EMAIL, TEST_USER_USERNAME, TEST_USER_PASSWORD
|
||||||
)
|
)
|
||||||
|
|
||||||
from ..models import Workflow
|
from ..models import Workflow
|
||||||
|
from ..permissions import permission_workflow_transition
|
||||||
|
|
||||||
from .literals import (
|
from .literals import (
|
||||||
TEST_WORKFLOW_LABEL, TEST_WORKFLOW_LABEL_EDITED,
|
TEST_WORKFLOW_LABEL, TEST_WORKFLOW_LABEL_EDITED,
|
||||||
@@ -634,3 +643,154 @@ class DocumentWorkflowsAPITestCase(BaseAPITestCase):
|
|||||||
response.data['results'][0]['transition']['label'],
|
response.data['results'][0]['transition']['label'],
|
||||||
TEST_WORKFLOW_TRANSITION_LABEL
|
TEST_WORKFLOW_TRANSITION_LABEL
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(OCR_AUTO_OCR=False)
|
||||||
|
class DocumentWorkflowsTransitionACLsAPITestCase(APITestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user = get_user_model().objects.create_user(
|
||||||
|
username=TEST_USER_USERNAME, email=TEST_USER_EMAIL,
|
||||||
|
password=TEST_USER_PASSWORD
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.login(
|
||||||
|
username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD
|
||||||
|
)
|
||||||
|
|
||||||
|
self.document_type = DocumentType.objects.create(
|
||||||
|
label=TEST_DOCUMENT_TYPE
|
||||||
|
)
|
||||||
|
|
||||||
|
self.group = Group.objects.create(name=TEST_GROUP)
|
||||||
|
self.role = Role.objects.create(label=TEST_ROLE_LABEL)
|
||||||
|
self.group.user_set.add(self.user)
|
||||||
|
self.role.groups.add(self.group)
|
||||||
|
Permission.invalidate_cache()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
if hasattr(self, 'document_type'):
|
||||||
|
self.document_type.delete()
|
||||||
|
|
||||||
|
def _create_document(self):
|
||||||
|
with open(TEST_SMALL_DOCUMENT_PATH) as file_object:
|
||||||
|
self.document = self.document_type.new_document(
|
||||||
|
file_object=file_object
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_workflow(self):
|
||||||
|
self.workflow = Workflow.objects.create(label=TEST_WORKFLOW_LABEL)
|
||||||
|
self.workflow.document_types.add(self.document_type)
|
||||||
|
|
||||||
|
def _create_workflow_states(self):
|
||||||
|
self._create_workflow()
|
||||||
|
self.workflow_state_1 = self.workflow.states.create(
|
||||||
|
completion=TEST_WORKFLOW_INITIAL_STATE_COMPLETION,
|
||||||
|
initial=True, label=TEST_WORKFLOW_INITIAL_STATE_LABEL
|
||||||
|
)
|
||||||
|
self.workflow_state_2 = self.workflow.states.create(
|
||||||
|
completion=TEST_WORKFLOW_STATE_COMPLETION,
|
||||||
|
label=TEST_WORKFLOW_STATE_LABEL
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_workflow_transition(self):
|
||||||
|
self._create_workflow_states()
|
||||||
|
self.workflow_transition = self.workflow.transitions.create(
|
||||||
|
label=TEST_WORKFLOW_TRANSITION_LABEL,
|
||||||
|
origin_state=self.workflow_state_1,
|
||||||
|
destination_state=self.workflow_state_2,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_workflow_transition_view_no_permission(self):
|
||||||
|
self._create_workflow_transition()
|
||||||
|
self._create_document()
|
||||||
|
|
||||||
|
workflow_instance = self.document.workflows.first()
|
||||||
|
|
||||||
|
self.client.post(
|
||||||
|
reverse(
|
||||||
|
'rest_api:workflowinstancelogentry-list', args=(
|
||||||
|
self.document.pk, workflow_instance.pk
|
||||||
|
),
|
||||||
|
), data={'transition_pk': self.workflow_transition.pk}
|
||||||
|
)
|
||||||
|
|
||||||
|
workflow_instance.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(workflow_instance.log_entries.count(), 0)
|
||||||
|
|
||||||
|
def test_workflow_transition_view_with_permission(self):
|
||||||
|
self._create_workflow_transition()
|
||||||
|
self._create_document()
|
||||||
|
|
||||||
|
workflow_instance = self.document.workflows.first()
|
||||||
|
|
||||||
|
self.role.permissions.add(
|
||||||
|
permission_workflow_transition.stored_permission
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.post(
|
||||||
|
reverse(
|
||||||
|
'rest_api:workflowinstancelogentry-list', args=(
|
||||||
|
self.document.pk, workflow_instance.pk
|
||||||
|
),
|
||||||
|
), data={'transition_pk': self.workflow_transition.pk}
|
||||||
|
)
|
||||||
|
|
||||||
|
workflow_instance.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
workflow_instance.log_entries.first().transition.label,
|
||||||
|
TEST_WORKFLOW_TRANSITION_LABEL
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_workflow_transition_view_with_workflow_acl(self):
|
||||||
|
self._create_workflow_transition()
|
||||||
|
self._create_document()
|
||||||
|
|
||||||
|
workflow_instance = self.document.workflows.first()
|
||||||
|
|
||||||
|
acl = AccessControlList.objects.create(
|
||||||
|
content_object=self.workflow, role=self.role
|
||||||
|
)
|
||||||
|
acl.permissions.add(permission_workflow_transition.stored_permission)
|
||||||
|
|
||||||
|
self.client.post(
|
||||||
|
reverse(
|
||||||
|
'rest_api:workflowinstancelogentry-list', args=(
|
||||||
|
self.document.pk, workflow_instance.pk
|
||||||
|
),
|
||||||
|
), data={'transition_pk': self.workflow_transition.pk}
|
||||||
|
)
|
||||||
|
|
||||||
|
workflow_instance.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
workflow_instance.log_entries.first().transition.label,
|
||||||
|
TEST_WORKFLOW_TRANSITION_LABEL
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_workflow_transition_view_transition_acl(self):
|
||||||
|
self._create_workflow_transition()
|
||||||
|
self._create_document()
|
||||||
|
|
||||||
|
workflow_instance = self.document.workflows.first()
|
||||||
|
|
||||||
|
acl = AccessControlList.objects.create(
|
||||||
|
content_object=self.workflow_transition, role=self.role
|
||||||
|
)
|
||||||
|
acl.permissions.add(permission_workflow_transition.stored_permission)
|
||||||
|
|
||||||
|
self.client.post(
|
||||||
|
reverse(
|
||||||
|
'rest_api:workflowinstancelogentry-list', args=(
|
||||||
|
self.document.pk, workflow_instance.pk
|
||||||
|
),
|
||||||
|
), data={'transition_pk': self.workflow_transition.pk}
|
||||||
|
)
|
||||||
|
|
||||||
|
workflow_instance.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
workflow_instance.log_entries.first().transition.label,
|
||||||
|
TEST_WORKFLOW_TRANSITION_LABEL
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from acls.models import AccessControlList
|
||||||
from common.tests.test_views import GenericViewTestCase
|
from common.tests.test_views import GenericViewTestCase
|
||||||
|
from documents.tests.literals import TEST_SMALL_DOCUMENT_PATH
|
||||||
from documents.tests.test_views import GenericDocumentViewTestCase
|
from documents.tests.test_views import GenericDocumentViewTestCase
|
||||||
|
|
||||||
from ..models import Workflow, WorkflowState, WorkflowTransition
|
from ..models import Workflow, WorkflowState, WorkflowTransition
|
||||||
from ..permissions import permission_workflow_tools
|
from ..permissions import (
|
||||||
|
permission_workflow_tools, permission_workflow_transition
|
||||||
|
)
|
||||||
|
|
||||||
from .literals import (
|
from .literals import (
|
||||||
TEST_WORKFLOW_LABEL, TEST_WORKFLOW_INITIAL_STATE_LABEL,
|
TEST_WORKFLOW_LABEL, TEST_WORKFLOW_INITIAL_STATE_LABEL,
|
||||||
TEST_WORKFLOW_INITIAL_STATE_COMPLETION, TEST_WORKFLOW_STATE_LABEL,
|
TEST_WORKFLOW_INITIAL_STATE_COMPLETION, TEST_WORKFLOW_STATE_LABEL,
|
||||||
TEST_WORKFLOW_STATE_COMPLETION, TEST_WORKFLOW_TRANSITION_LABEL
|
TEST_WORKFLOW_STATE_COMPLETION, TEST_WORKFLOW_TRANSITION_LABEL,
|
||||||
|
TEST_WORKFLOW_TRANSITION_LABEL_2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -19,6 +24,26 @@ class DocumentStateViewTestCase(GenericViewTestCase):
|
|||||||
|
|
||||||
self.login_admin_user()
|
self.login_admin_user()
|
||||||
|
|
||||||
|
def _create_workflow(self):
|
||||||
|
self.workflow = Workflow.objects.create(label=TEST_WORKFLOW_LABEL)
|
||||||
|
|
||||||
|
def _create_workflow_states(self):
|
||||||
|
self.workflow_initial_state = WorkflowState.objects.create(
|
||||||
|
workflow=self.workflow, label=TEST_WORKFLOW_INITIAL_STATE_LABEL,
|
||||||
|
completion=TEST_WORKFLOW_INITIAL_STATE_COMPLETION, initial=True
|
||||||
|
)
|
||||||
|
self.workflow_state = WorkflowState.objects.create(
|
||||||
|
workflow=self.workflow, label=TEST_WORKFLOW_STATE_LABEL,
|
||||||
|
completion=TEST_WORKFLOW_STATE_COMPLETION
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_workflow_transition(self):
|
||||||
|
self.workflow_transition = WorkflowTransition.objects.create(
|
||||||
|
workflow=self.workflow, label=TEST_WORKFLOW_TRANSITION_LABEL,
|
||||||
|
origin_state=self.workflow_initial_state,
|
||||||
|
destination_state=self.workflow_state
|
||||||
|
)
|
||||||
|
|
||||||
def test_creating_workflow(self):
|
def test_creating_workflow(self):
|
||||||
response = self.post(
|
response = self.post(
|
||||||
'document_states:setup_workflow_create',
|
'document_states:setup_workflow_create',
|
||||||
@@ -33,13 +58,10 @@ class DocumentStateViewTestCase(GenericViewTestCase):
|
|||||||
self.assertEquals(Workflow.objects.all()[0].label, TEST_WORKFLOW_LABEL)
|
self.assertEquals(Workflow.objects.all()[0].label, TEST_WORKFLOW_LABEL)
|
||||||
|
|
||||||
def test_delete_workflow(self):
|
def test_delete_workflow(self):
|
||||||
workflow = Workflow.objects.create(label=TEST_WORKFLOW_LABEL)
|
self._create_workflow()
|
||||||
|
|
||||||
self.assertEquals(Workflow.objects.count(), 1)
|
|
||||||
self.assertEquals(Workflow.objects.all()[0].label, TEST_WORKFLOW_LABEL)
|
|
||||||
|
|
||||||
response = self.post(
|
response = self.post(
|
||||||
'document_states:setup_workflow_delete', args=(workflow.pk,),
|
'document_states:setup_workflow_delete', args=(self.workflow.pk,),
|
||||||
follow=True
|
follow=True
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -48,11 +70,11 @@ class DocumentStateViewTestCase(GenericViewTestCase):
|
|||||||
self.assertEquals(Workflow.objects.count(), 0)
|
self.assertEquals(Workflow.objects.count(), 0)
|
||||||
|
|
||||||
def test_create_workflow_state(self):
|
def test_create_workflow_state(self):
|
||||||
workflow = Workflow.objects.create(label=TEST_WORKFLOW_LABEL)
|
self._create_workflow()
|
||||||
|
|
||||||
response = self.post(
|
response = self.post(
|
||||||
'document_states:setup_workflow_state_create',
|
'document_states:setup_workflow_state_create',
|
||||||
args=(workflow.pk,),
|
args=(self.workflow.pk,),
|
||||||
data={
|
data={
|
||||||
'label': TEST_WORKFLOW_STATE_LABEL,
|
'label': TEST_WORKFLOW_STATE_LABEL,
|
||||||
'completion': TEST_WORKFLOW_STATE_COMPLETION,
|
'completion': TEST_WORKFLOW_STATE_COMPLETION,
|
||||||
@@ -71,39 +93,29 @@ class DocumentStateViewTestCase(GenericViewTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_delete_workflow_state(self):
|
def test_delete_workflow_state(self):
|
||||||
workflow = Workflow.objects.create(label=TEST_WORKFLOW_LABEL)
|
self._create_workflow()
|
||||||
workflow_state = WorkflowState.objects.create(
|
self._create_workflow_states()
|
||||||
workflow=workflow, label=TEST_WORKFLOW_STATE_LABEL,
|
|
||||||
completion=TEST_WORKFLOW_STATE_COMPLETION
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self.post(
|
response = self.post(
|
||||||
'document_states:setup_workflow_state_delete',
|
'document_states:setup_workflow_state_delete',
|
||||||
args=(workflow_state.pk,), follow=True
|
args=(self.workflow_state.pk,), follow=True
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEquals(response.status_code, 200)
|
||||||
|
|
||||||
self.assertEquals(WorkflowState.objects.count(), 0)
|
self.assertEquals(WorkflowState.objects.count(), 1)
|
||||||
self.assertEquals(Workflow.objects.count(), 1)
|
self.assertEquals(Workflow.objects.count(), 1)
|
||||||
|
|
||||||
def test_create_workflow_transition(self):
|
def test_create_workflow_transition(self):
|
||||||
workflow = Workflow.objects.create(label=TEST_WORKFLOW_LABEL)
|
self._create_workflow()
|
||||||
workflow_initial_state = WorkflowState.objects.create(
|
self._create_workflow_states()
|
||||||
workflow=workflow, label=TEST_WORKFLOW_INITIAL_STATE_LABEL,
|
|
||||||
completion=TEST_WORKFLOW_INITIAL_STATE_COMPLETION, initial=True
|
|
||||||
)
|
|
||||||
workflow_state = WorkflowState.objects.create(
|
|
||||||
workflow=workflow, label=TEST_WORKFLOW_STATE_LABEL,
|
|
||||||
completion=TEST_WORKFLOW_STATE_COMPLETION
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self.post(
|
response = self.post(
|
||||||
'document_states:setup_workflow_transition_create',
|
'document_states:setup_workflow_transition_create',
|
||||||
args=(workflow.pk,), data={
|
args=(self.workflow.pk,), data={
|
||||||
'label': TEST_WORKFLOW_TRANSITION_LABEL,
|
'label': TEST_WORKFLOW_TRANSITION_LABEL,
|
||||||
'origin_state': workflow_initial_state.pk,
|
'origin_state': self.workflow_initial_state.pk,
|
||||||
'destination_state': workflow_state.pk,
|
'destination_state': self.workflow_state.pk,
|
||||||
}, follow=True
|
}, follow=True
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -116,34 +128,21 @@ class DocumentStateViewTestCase(GenericViewTestCase):
|
|||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEquals(
|
||||||
WorkflowTransition.objects.all()[0].origin_state,
|
WorkflowTransition.objects.all()[0].origin_state,
|
||||||
workflow_initial_state
|
self.workflow_initial_state
|
||||||
)
|
)
|
||||||
self.assertEquals(
|
self.assertEquals(
|
||||||
WorkflowTransition.objects.all()[0].destination_state,
|
WorkflowTransition.objects.all()[0].destination_state,
|
||||||
workflow_state
|
self.workflow_state
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_delete_workflow_transition(self):
|
def test_delete_workflow_transition(self):
|
||||||
workflow = Workflow.objects.create(label=TEST_WORKFLOW_LABEL)
|
self._create_workflow()
|
||||||
workflow_initial_state = WorkflowState.objects.create(
|
self._create_workflow_states()
|
||||||
workflow=workflow, label=TEST_WORKFLOW_INITIAL_STATE_LABEL,
|
self._create_workflow_transition()
|
||||||
completion=TEST_WORKFLOW_INITIAL_STATE_COMPLETION, initial=True
|
|
||||||
)
|
|
||||||
workflow_state = WorkflowState.objects.create(
|
|
||||||
workflow=workflow, label=TEST_WORKFLOW_STATE_LABEL,
|
|
||||||
completion=TEST_WORKFLOW_STATE_COMPLETION
|
|
||||||
)
|
|
||||||
workflow_transition = WorkflowTransition.objects.create(
|
|
||||||
workflow=workflow, label=TEST_WORKFLOW_TRANSITION_LABEL,
|
|
||||||
origin_state=workflow_initial_state,
|
|
||||||
destination_state=workflow_state
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEquals(WorkflowTransition.objects.count(), 1)
|
|
||||||
|
|
||||||
response = self.post(
|
response = self.post(
|
||||||
'document_states:setup_workflow_transition_delete',
|
'document_states:setup_workflow_transition_delete',
|
||||||
args=(workflow_transition.pk,), follow=True
|
args=(self.workflow_transition.pk,), follow=True
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEquals(response.status_code, 200)
|
self.assertEquals(response.status_code, 200)
|
||||||
@@ -206,3 +205,152 @@ class DocumentStateToolViewTestCase(GenericDocumentViewTestCase):
|
|||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.document.workflows.first().workflow, self.workflow
|
self.document.workflows.first().workflow, self.workflow
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentStateTransitionViewTestCase(GenericDocumentViewTestCase):
|
||||||
|
def _create_workflow(self):
|
||||||
|
self.workflow = Workflow.objects.create(label=TEST_WORKFLOW_LABEL)
|
||||||
|
self.workflow.document_types.add(self.document_type)
|
||||||
|
|
||||||
|
def _create_workflow_states(self):
|
||||||
|
self.workflow_initial_state = WorkflowState.objects.create(
|
||||||
|
workflow=self.workflow, label=TEST_WORKFLOW_INITIAL_STATE_LABEL,
|
||||||
|
completion=TEST_WORKFLOW_INITIAL_STATE_COMPLETION, initial=True
|
||||||
|
)
|
||||||
|
self.workflow_state = WorkflowState.objects.create(
|
||||||
|
workflow=self.workflow, label=TEST_WORKFLOW_STATE_LABEL,
|
||||||
|
completion=TEST_WORKFLOW_STATE_COMPLETION
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_workflow_transitions(self):
|
||||||
|
self.workflow_transition = WorkflowTransition.objects.create(
|
||||||
|
workflow=self.workflow, label=TEST_WORKFLOW_TRANSITION_LABEL,
|
||||||
|
origin_state=self.workflow_initial_state,
|
||||||
|
destination_state=self.workflow_state
|
||||||
|
)
|
||||||
|
|
||||||
|
self.workflow_transition_2 = WorkflowTransition.objects.create(
|
||||||
|
workflow=self.workflow, label=TEST_WORKFLOW_TRANSITION_LABEL_2,
|
||||||
|
origin_state=self.workflow_initial_state,
|
||||||
|
destination_state=self.workflow_state
|
||||||
|
)
|
||||||
|
|
||||||
|
def _create_document(self):
|
||||||
|
with open(TEST_SMALL_DOCUMENT_PATH) as file_object:
|
||||||
|
self.document_2 = self.document_type.new_document(
|
||||||
|
file_object=file_object
|
||||||
|
)
|
||||||
|
|
||||||
|
def _request_workflow_transition(self, workflow_instance):
|
||||||
|
return self.post(
|
||||||
|
'document_states:workflow_instance_transition',
|
||||||
|
args=(workflow_instance.pk,), data={
|
||||||
|
'transition': self.workflow_transition.pk,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_transition_workflow_no_permission(self):
|
||||||
|
self.login_user()
|
||||||
|
self._create_workflow()
|
||||||
|
self._create_workflow_states()
|
||||||
|
self._create_workflow_transitions()
|
||||||
|
self._create_document()
|
||||||
|
|
||||||
|
workflow_instance = self.document_2.workflows.first()
|
||||||
|
|
||||||
|
response = self._request_workflow_transition(
|
||||||
|
workflow_instance=workflow_instance
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
# Workflow should remain in the same initial state
|
||||||
|
self.assertEqual(
|
||||||
|
workflow_instance.get_current_state(), self.workflow_initial_state
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_transition_workflow_with_permission(self):
|
||||||
|
"""
|
||||||
|
Test transitioning a workflow by granting the transition workflow
|
||||||
|
permission to the role.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.login_user()
|
||||||
|
self._create_workflow()
|
||||||
|
self._create_workflow_states()
|
||||||
|
self._create_workflow_transitions()
|
||||||
|
self._create_document()
|
||||||
|
|
||||||
|
workflow_instance = self.document_2.workflows.first()
|
||||||
|
|
||||||
|
self.grant(permission_workflow_transition)
|
||||||
|
response = self._request_workflow_transition(
|
||||||
|
workflow_instance=workflow_instance
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
# Workflow should remain in the same initial state
|
||||||
|
self.assertEqual(
|
||||||
|
workflow_instance.get_current_state(), self.workflow_state
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_transition_workflow_with_workflow_acl(self):
|
||||||
|
"""
|
||||||
|
Test transitioning a workflow by granting the transition workflow
|
||||||
|
permission to the workflow itself via ACL.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.login_user()
|
||||||
|
self._create_workflow()
|
||||||
|
self._create_workflow_states()
|
||||||
|
self._create_workflow_transitions()
|
||||||
|
self._create_document()
|
||||||
|
|
||||||
|
workflow_instance = self.document_2.workflows.first()
|
||||||
|
|
||||||
|
acl = AccessControlList.objects.create(
|
||||||
|
content_object=self.workflow, role=self.role
|
||||||
|
)
|
||||||
|
acl.permissions.add(permission_workflow_transition.stored_permission)
|
||||||
|
|
||||||
|
response = self._request_workflow_transition(
|
||||||
|
workflow_instance=workflow_instance
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
# Workflow should remain in the same initial state
|
||||||
|
self.assertEqual(
|
||||||
|
workflow_instance.get_current_state(), self.workflow_state
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_transition_workflow_with_transition_acl(self):
|
||||||
|
"""
|
||||||
|
Test transitioning a workflow by granting the transition workflow
|
||||||
|
permission to the transition via ACL.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.login_user()
|
||||||
|
self._create_workflow()
|
||||||
|
self._create_workflow_states()
|
||||||
|
self._create_workflow_transitions()
|
||||||
|
self._create_document()
|
||||||
|
|
||||||
|
workflow_instance = self.document_2.workflows.first()
|
||||||
|
|
||||||
|
acl = AccessControlList.objects.create(
|
||||||
|
content_object=self.workflow_transition, role=self.role
|
||||||
|
)
|
||||||
|
acl.permissions.add(permission_workflow_transition.stored_permission)
|
||||||
|
|
||||||
|
response = self._request_workflow_transition(
|
||||||
|
workflow_instance=workflow_instance
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
||||||
|
# Workflow should remain in the same initial state
|
||||||
|
self.assertEqual(
|
||||||
|
workflow_instance.get_current_state(), self.workflow_state
|
||||||
|
)
|
||||||
|
|||||||
@@ -7,11 +7,10 @@ from django.db.utils import IntegrityError
|
|||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from django.views.generic import FormView
|
|
||||||
|
|
||||||
from acls.models import AccessControlList
|
from acls.models import AccessControlList
|
||||||
from common.views import (
|
from common.views import (
|
||||||
AssignRemoveView, ConfirmView, SingleObjectCreateView,
|
AssignRemoveView, ConfirmView, FormView, SingleObjectCreateView,
|
||||||
SingleObjectDeleteView, SingleObjectEditView, SingleObjectListView
|
SingleObjectDeleteView, SingleObjectEditView, SingleObjectListView
|
||||||
)
|
)
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
@@ -27,8 +26,8 @@ from .models import (
|
|||||||
)
|
)
|
||||||
from .permissions import (
|
from .permissions import (
|
||||||
permission_workflow_create, permission_workflow_delete,
|
permission_workflow_create, permission_workflow_delete,
|
||||||
permission_workflow_edit, permission_workflow_transition,
|
permission_workflow_edit, permission_workflow_tools,
|
||||||
permission_workflow_tools, permission_workflow_view,
|
permission_workflow_view,
|
||||||
)
|
)
|
||||||
from .tasks import task_launch_all_workflows
|
from .tasks import task_launch_all_workflows
|
||||||
|
|
||||||
@@ -93,23 +92,10 @@ class WorkflowInstanceTransitionView(FormView):
|
|||||||
form_class = WorkflowInstanceTransitionForm
|
form_class = WorkflowInstanceTransitionForm
|
||||||
template_name = 'appearance/generic_form.html'
|
template_name = 'appearance/generic_form.html'
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
AccessControlList.objects.check_access(
|
|
||||||
permissions=permission_workflow_transition, user=request.user,
|
|
||||||
obj=self.get_workflow_instance().document
|
|
||||||
)
|
|
||||||
|
|
||||||
return super(
|
|
||||||
WorkflowInstanceTransitionView, self
|
|
||||||
).dispatch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
transition = self.get_workflow_instance().workflow.transitions.get(
|
|
||||||
pk=form.cleaned_data['transition']
|
|
||||||
)
|
|
||||||
self.get_workflow_instance().do_transition(
|
self.get_workflow_instance().do_transition(
|
||||||
comment=form.cleaned_data['comment'], transition=transition,
|
comment=form.cleaned_data['comment'],
|
||||||
user=self.request.user
|
transition=form.cleaned_data['transition'], user=self.request.user
|
||||||
)
|
)
|
||||||
return HttpResponseRedirect(self.get_success_url())
|
return HttpResponseRedirect(self.get_success_url())
|
||||||
|
|
||||||
@@ -124,10 +110,11 @@ class WorkflowInstanceTransitionView(FormView):
|
|||||||
'workflow_instance': self.get_workflow_instance(),
|
'workflow_instance': self.get_workflow_instance(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
def get_form_extra_kwargs(self):
|
||||||
kwargs = super(WorkflowInstanceTransitionView, self).get_form_kwargs()
|
return {
|
||||||
kwargs['workflow'] = self.get_workflow_instance()
|
'user': self.request.user,
|
||||||
return kwargs
|
'workflow_instance': self.get_workflow_instance()
|
||||||
|
}
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return self.get_workflow_instance().get_absolute_url()
|
return self.get_workflow_instance().get_absolute_url()
|
||||||
|
|||||||
@@ -79,10 +79,11 @@ class APIDeletedDocumentRestoreView(generics.GenericAPIView):
|
|||||||
mayan_object_permissions = {
|
mayan_object_permissions = {
|
||||||
'POST': (permission_document_restore,)
|
'POST': (permission_document_restore,)
|
||||||
}
|
}
|
||||||
|
|
||||||
permission_classes = (MayanPermission,)
|
permission_classes = (MayanPermission,)
|
||||||
queryset = Document.trash.all()
|
queryset = Document.trash.all()
|
||||||
serializer_class = DeletedDocumentSerializer
|
|
||||||
|
def get_serializer_class(self):
|
||||||
|
return None
|
||||||
|
|
||||||
def post(self, *args, **kwargs):
|
def post(self, *args, **kwargs):
|
||||||
self.get_object().restore()
|
self.get_object().restore()
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ class DocumentVersionSerializer(serializers.HyperlinkedModelSerializer):
|
|||||||
document_url = serializers.SerializerMethodField()
|
document_url = serializers.SerializerMethodField()
|
||||||
download_url = serializers.SerializerMethodField()
|
download_url = serializers.SerializerMethodField()
|
||||||
pages_url = serializers.SerializerMethodField()
|
pages_url = serializers.SerializerMethodField()
|
||||||
|
size = serializers.SerializerMethodField()
|
||||||
url = serializers.SerializerMethodField()
|
url = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -111,7 +112,10 @@ class DocumentVersionSerializer(serializers.HyperlinkedModelSerializer):
|
|||||||
'file', 'mimetype', 'pages_url', 'timestamp', 'url'
|
'file', 'mimetype', 'pages_url', 'timestamp', 'url'
|
||||||
)
|
)
|
||||||
model = DocumentVersion
|
model = DocumentVersion
|
||||||
read_only_fields = ('document', 'file')
|
read_only_fields = ('document', 'file', 'size')
|
||||||
|
|
||||||
|
def get_size(self, instance):
|
||||||
|
return instance.size
|
||||||
|
|
||||||
def get_document_url(self, instance):
|
def get_document_url(self, instance):
|
||||||
return reverse(
|
return reverse(
|
||||||
@@ -210,9 +214,6 @@ class DeletedDocumentSerializer(serializers.HyperlinkedModelSerializer):
|
|||||||
view_name='rest_api:trasheddocument-restore'
|
view_name='rest_api:trasheddocument-restore'
|
||||||
)
|
)
|
||||||
|
|
||||||
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'},
|
||||||
@@ -229,6 +230,9 @@ class DeletedDocumentSerializer(serializers.HyperlinkedModelSerializer):
|
|||||||
'language'
|
'language'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_document_type_label(self, instance):
|
||||||
|
return instance.document_type.label
|
||||||
|
|
||||||
|
|
||||||
class DocumentSerializer(serializers.HyperlinkedModelSerializer):
|
class DocumentSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
document_type = DocumentTypeSerializer()
|
document_type = DocumentTypeSerializer()
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
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 rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
from documents.serializers import DocumentSerializer
|
from documents.models import DocumentType
|
||||||
|
from documents.serializers import DocumentSerializer, DocumentTypeSerializer
|
||||||
|
|
||||||
from .models import SmartLink, SmartLinkCondition
|
from .models import SmartLink, SmartLinkCondition
|
||||||
|
|
||||||
@@ -41,13 +45,15 @@ class SmartLinkSerializer(serializers.HyperlinkedModelSerializer):
|
|||||||
conditions_url = serializers.HyperlinkedIdentityField(
|
conditions_url = serializers.HyperlinkedIdentityField(
|
||||||
view_name='rest_api:smartlinkcondition-list'
|
view_name='rest_api:smartlinkcondition-list'
|
||||||
)
|
)
|
||||||
|
document_types = DocumentTypeSerializer(read_only=True, many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'url': {'view_name': 'rest_api:smartlink-detail'},
|
'url': {'view_name': 'rest_api:smartlink-detail'},
|
||||||
}
|
}
|
||||||
fields = (
|
fields = (
|
||||||
'conditions_url', 'dynamic_label', 'enabled', 'label', 'id', 'url'
|
'conditions_url', 'document_types', 'dynamic_label', 'enabled',
|
||||||
|
'label', 'id', 'url'
|
||||||
)
|
)
|
||||||
model = SmartLink
|
model = SmartLink
|
||||||
|
|
||||||
@@ -104,12 +110,38 @@ class WritableSmartLinkSerializer(serializers.ModelSerializer):
|
|||||||
conditions_url = serializers.HyperlinkedIdentityField(
|
conditions_url = serializers.HyperlinkedIdentityField(
|
||||||
view_name='rest_api:smartlinkcondition-list'
|
view_name='rest_api:smartlinkcondition-list'
|
||||||
)
|
)
|
||||||
|
document_types_pk_list = serializers.CharField(
|
||||||
|
help_text=_(
|
||||||
|
'Comma separated list of document type primary keys to which this '
|
||||||
|
'smart link will be attached.'
|
||||||
|
), required=False
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'url': {'view_name': 'rest_api:smartlink-detail'},
|
'url': {'view_name': 'rest_api:smartlink-detail'},
|
||||||
}
|
}
|
||||||
fields = (
|
fields = (
|
||||||
'conditions_url', 'dynamic_label', 'enabled', 'label', 'id', 'url'
|
'conditions_url', 'document_types_pk_list', 'dynamic_label',
|
||||||
|
'enabled', 'label', 'id', 'url'
|
||||||
)
|
)
|
||||||
model = SmartLink
|
model = SmartLink
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
document_types_pk_list = attrs.pop('document_types_pk_list', None)
|
||||||
|
document_types_result = []
|
||||||
|
|
||||||
|
if document_types_pk_list:
|
||||||
|
for pk in document_types_pk_list.split(','):
|
||||||
|
try:
|
||||||
|
document_type = DocumentType.objects.get(pk=pk)
|
||||||
|
except DocumentType.DoesNotExist:
|
||||||
|
raise ValidationError(_('No such document type: %s') % pk)
|
||||||
|
else:
|
||||||
|
# Accumulate valid stored document_type pks
|
||||||
|
document_types_result.append(document_type.pk)
|
||||||
|
|
||||||
|
attrs['document_types'] = DocumentType.objects.filter(
|
||||||
|
pk__in=document_types_result
|
||||||
|
)
|
||||||
|
return attrs
|
||||||
|
|||||||
@@ -73,6 +73,26 @@ class SmartLinkAPITestCase(BaseAPITestCase):
|
|||||||
self.assertEqual(SmartLink.objects.count(), 1)
|
self.assertEqual(SmartLink.objects.count(), 1)
|
||||||
self.assertEqual(smart_link.label, TEST_SMART_LINK_LABEL)
|
self.assertEqual(smart_link.label, TEST_SMART_LINK_LABEL)
|
||||||
|
|
||||||
|
def test_smart_link_create_with_document_types_view(self):
|
||||||
|
self._create_document_type()
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('rest_api:smartlink-list'), data={
|
||||||
|
'label': TEST_SMART_LINK_LABEL,
|
||||||
|
'document_types_pk_list': self.document_type.pk
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
self.assertQuerysetEqual(
|
||||||
|
smart_link.document_types.all(), (repr(self.document_type),)
|
||||||
|
)
|
||||||
|
|
||||||
def test_smart_link_delete_view(self):
|
def test_smart_link_delete_view(self):
|
||||||
smart_link = self._create_smart_link()
|
smart_link = self._create_smart_link()
|
||||||
|
|
||||||
@@ -94,18 +114,23 @@ class SmartLinkAPITestCase(BaseAPITestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_smart_link_patch_view(self):
|
def test_smart_link_patch_view(self):
|
||||||
|
self._create_document_type()
|
||||||
smart_link = self._create_smart_link()
|
smart_link = self._create_smart_link()
|
||||||
|
|
||||||
self.client.patch(
|
self.client.patch(
|
||||||
reverse('rest_api:smartlink-detail', args=(smart_link.pk,)),
|
reverse('rest_api:smartlink-detail', args=(smart_link.pk,)),
|
||||||
data={
|
data={
|
||||||
'label': TEST_SMART_LINK_LABEL_EDITED,
|
'label': TEST_SMART_LINK_LABEL_EDITED,
|
||||||
|
'document_types_pk_list': self.document_type.pk
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
smart_link.refresh_from_db()
|
smart_link.refresh_from_db()
|
||||||
|
|
||||||
self.assertEqual(smart_link.label, TEST_SMART_LINK_LABEL_EDITED)
|
self.assertEqual(smart_link.label, TEST_SMART_LINK_LABEL_EDITED)
|
||||||
|
self.assertQuerysetEqual(
|
||||||
|
smart_link.document_types.all(), (repr(self.document_type),)
|
||||||
|
)
|
||||||
|
|
||||||
def test_smart_link_put_view(self):
|
def test_smart_link_put_view(self):
|
||||||
smart_link = self._create_smart_link()
|
smart_link = self._create_smart_link()
|
||||||
|
|||||||
@@ -66,8 +66,9 @@ class Permission(object):
|
|||||||
if permissions.stored_permission.requester_has_this(requester):
|
if permissions.stored_permission.requester_has_this(requester):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
logger.debug('no permission')
|
logger.debug('User "%s" does not have permissions "%s"',
|
||||||
|
requester,
|
||||||
|
permissions)
|
||||||
raise PermissionDenied(_('Insufficient permissions.'))
|
raise PermissionDenied(_('Insufficient permissions.'))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -46,17 +46,25 @@ class StoredPermission(models.Model):
|
|||||||
verbose_name_plural = _('Permissions')
|
verbose_name_plural = _('Permissions')
|
||||||
|
|
||||||
def requester_has_this(self, user):
|
def requester_has_this(self, user):
|
||||||
logger.debug('user: %s', user)
|
|
||||||
if user.is_superuser or user.is_staff:
|
if user.is_superuser or user.is_staff:
|
||||||
|
logger.debug('Permission "%s" granted to user "%s" as superuser or staff',
|
||||||
|
self,
|
||||||
|
user)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# Request is one of the permission's holders?
|
# Request is one of the permission's holders?
|
||||||
for group in user.groups.all():
|
for group in user.groups.all():
|
||||||
for role in group.roles.all():
|
for role in group.roles.all():
|
||||||
if self in role.permissions.all():
|
if self in role.permissions.all():
|
||||||
|
logger.debug('Permission "%s" granted to user "%s" through role "%s"',
|
||||||
|
self,
|
||||||
|
user,
|
||||||
|
role)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
logger.debug('Fallthru')
|
logger.debug('Fallthru: Permission "%s" not granted to user "%s"',
|
||||||
|
self,
|
||||||
|
user)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ from .models import Role, StoredPermission
|
|||||||
|
|
||||||
|
|
||||||
class PermissionSerializer(serializers.Serializer):
|
class PermissionSerializer(serializers.Serializer):
|
||||||
namespace = serializers.CharField()
|
namespace = serializers.CharField(read_only=True)
|
||||||
pk = serializers.CharField()
|
pk = serializers.CharField(read_only=True)
|
||||||
label = serializers.CharField()
|
label = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
if isinstance(instance, StoredPermission):
|
if isinstance(instance, StoredPermission):
|
||||||
@@ -33,7 +33,10 @@ class RoleSerializer(serializers.HyperlinkedModelSerializer):
|
|||||||
permissions = PermissionSerializer(many=True, read_only=True)
|
permissions = PermissionSerializer(many=True, read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
fields = ('id', 'label', 'groups', 'permissions')
|
extra_kwargs = {
|
||||||
|
'url': {'view_name': 'rest_api:role-detail'},
|
||||||
|
}
|
||||||
|
fields = ('groups', 'id', 'label', 'permissions', 'url')
|
||||||
model = Role
|
model = Role
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -257,7 +257,13 @@ class UploadInteractiveView(UploadBaseView):
|
|||||||
'shortly.'
|
'shortly.'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return HttpResponseRedirect(self.request.get_full_path())
|
|
||||||
|
return HttpResponseRedirect(
|
||||||
|
'{}?{}'.format(
|
||||||
|
reverse(self.request.resolver_match.view_name),
|
||||||
|
self.request.META['QUERY_STRING']
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def create_source_form_form(self, **kwargs):
|
def create_source_form_form(self, **kwargs):
|
||||||
return self.get_form_classes()['source_form'](
|
return self.get_form_classes()['source_form'](
|
||||||
@@ -298,7 +304,10 @@ class UploadInteractiveView(UploadBaseView):
|
|||||||
if not isinstance(self.source, StagingFolderSource):
|
if not isinstance(self.source, StagingFolderSource):
|
||||||
context['subtemplates_list'][0]['context'].update(
|
context['subtemplates_list'][0]['context'].update(
|
||||||
{
|
{
|
||||||
'form_action': self.request.get_full_path(),
|
'form_action': '{}?{}'.format(
|
||||||
|
reverse(self.request.resolver_match.view_name),
|
||||||
|
self.request.META['QUERY_STRING']
|
||||||
|
),
|
||||||
'form_class': 'dropzone',
|
'form_class': 'dropzone',
|
||||||
'form_disable_submit': True,
|
'form_disable_submit': True,
|
||||||
'form_id': 'html5upload',
|
'form_id': 'html5upload',
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from celery import Celery
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
from .runtime import celery_class
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mayan.settings.production')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mayan.settings.production')
|
||||||
|
|
||||||
app = Celery('mayan')
|
app = celery_class('mayan')
|
||||||
|
|
||||||
app.config_from_object('django.conf:settings')
|
app.config_from_object('django.conf:settings')
|
||||||
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
|
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
|
||||||
|
|||||||
18
mayan/conf.py
Normal file
18
mayan/conf.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
"""
|
||||||
|
This module should be called settings.py but is named conf.py to avoid a
|
||||||
|
class with the mayan/settings/* module
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from smart_settings import Namespace
|
||||||
|
|
||||||
|
namespace = Namespace(name='mayan', label=_('Mayan'))
|
||||||
|
|
||||||
|
setting_celery_class = namespace.add_setting(
|
||||||
|
help_text=_('The class used to instanciate the main Celery app.'),
|
||||||
|
global_name='MAYAN_CELERY_CLASS',
|
||||||
|
default='celery.Celery'
|
||||||
|
)
|
||||||
5
mayan/runtime.py
Normal file
5
mayan/runtime.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from django.utils.module_loading import import_string
|
||||||
|
|
||||||
|
from .conf import setting_celery_class
|
||||||
|
|
||||||
|
celery_class = import_string(setting_celery_class.value)
|
||||||
Reference in New Issue
Block a user