Merge branch 'features/mercs_5_6' into 3_way_merge
Signed-off-by: Roberto Rosario <Roberto.Rosario@mayan-edms.com>
This commit is contained in:
36
HISTORY.rst
36
HISTORY.rst
@@ -217,6 +217,42 @@
|
||||
- The tags app permission workflow is now reciprocal. In order
|
||||
to attach a tag, the user's role will need the tag attach
|
||||
permissions for both, the document and the tag.
|
||||
- Refactor and optimize the access control computation. Most of
|
||||
the computation has been moved to the database instead of doing
|
||||
filtering in Python. The refactor added cascading access checking
|
||||
in preparation for nested cabinet access control and the removal
|
||||
of the permission proxy support which is now redundant.
|
||||
- Remove the permissions to grant or revoke a permission to a role.
|
||||
The instead the role edit permission is used.
|
||||
- Add a test mixin to generate random model primary keys.
|
||||
- Add support for checkout and check in multiple documents at
|
||||
the same time.
|
||||
- Move file and storage code to the storage app. The setting
|
||||
COMMON_TEMPORARY_DIRECTORY is now STORAGE_TEMPORARY_DIRECTORY.
|
||||
- To lower memory usage and reduce memory leaks, the entire
|
||||
entire converter class is no longer cached and instead loaded
|
||||
on demand. This allows the garbage collector to clear the memory
|
||||
used.
|
||||
- Update the permission requirements for the index template
|
||||
document type selection screen. The document type view
|
||||
permission is now required in addition to the index
|
||||
template edit permission.
|
||||
- Update the links display templates to show which object the
|
||||
links belong to when there is more than one object.
|
||||
- Update the links display templates to show which menu
|
||||
the links belong to when there is more than one menu.
|
||||
- Remove the sidebar menu and unify its links with the
|
||||
secondary menu.
|
||||
- Increate the default maximum title lenght to 120 characters.
|
||||
- In the search API, the search function is now a service
|
||||
of the search model resource.
|
||||
- The simple and advance search are now the same service. The
|
||||
difference is determined by the URL query. A ?q= means a
|
||||
simple search. For advanced search pass the search model
|
||||
fields in the URL query, example: ?q=document_type__label=
|
||||
- Remove django-mathfilters from requirements. These tags
|
||||
are provided by default by Jinja2 template engine
|
||||
(http://jinja.pocoo.org/docs/2.10/templates/#math).
|
||||
|
||||
3.1.9 (2018-11-01)
|
||||
==================
|
||||
|
||||
5
Makefile
5
Makefile
@@ -62,15 +62,16 @@ clean-pyc:
|
||||
find . -name '*.pyc' -exec rm -f {} +
|
||||
find . -name '*.pyo' -exec rm -f {} +
|
||||
find . -name '*~' -exec rm -f {} +
|
||||
find . -name '__pycache__' -exec rm -R -f {} +
|
||||
|
||||
|
||||
# Testing
|
||||
|
||||
test:
|
||||
./manage.py test $(MODULE) --settings=mayan.settings.testing.development --nomigrations
|
||||
./manage.py test $(MODULE) --settings=mayan.settings.testing.development --nomigrations $(ARGUMENTS)
|
||||
|
||||
test-all:
|
||||
./manage.py test --mayan-apps --settings=mayan.settings.testing.development --nomigrations
|
||||
./manage.py test --mayan-apps --settings=mayan.settings.testing.development --nomigrations $(ARGUMENTS)
|
||||
|
||||
test-launch-postgres:
|
||||
@docker rm -f test-postgres || true
|
||||
|
||||
@@ -63,5 +63,5 @@ Changes needed:
|
||||
the Role model's permissions many to many field.
|
||||
4. Update the ``AccessControlList`` models roles field to point to the group
|
||||
models.
|
||||
5. Update the role checks in the ``check_access`` and ``filter_by_access``
|
||||
5. Update the role checks in the ``check_access`` and ``restrict_queryset``
|
||||
``AccessControlList`` model manager methods.
|
||||
|
||||
@@ -1,203 +1,121 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from rest_framework import generics
|
||||
|
||||
from .models import AccessControlList
|
||||
from .permissions import permission_acl_edit, permission_acl_view
|
||||
from .serializers import (
|
||||
AccessControlListPermissionSerializer, AccessControlListSerializer,
|
||||
WritableAccessControlListPermissionSerializer,
|
||||
WritableAccessControlListSerializer
|
||||
from mayan.apps.common.mixins import ContentTypeViewMixin
|
||||
from mayan.apps.permissions.serializers import (
|
||||
PermissionSerializer, RolePermissionAddRemoveSerializer
|
||||
)
|
||||
from mayan.apps.rest_api.mixins import ExternalObjectAPIViewSetMixin
|
||||
from mayan.apps.rest_api.viewsets import MayanAPIModelViewSet
|
||||
|
||||
from .permissions import permission_acl_edit, permission_acl_view
|
||||
from .serializers import AccessControlListSerializer
|
||||
|
||||
|
||||
class APIObjectACLListView(generics.ListCreateAPIView):
|
||||
"""
|
||||
get: Returns a list of all the object's access control lists
|
||||
post: Create a new access control list for the selected object.
|
||||
"""
|
||||
def get_content_object(self):
|
||||
content_type = get_object_or_404(
|
||||
klass=ContentType, app_label=self.kwargs['app_label'],
|
||||
model=self.kwargs['model']
|
||||
)
|
||||
|
||||
content_object = get_object_or_404(
|
||||
klass=content_type.model_class(), pk=self.kwargs['object_id']
|
||||
)
|
||||
|
||||
if self.request.method == 'GET':
|
||||
permission_required = permission_acl_view
|
||||
else:
|
||||
permission_required = permission_acl_edit
|
||||
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=permission_required, user=self.request.user,
|
||||
obj=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.
|
||||
"""
|
||||
context = super(APIObjectACLListView, self).get_serializer_context()
|
||||
if self.kwargs:
|
||||
context.update(
|
||||
{
|
||||
'content_object': self.get_content_object(),
|
||||
}
|
||||
)
|
||||
|
||||
return context
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
if not self.request:
|
||||
return None
|
||||
|
||||
return super(APIObjectACLListView, self).get_serializer(*args, **kwargs)
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.request.method == 'GET':
|
||||
return AccessControlListSerializer
|
||||
else:
|
||||
return WritableAccessControlListSerializer
|
||||
|
||||
|
||||
class APIObjectACLView(generics.RetrieveDestroyAPIView):
|
||||
"""
|
||||
delete: Delete the selected access control list.
|
||||
get: Returns the details of the selected access control list.
|
||||
"""
|
||||
class ObjectACLAPIViewSet(ContentTypeViewMixin, ExternalObjectAPIViewSetMixin, MayanAPIModelViewSet):
|
||||
content_type_url_kw_args = {
|
||||
'app_label': 'app_label',
|
||||
'model': 'model_name'
|
||||
}
|
||||
external_object_pk_url_kwarg = 'object_id'
|
||||
lookup_url_kwarg = 'acl_id'
|
||||
serializer_class = AccessControlListSerializer
|
||||
|
||||
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(
|
||||
klass=ContentType, app_label=self.kwargs['app_label'],
|
||||
model=self.kwargs['model']
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.validated_data.update(
|
||||
{
|
||||
'object_id': self.external_object.pk,
|
||||
'content_type': self.get_content_type(),
|
||||
}
|
||||
)
|
||||
self.perform_create(serializer)
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
|
||||
content_object = get_object_or_404(
|
||||
klass=content_type.model_class(), pk=self.kwargs['object_id']
|
||||
)
|
||||
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=permission_required, user=self.request.user,
|
||||
obj=content_object
|
||||
)
|
||||
|
||||
return content_object
|
||||
|
||||
def get_queryset(self):
|
||||
return self.get_content_object().acls.all()
|
||||
|
||||
|
||||
class APIObjectACLPermissionListView(generics.ListCreateAPIView):
|
||||
"""
|
||||
get: Returns the access control list permission list.
|
||||
post: Add a new permission to the selected access control list.
|
||||
"""
|
||||
def get_acl(self):
|
||||
return get_object_or_404(
|
||||
klass=self.get_content_object().acls, pk=self.kwargs['acl_pk']
|
||||
)
|
||||
|
||||
def get_content_object(self):
|
||||
content_type = get_object_or_404(
|
||||
klass=ContentType, app_label=self.kwargs['app_label'],
|
||||
model=self.kwargs['model']
|
||||
)
|
||||
|
||||
content_object = get_object_or_404(
|
||||
klass=content_type.model_class(), pk=self.kwargs['object_id']
|
||||
)
|
||||
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=permission_acl_view, user=self.request.user,
|
||||
obj=content_object
|
||||
)
|
||||
|
||||
return content_object
|
||||
|
||||
def get_queryset(self):
|
||||
return self.get_acl().permissions.all()
|
||||
|
||||
def get_serializer(self, *args, **kwargs):
|
||||
if not self.request:
|
||||
def get_external_object_permission(self):
|
||||
action = getattr(self, 'action', None)
|
||||
if action is None:
|
||||
return None
|
||||
|
||||
return super(APIObjectACLPermissionListView, self).get_serializer(*args, **kwargs)
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.request.method == 'GET':
|
||||
return AccessControlListPermissionSerializer
|
||||
elif action in ['list', 'retrieve', 'permission_list', 'permission_inherited_list']:
|
||||
return permission_acl_view
|
||||
else:
|
||||
return WritableAccessControlListPermissionSerializer
|
||||
return permission_acl_edit
|
||||
|
||||
def get_serializer_context(self):
|
||||
context = super(APIObjectACLPermissionListView, self).get_serializer_context()
|
||||
if self.kwargs:
|
||||
context.update(
|
||||
{
|
||||
'acl': self.get_acl(),
|
||||
}
|
||||
)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class APIObjectACLPermissionView(generics.RetrieveDestroyAPIView):
|
||||
"""
|
||||
delete: Remove the permission from the selected access control list.
|
||||
get: Returns the details of the selected access control list permission.
|
||||
"""
|
||||
lookup_url_kwarg = 'permission_pk'
|
||||
serializer_class = AccessControlListPermissionSerializer
|
||||
|
||||
def get_acl(self):
|
||||
return get_object_or_404(
|
||||
klass=self.get_content_object().acls, pk=self.kwargs['acl_pk']
|
||||
)
|
||||
|
||||
def get_content_object(self):
|
||||
content_type = get_object_or_404(
|
||||
klass=ContentType, app_label=self.kwargs['app_label'],
|
||||
model=self.kwargs['model']
|
||||
)
|
||||
|
||||
content_object = get_object_or_404(
|
||||
klass=content_type.model_class(), pk=self.kwargs['object_id']
|
||||
)
|
||||
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=permission_acl_view, user=self.request.user,
|
||||
obj=content_object
|
||||
)
|
||||
|
||||
return content_object
|
||||
def get_external_object_queryset(self):
|
||||
# Here we get a queryset the object model for which the event
|
||||
# will be accessed.
|
||||
return self.get_content_type().get_all_objects_for_this_type()
|
||||
|
||||
def get_queryset(self):
|
||||
return self.get_acl().permissions.all()
|
||||
return self.get_external_object().acls.all()
|
||||
|
||||
def get_serializer_context(self):
|
||||
context = super(APIObjectACLPermissionView, self).get_serializer_context()
|
||||
if self.kwargs:
|
||||
context.update(
|
||||
{
|
||||
'acl': self.get_acl(),
|
||||
}
|
||||
)
|
||||
@action(
|
||||
detail=True, lookup_url_kwarg='acl_id', methods=('post',),
|
||||
serializer_class=RolePermissionAddRemoveSerializer,
|
||||
url_name='permission-add', url_path='permissions/add'
|
||||
)
|
||||
def permission_add(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.permissions_add(instance=instance)
|
||||
headers = self.get_success_headers(data=serializer.data)
|
||||
return Response(
|
||||
serializer.data, headers=headers, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
return context
|
||||
@action(
|
||||
detail=True, lookup_url_kwarg='acl_id',
|
||||
serializer_class=PermissionSerializer, url_name='permission-list',
|
||||
url_path='permissions'
|
||||
)
|
||||
def permission_list(self, request, *args, **kwargs):
|
||||
queryset = self.get_object().permissions.all()
|
||||
page = self.paginate_queryset(queryset)
|
||||
|
||||
serializer = self.get_serializer(
|
||||
queryset, many=True, context={'request': request}
|
||||
)
|
||||
|
||||
if page is not None:
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(
|
||||
detail=True, lookup_url_kwarg='acl_id',
|
||||
serializer_class=PermissionSerializer,
|
||||
url_name='permission-inherited-list', url_path='permissions/inherited'
|
||||
)
|
||||
def permission_inherited_list(self, request, *args, **kwargs):
|
||||
queryset = self.get_object().get_inherited_permissions()
|
||||
page = self.paginate_queryset(queryset)
|
||||
|
||||
serializer = self.get_serializer(
|
||||
queryset, many=True, context={'request': request}
|
||||
)
|
||||
|
||||
if page is not None:
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(
|
||||
detail=True, lookup_url_kwarg='acl_id',
|
||||
methods=('post',), serializer_class=RolePermissionAddRemoveSerializer,
|
||||
url_name='permission-remove', url_path='permissions/remove'
|
||||
)
|
||||
def permission_remove(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.permissions_remove(instance=instance)
|
||||
headers = self.get_success_headers(data=serializer.data)
|
||||
return Response(
|
||||
serializer.data, headers=headers, status=status.HTTP_200_OK
|
||||
)
|
||||
|
||||
@@ -2,9 +2,15 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from mayan.apps.common import MayanAppConfig, menu_object, menu_sidebar
|
||||
from mayan.apps.common import MayanAppConfig, menu_object, menu_secondary
|
||||
from mayan.apps.events import ModelEventType
|
||||
from mayan.apps.events.links import (
|
||||
link_events_for_object, link_object_event_types_user_subcriptions_list
|
||||
)
|
||||
from mayan.apps.navigation import SourceColumn
|
||||
|
||||
from .classes import ModelPermission
|
||||
from .events import event_acl_created, event_acl_edited
|
||||
from .links import link_acl_create, link_acl_delete, link_acl_permissions
|
||||
|
||||
|
||||
@@ -18,22 +24,33 @@ class ACLsApp(MayanAppConfig):
|
||||
|
||||
def ready(self):
|
||||
super(ACLsApp, self).ready()
|
||||
from actstream import registry
|
||||
|
||||
AccessControlList = self.get_model(model_name='AccessControlList')
|
||||
|
||||
ModelEventType.register(
|
||||
event_types=(event_acl_created, event_acl_edited),
|
||||
model=AccessControlList
|
||||
)
|
||||
ModelPermission.register_inheritance(
|
||||
model=AccessControlList, related='content_object',
|
||||
)
|
||||
|
||||
SourceColumn(
|
||||
attribute='role', is_identifier=True, is_sortable=True,
|
||||
source=AccessControlList
|
||||
)
|
||||
SourceColumn(
|
||||
attribute='get_permission_titles', include_label=True,
|
||||
source=AccessControlList
|
||||
)
|
||||
|
||||
menu_object.bind_links(
|
||||
links=(link_acl_permissions, link_acl_delete),
|
||||
links=(
|
||||
link_acl_permissions, link_acl_delete,
|
||||
link_events_for_object,
|
||||
link_object_event_types_user_subcriptions_list
|
||||
),
|
||||
sources=(AccessControlList,)
|
||||
)
|
||||
menu_sidebar.bind_links(
|
||||
menu_secondary.bind_links(
|
||||
links=(link_acl_create,), sources=('acls:acl_list',)
|
||||
)
|
||||
|
||||
registry.register(AccessControlList)
|
||||
|
||||
@@ -40,25 +40,15 @@ class ModelPermission(object):
|
||||
app_label='permissions', model_name='StoredPermission'
|
||||
)
|
||||
|
||||
permissions = []
|
||||
|
||||
class_permissions = cls.get_for_class(klass=type(instance))
|
||||
|
||||
if class_permissions:
|
||||
permissions.extend(class_permissions)
|
||||
|
||||
proxy = cls._proxies.get(type(instance))
|
||||
|
||||
if proxy:
|
||||
permissions.extend(cls._registry.get(proxy))
|
||||
permissions = cls.get_for_class(klass=type(instance))
|
||||
|
||||
pks = [
|
||||
permission.stored_permission.pk for permission in set(permissions)
|
||||
permission.stored_permission.pk for permission in permissions
|
||||
]
|
||||
return StoredPermission.objects.filter(pk__in=pks)
|
||||
|
||||
@classmethod
|
||||
def get_inheritance(cls, model):
|
||||
def get_inheritances(cls, model):
|
||||
return cls._inheritances[model]
|
||||
|
||||
@classmethod
|
||||
@@ -79,7 +69,8 @@ class ModelPermission(object):
|
||||
|
||||
@classmethod
|
||||
def register_inheritance(cls, model, related):
|
||||
cls._inheritances[model] = related
|
||||
cls._inheritances.setdefault(model, [])
|
||||
cls._inheritances[model].append(related)
|
||||
|
||||
@classmethod
|
||||
def register_proxy(cls, source, model):
|
||||
|
||||
16
mayan/apps/acls/events.py
Normal file
16
mayan/apps/acls/events.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from mayan.apps.events import EventTypeNamespace
|
||||
|
||||
namespace = EventTypeNamespace(
|
||||
label=_('Access control lists'), name='acls'
|
||||
)
|
||||
|
||||
event_acl_created = namespace.add_event_type(
|
||||
label=_('ACL created'), name='acl_created'
|
||||
)
|
||||
event_acl_edited = namespace.add_event_type(
|
||||
label=_('ACL edited'), name='acl_edited'
|
||||
)
|
||||
@@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from mayan.apps.appearance.classes import Icon
|
||||
|
||||
icon_acl_delete = Icon(driver_name='fontawesome', symbol='minus')
|
||||
icon_acl_delete = Icon(driver_name='fontawesome', symbol='times')
|
||||
icon_acl_list = Icon(driver_name='fontawesome', symbol='lock')
|
||||
icon_acl_new = Icon(
|
||||
driver_name='fontawesome-dual', primary_symbol='lock',
|
||||
|
||||
@@ -21,7 +21,7 @@ def get_kwargs_factory(variable_name):
|
||||
)
|
||||
return {
|
||||
'app_label': '"{}"'.format(content_type.app_label),
|
||||
'model': '"{}"'.format(content_type.model),
|
||||
'model_name': '"{}"'.format(content_type.model),
|
||||
'object_id': '{}.pk'.format(variable_name)
|
||||
}
|
||||
|
||||
@@ -29,21 +29,21 @@ def get_kwargs_factory(variable_name):
|
||||
|
||||
|
||||
link_acl_delete = Link(
|
||||
args='resolved_object.pk', icon_class=icon_acl_delete,
|
||||
permissions=(permission_acl_edit,), permissions_related='content_object',
|
||||
tags='dangerous', text=_('Delete'), view='acls:acl_delete',
|
||||
icon_class=icon_acl_delete, kwargs={'acl_id': 'resolved_object.pk'},
|
||||
permission=permission_acl_edit, tags='dangerous', text=_('Delete'),
|
||||
view='acls:acl_delete',
|
||||
)
|
||||
link_acl_list = Link(
|
||||
icon_class=icon_acl_list, kwargs=get_kwargs_factory('resolved_object'),
|
||||
permissions=(permission_acl_view,), text=_('ACLs'), view='acls:acl_list'
|
||||
icon_class=icon_acl_list, kwargs=get_kwargs_factory(
|
||||
variable_name='resolved_object'
|
||||
), permission=permission_acl_view, text=_('ACLs'), view='acls:acl_list'
|
||||
)
|
||||
link_acl_create = Link(
|
||||
icon_class=icon_acl_new, kwargs=get_kwargs_factory('resolved_object'),
|
||||
permissions=(permission_acl_edit,), text=_('New ACL'),
|
||||
view='acls:acl_create'
|
||||
permission=permission_acl_edit, text=_('New ACL'), view='acls:acl_create'
|
||||
)
|
||||
link_acl_permissions = Link(
|
||||
args='resolved_object.pk', icon_class=icon_permission,
|
||||
permissions=(permission_acl_edit,), permissions_related='content_object',
|
||||
text=_('Permissions'), view='acls:acl_permissions',
|
||||
permission=permission_acl_edit, text=_('Permissions'),
|
||||
view='acls:acl_permissions',
|
||||
)
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import logging
|
||||
import operator
|
||||
import warnings
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.db.models import CharField, Value as V, Q
|
||||
from django.db.models.functions import Concat
|
||||
from django.http import Http404
|
||||
|
||||
from mayan.apps.common.utils import resolve_attribute, return_related
|
||||
from mayan.apps.common.utils import (
|
||||
get_related_field, resolve_attribute, return_related
|
||||
)
|
||||
from mayan.apps.common.warnings import InterfaceWarning
|
||||
from mayan.apps.permissions import Permission
|
||||
from mayan.apps.permissions.models import StoredPermission
|
||||
|
||||
@@ -24,210 +30,189 @@ class AccessControlListManager(models.Manager):
|
||||
Implement a 3 tier permission system, involving a permissions, an actor
|
||||
and an object
|
||||
"""
|
||||
def check_access(self, permissions, user, obj, related=None):
|
||||
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
|
||||
def _get_acl_filters(self, queryset, stored_permission, user, related_field_name=None):
|
||||
"""
|
||||
This method does the bulk of the work. It generates filters for the
|
||||
AccessControlList model to determine if there are ACL entries for the
|
||||
members of the queryset's model provided.
|
||||
"""
|
||||
# Determine which of the cases we need to address
|
||||
# 1: No related field
|
||||
# 2: Related field
|
||||
# 3: Related field that is Generic Foreign Key
|
||||
# 4: No related field, but has an inherited related field, solved by
|
||||
# recursion, branches to #2 or #3.
|
||||
# 5: Inherited field of a related field
|
||||
# -- Not addressed yet --
|
||||
# 6: Inherited field of a related field that is Generic Foreign Key
|
||||
result = []
|
||||
|
||||
if related_field_name:
|
||||
related_field = get_related_field(
|
||||
model=queryset.model, related_field_name=related_field_name
|
||||
)
|
||||
return True
|
||||
|
||||
try:
|
||||
return Permission.check_permissions(
|
||||
requester=user, permissions=permissions
|
||||
)
|
||||
except PermissionDenied:
|
||||
try:
|
||||
stored_permissions = [
|
||||
permission.stored_permission for permission in permissions
|
||||
]
|
||||
except TypeError:
|
||||
# Not a list of permissions, just one
|
||||
stored_permissions = (permissions.stored_permission,)
|
||||
if isinstance(related_field, GenericForeignKey):
|
||||
# Case 3: Generic Foreign Key, multiple ContentTypes + object
|
||||
# id combinations
|
||||
content_type_object_id_queryset = queryset.annotate(
|
||||
ct_fk_combination=Concat(
|
||||
related_field.ct_field, V('-'), related_field.fk_field,
|
||||
output_field=CharField()
|
||||
)
|
||||
).values('ct_fk_combination')
|
||||
|
||||
if related:
|
||||
obj = resolve_attribute(obj=obj, attribute=related)
|
||||
acl_filter = self.annotate(
|
||||
ct_fk_combination=Concat(
|
||||
'content_type', V('-'), 'object_id', output_field=CharField()
|
||||
)
|
||||
).filter(
|
||||
permissions=stored_permission, role__groups__user=user,
|
||||
ct_fk_combination__in=content_type_object_id_queryset
|
||||
).values('object_id')
|
||||
|
||||
try:
|
||||
parent_accessor = ModelPermission.get_inheritance(
|
||||
model=obj._meta.model
|
||||
field_lookup = 'object_id__in'
|
||||
|
||||
result.append(Q(**{field_lookup: acl_filter}))
|
||||
else:
|
||||
# Case 2: Related field of a single type, single ContentType,
|
||||
# multiple object id
|
||||
content_type = ContentType.objects.get_for_model(
|
||||
model=related_field.related_model
|
||||
)
|
||||
except AttributeError:
|
||||
# AttributeError means non model objects: ie Statistics
|
||||
# These can't have ACLs so we raise PermissionDenied
|
||||
raise PermissionDenied(
|
||||
_('Insufficient access for: %(object)s') % {'object': obj}
|
||||
field_lookup = '{}_id__in'.format(related_field_name)
|
||||
acl_filter = self.filter(
|
||||
content_type=content_type, permissions=stored_permission,
|
||||
role__groups__user=user
|
||||
).values('object_id')
|
||||
result.append(Q(**{field_lookup: acl_filter}))
|
||||
# Case 5: Related field, has an inherited related field itself
|
||||
# Bubble up permssion check
|
||||
# TODO: Add relationship support: OR or AND
|
||||
# TODO: OR for document pages, version, doc, and types
|
||||
# TODO: AND for new cabinet levels ACLs
|
||||
try:
|
||||
related_field_model_related_fields = ModelPermission.get_inheritances(
|
||||
model=related_field.related_model
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
relation_result = []
|
||||
for related_field_model_related_field_name in related_field_model_related_fields:
|
||||
related_field_name = '{}__{}'.format(related_field_name, related_field_model_related_field_name)
|
||||
related_field_inherited_acl_queries = self._get_acl_filters(
|
||||
queryset=queryset, stored_permission=stored_permission,
|
||||
user=user, related_field_name=related_field_name
|
||||
)
|
||||
|
||||
relation_result.append(reduce(operator.and_, related_field_inherited_acl_queries))
|
||||
|
||||
result.append(reduce(operator.or_, relation_result))
|
||||
else:
|
||||
# Case 1: Original model, single ContentType, multiple object id
|
||||
content_type = ContentType.objects.get_for_model(model=queryset.model)
|
||||
field_lookup = 'id__in'
|
||||
acl_filter = self.filter(
|
||||
content_type=content_type, permissions=stored_permission,
|
||||
role__groups__user=user
|
||||
).values('object_id')
|
||||
result.append(Q(**{field_lookup: acl_filter}))
|
||||
|
||||
# Case 4: Original model, has an inherited related field
|
||||
try:
|
||||
related_fields = ModelPermission.get_inheritances(
|
||||
model=queryset.model
|
||||
)
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
return self.check_access(
|
||||
obj=getattr(obj, parent_accessor),
|
||||
permissions=permissions, user=user
|
||||
relation_result = []
|
||||
|
||||
for related_field_name in related_fields:
|
||||
inherited_acl_queries = self._get_acl_filters(
|
||||
queryset=queryset, stored_permission=stored_permission,
|
||||
related_field_name=related_field_name, user=user
|
||||
)
|
||||
except AttributeError:
|
||||
# Has no such attribute, try it as a related field
|
||||
try:
|
||||
return self.check_access(
|
||||
obj=return_related(
|
||||
instance=obj, related_field=parent_accessor
|
||||
), permissions=permissions, user=user
|
||||
)
|
||||
except PermissionDenied:
|
||||
pass
|
||||
except PermissionDenied:
|
||||
pass
|
||||
relation_result.append(reduce(operator.and_, inherited_acl_queries))
|
||||
|
||||
user_roles = []
|
||||
for group in user.groups.all():
|
||||
for role in group.roles.all():
|
||||
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
|
||||
result.append(reduce(operator.or_, relation_result))
|
||||
|
||||
user_roles.append(role)
|
||||
return result
|
||||
|
||||
if not self.filter(content_type=ContentType.objects.get_for_model(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 for: %s') % obj)
|
||||
def check_access(self, obj, permission, user, raise_404=False):
|
||||
warnings.warn(
|
||||
'check_access() is deprecated, use restrict_queryset() to '
|
||||
'produce a queryset from which to .get() the corresponding '
|
||||
'object in the local code.', InterfaceWarning
|
||||
)
|
||||
queryset = self.restrict_queryset(
|
||||
permission=permission, queryset=obj._meta.default_manager.all(),
|
||||
user=user
|
||||
)
|
||||
|
||||
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):
|
||||
if user.is_superuser or user.is_staff:
|
||||
logger.debug(
|
||||
'Unfiltered queryset returned to user "%s" as superuser '
|
||||
'or staff', user
|
||||
)
|
||||
return queryset
|
||||
|
||||
try:
|
||||
Permission.check_permissions(
|
||||
requester=user, permissions=(permission,)
|
||||
)
|
||||
except PermissionDenied:
|
||||
user_roles = []
|
||||
for group in user.groups.all():
|
||||
for role in group.roles.all():
|
||||
user_roles.append(role)
|
||||
|
||||
try:
|
||||
parent_accessor = ModelPermission.get_inheritance(
|
||||
model=queryset.model
|
||||
)
|
||||
except KeyError:
|
||||
parent_acl_query = Q()
|
||||
if queryset.filter(pk=obj.pk).exists():
|
||||
return True
|
||||
else:
|
||||
if raise_404:
|
||||
raise Http404
|
||||
else:
|
||||
instance = queryset.first()
|
||||
if instance:
|
||||
parent_object = return_related(
|
||||
instance=instance, related_field=parent_accessor
|
||||
)
|
||||
raise PermissionDenied
|
||||
|
||||
try:
|
||||
# Try to see if parent_object is a function
|
||||
parent_object()
|
||||
except TypeError:
|
||||
# Is not a function, try it as a field
|
||||
parent_content_type = ContentType.objects.get_for_model(
|
||||
model=parent_object
|
||||
)
|
||||
parent_queryset = self.filter(
|
||||
content_type=parent_content_type,
|
||||
role__in=user_roles,
|
||||
permissions=permission.stored_permission
|
||||
)
|
||||
parent_acl_query = Q(
|
||||
**{
|
||||
'{}__pk__in'.format(
|
||||
parent_accessor
|
||||
): parent_queryset.values_list(
|
||||
'object_id', flat=True
|
||||
)
|
||||
}
|
||||
)
|
||||
else:
|
||||
# Is a function. Can't perform Q object filtering.
|
||||
# Perform iterative filtering.
|
||||
result = []
|
||||
for entry in queryset:
|
||||
try:
|
||||
self.check_access(
|
||||
obj=entry, permissions=permission,
|
||||
user=user
|
||||
)
|
||||
except PermissionDenied:
|
||||
pass
|
||||
else:
|
||||
result.append(entry.pk)
|
||||
def get_inherited_permissions(self, obj, role):
|
||||
queryset = self._get_inherited_object_permissions(obj=obj, role=role)
|
||||
|
||||
return queryset.filter(pk__in=result)
|
||||
else:
|
||||
parent_acl_query = Q()
|
||||
queryset = queryset | role.permissions.all()
|
||||
|
||||
# Directly granted access
|
||||
content_type = ContentType.objects.get_for_model(
|
||||
model=queryset.model
|
||||
)
|
||||
acl_query = Q(pk__in=self.filter(
|
||||
content_type=content_type, role__in=user_roles,
|
||||
permissions=permission.stored_permission
|
||||
).values_list('object_id', flat=True))
|
||||
logger.debug(
|
||||
'Filtered queryset returned to user "%s" based on roles "%s"',
|
||||
user, user_roles
|
||||
)
|
||||
# Filter the permissions to the ones that apply to the model
|
||||
queryset = ModelPermission.get_for_instance(
|
||||
instance=obj
|
||||
).filter(
|
||||
pk__in=queryset
|
||||
)
|
||||
|
||||
return queryset.filter(parent_acl_query | acl_query)
|
||||
else:
|
||||
return queryset
|
||||
|
||||
def _get_inherited_object_permissions(self, obj, role):
|
||||
queryset = StoredPermission.objects.none()
|
||||
|
||||
if not obj:
|
||||
return queryset
|
||||
|
||||
def get_inherited_permissions(self, role, obj):
|
||||
try:
|
||||
instance = obj.first()
|
||||
except AttributeError:
|
||||
instance = obj
|
||||
else:
|
||||
if not instance:
|
||||
return StoredPermission.objects.none()
|
||||
|
||||
try:
|
||||
parent_accessor = ModelPermission.get_inheritance(
|
||||
model=type(instance)
|
||||
related_fields = ModelPermission.get_inheritances(
|
||||
model=type(obj)
|
||||
)
|
||||
except KeyError:
|
||||
return StoredPermission.objects.none()
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
parent_object = resolve_attribute(
|
||||
obj=instance, attribute=parent_accessor
|
||||
)
|
||||
except AttributeError:
|
||||
# Parent accessor is not an attribute, try it as a related
|
||||
# field.
|
||||
parent_object = return_related(
|
||||
instance=instance, related_field=parent_accessor
|
||||
)
|
||||
content_type = ContentType.objects.get_for_model(parent_object)
|
||||
try:
|
||||
return self.get(
|
||||
role=role, content_type=content_type,
|
||||
object_id=parent_object.pk
|
||||
).permissions.all()
|
||||
except self.model.DoesNotExist:
|
||||
return StoredPermission.objects.none()
|
||||
for related_field_name in related_fields:
|
||||
try:
|
||||
parent_object = resolve_attribute(
|
||||
obj=obj, attribute=related_field_name
|
||||
)
|
||||
except AttributeError:
|
||||
# Parent accessor is not an attribute, try it as a related
|
||||
# field.
|
||||
parent_object = return_related(
|
||||
instance=obj, related_field=related_field_name
|
||||
)
|
||||
content_type = ContentType.objects.get_for_model(model=parent_object)
|
||||
try:
|
||||
queryset = queryset | self.get(
|
||||
content_type=content_type, object_id=parent_object.pk,
|
||||
role=role
|
||||
).permissions.all()
|
||||
except self.model.DoesNotExist:
|
||||
pass
|
||||
|
||||
def grant(self, permission, role, obj):
|
||||
queryset = queryset | self._get_inherited_object_permissions(
|
||||
obj=parent_object, role=role
|
||||
)
|
||||
|
||||
return queryset
|
||||
|
||||
def grant(self, obj, permission, role):
|
||||
class_permissions = ModelPermission.get_for_class(klass=obj.__class__)
|
||||
if permission not in class_permissions:
|
||||
raise PermissionNotValidForClass
|
||||
@@ -242,7 +227,42 @@ class AccessControlListManager(models.Manager):
|
||||
|
||||
return acl
|
||||
|
||||
def revoke(self, permission, role, obj):
|
||||
def restrict_queryset_by_accesses(self, operator, permissions, queryset, user):
|
||||
result = []
|
||||
|
||||
for permission in permissions:
|
||||
result.append(
|
||||
self.restrict_queryset(
|
||||
permission=permission, queryset=queryset, user=user
|
||||
)
|
||||
)
|
||||
|
||||
return reduce(operator, result)
|
||||
|
||||
def restrict_queryset(self, permission, queryset, user):
|
||||
# Check directly granted permission via a role
|
||||
try:
|
||||
Permission.check_user_permission(permission=permission, user=user)
|
||||
except PermissionDenied:
|
||||
acl_filters = self._get_acl_filters(
|
||||
queryset=queryset,
|
||||
stored_permission=permission.stored_permission, user=user
|
||||
)
|
||||
|
||||
final_query = None
|
||||
for acl_filter in acl_filters:
|
||||
if final_query is None:
|
||||
final_query = acl_filter
|
||||
else:
|
||||
final_query = final_query | acl_filter
|
||||
|
||||
return queryset.filter(final_query)
|
||||
else:
|
||||
# User has direct permission assignment via a role, is superuser or
|
||||
# is staff. Return the entire queryset.
|
||||
return queryset
|
||||
|
||||
def revoke(self, obj, permission, role):
|
||||
content_type = ContentType.objects.get_for_model(model=obj)
|
||||
acl, created = self.get_or_create(
|
||||
content_type=content_type, object_id=obj.pk,
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import logging
|
||||
import operator
|
||||
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.db import models, transaction
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import force_text, python_2_unicode_compatible
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from mayan.apps.permissions.models import Role, StoredPermission
|
||||
|
||||
from .events import event_acl_created, event_acl_edited
|
||||
from .managers import AccessControlListManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -30,6 +32,11 @@ class AccessControlList(models.Model):
|
||||
* Role - Custom role that is being granted a permission. Roles are created
|
||||
in the Setup menu.
|
||||
"""
|
||||
# Multiple inheritance operator types
|
||||
OPERATOR_AND = operator.and_
|
||||
OPERATOR_OR = operator.or_
|
||||
operator_default = OPERATOR_AND
|
||||
|
||||
content_type = models.ForeignKey(
|
||||
on_delete=models.CASCADE, related_name='object_content_type',
|
||||
to=ContentType
|
||||
@@ -58,16 +65,15 @@ class AccessControlList(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return _(
|
||||
'Permissions "%(permissions)s" to role "%(role)s" for "%(object)s"'
|
||||
'Role "%(role)s" permission\'s for "%(object)s"'
|
||||
) % {
|
||||
'permissions': self.get_permission_titles(),
|
||||
'object': self.content_object,
|
||||
'role': self.role
|
||||
'role': self.role,
|
||||
}
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
viewname='acls:acl_permissions', kwargs={'acl_pk': self.pk}
|
||||
viewname='acls:acl_permissions', kwargs={'acl_id': self.pk}
|
||||
)
|
||||
|
||||
def get_inherited_permissions(self):
|
||||
@@ -85,3 +91,32 @@ class AccessControlList(models.Model):
|
||||
|
||||
return result or _('None')
|
||||
get_permission_titles.short_description = _('Permissions')
|
||||
|
||||
def permissions_add(self, queryset, _user=None):
|
||||
with transaction.atomic():
|
||||
event_acl_edited.commit(
|
||||
actor=_user, target=self
|
||||
)
|
||||
self.permissions.add(*queryset)
|
||||
|
||||
def permissions_remove(self, queryset, _user=None):
|
||||
with transaction.atomic():
|
||||
event_acl_edited.commit(
|
||||
actor=_user, target=self
|
||||
)
|
||||
self.permissions.remove(*queryset)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
_user = kwargs.pop('_user', None)
|
||||
|
||||
with transaction.atomic():
|
||||
is_new = not self.pk
|
||||
super(AccessControlList, self).save(*args, **kwargs)
|
||||
if is_new:
|
||||
event_acl_created.commit(
|
||||
actor=_user, target=self
|
||||
)
|
||||
else:
|
||||
event_acl_edited.commit(
|
||||
actor=_user, target=self
|
||||
)
|
||||
|
||||
@@ -1,216 +1,143 @@
|
||||
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 mayan.apps.common.serializers import ContentTypeSerializer
|
||||
from mayan.apps.permissions import Permission
|
||||
from mayan.apps.permissions.models import Role, StoredPermission
|
||||
from mayan.apps.permissions.serializers import (
|
||||
PermissionSerializer, RoleSerializer
|
||||
)
|
||||
from mayan.apps.permissions.models import Role
|
||||
from mayan.apps.permissions.permissions import permission_role_edit
|
||||
from mayan.apps.permissions.serializers import RoleSerializer
|
||||
from mayan.apps.rest_api.mixins import ExternalObjectSerializerMixin
|
||||
from mayan.apps.rest_api.relations import MultiKwargHyperlinkedIdentityField
|
||||
|
||||
from .models import AccessControlList
|
||||
|
||||
|
||||
class AccessControlListSerializer(serializers.ModelSerializer):
|
||||
class AccessControlListSerializer(ExternalObjectSerializerMixin, 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()
|
||||
permission_add_url = MultiKwargHyperlinkedIdentityField(
|
||||
view_kwargs=(
|
||||
{
|
||||
'lookup_field': 'content_type__app_label', 'lookup_url_kwarg': 'app_label',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'content_type__model', 'lookup_url_kwarg': 'model_name',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'object_id', 'lookup_url_kwarg': 'object_id',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'pk', 'lookup_url_kwarg': 'acl_id',
|
||||
}
|
||||
),
|
||||
view_name='rest_api:object-acl-permission-add'
|
||||
)
|
||||
permission_list_url = MultiKwargHyperlinkedIdentityField(
|
||||
view_kwargs=(
|
||||
{
|
||||
'lookup_field': 'content_type__app_label', 'lookup_url_kwarg': 'app_label',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'content_type__model', 'lookup_url_kwarg': 'model_name',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'object_id', 'lookup_url_kwarg': 'object_id',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'pk', 'lookup_url_kwarg': 'acl_id',
|
||||
}
|
||||
),
|
||||
view_name='rest_api:object-acl-permission-list'
|
||||
)
|
||||
permission_list_inherited_url = MultiKwargHyperlinkedIdentityField(
|
||||
view_kwargs=(
|
||||
{
|
||||
'lookup_field': 'content_type__app_label', 'lookup_url_kwarg': 'app_label',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'content_type__model', 'lookup_url_kwarg': 'model_name',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'object_id', 'lookup_url_kwarg': 'object_id',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'pk', 'lookup_url_kwarg': 'acl_id',
|
||||
}
|
||||
),
|
||||
view_name='rest_api:object-acl-permission-inherited-list'
|
||||
)
|
||||
permission_remove_url = MultiKwargHyperlinkedIdentityField(
|
||||
view_kwargs=(
|
||||
{
|
||||
'lookup_field': 'content_type__app_label', 'lookup_url_kwarg': 'app_label',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'content_type__model', 'lookup_url_kwarg': 'model_name',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'object_id', 'lookup_url_kwarg': 'object_id',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'pk', 'lookup_url_kwarg': 'acl_id',
|
||||
}
|
||||
),
|
||||
view_name='rest_api:object-acl-permission-remove'
|
||||
)
|
||||
role_id = serializers.CharField(
|
||||
label=_('Role ID'),
|
||||
help_text=_(
|
||||
'Primary key of the role of the ACL that will be created or edited.'
|
||||
), required=False, write_only=True
|
||||
)
|
||||
url = MultiKwargHyperlinkedIdentityField(
|
||||
view_kwargs=(
|
||||
{
|
||||
'lookup_field': 'content_type__app_label', 'lookup_url_kwarg': 'app_label',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'content_type__model', 'lookup_url_kwarg': 'model_name',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'object_id', 'lookup_url_kwarg': 'object_id',
|
||||
},
|
||||
{
|
||||
'lookup_field': 'pk', 'lookup_url_kwarg': 'acl_id',
|
||||
}
|
||||
),
|
||||
view_name='rest_api:object-acl-detail'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
external_object_model = Role
|
||||
external_object_pk_field = 'role_id'
|
||||
external_object_permission = permission_role_edit
|
||||
fields = (
|
||||
'content_type', 'id', 'object_id', 'permissions_url', 'role', 'url'
|
||||
'content_type', 'id', 'object_id', 'permission_add_url',
|
||||
'permission_list_url', 'permission_list_inherited_url',
|
||||
'permission_remove_url', 'role', 'role_id',
|
||||
'url'
|
||||
)
|
||||
model = AccessControlList
|
||||
|
||||
def get_permissions_url(self, instance):
|
||||
return reverse(
|
||||
viewname='rest_api:accesscontrollist-permission-list', kwargs={
|
||||
'app_label': instance.content_type.app_label,
|
||||
'model': instance.content_type.model,
|
||||
'object_id': instance.object_id,
|
||||
'acl_pk': instance.pk
|
||||
}, request=self.context['request'], format=self.context['format']
|
||||
)
|
||||
|
||||
def get_url(self, instance):
|
||||
return reverse(
|
||||
'rest_api:accesscontrollist-detail', kwargs={
|
||||
'app_label': instance.content_type.app_label,
|
||||
'model': instance.content_type.model,
|
||||
'object_id': instance.object_id,
|
||||
'acl_pk': 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', kwargs={
|
||||
'app_label': self.context['acl'].content_type.app_label,
|
||||
'model': self.context['acl'].content_type.model,
|
||||
'object_id': self.context['acl'].object_id,
|
||||
'acl_pk': self.context['acl'].pk,
|
||||
'permission_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', kwargs={
|
||||
'app_label': self.context['acl'].content_type.app_label,
|
||||
'model': self.context['acl'].content_type.model,
|
||||
'object_id': self.context['acl'].object_id,
|
||||
'acl_pk': 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',)
|
||||
read_only_fields = ('object_id',)
|
||||
|
||||
def create(self, validated_data):
|
||||
for permission in validated_data['permissions']:
|
||||
self.context['acl'].permissions.add(permission)
|
||||
role = self.get_external_object()
|
||||
|
||||
return validated_data['permissions'][0]
|
||||
if role:
|
||||
validated_data['role'] = role
|
||||
|
||||
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(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', kwargs={
|
||||
'app_label': instance.content_type.app_label,
|
||||
'model': instance.content_type.model,
|
||||
'object_id': instance.object_id,
|
||||
'acl_pk': instance.pk
|
||||
}, request=self.context['request'], format=self.context['format']
|
||||
return super(AccessControlListSerializer, self).create(
|
||||
validated_data=validated_data
|
||||
)
|
||||
|
||||
def get_url(self, instance):
|
||||
return reverse(
|
||||
'rest_api:accesscontrollist-detail', kwargs={
|
||||
'app_label': instance.content_type.app_label,
|
||||
'model': instance.content_type.model,
|
||||
'object_id': instance.object_id,
|
||||
'acl_pk': instance.pk
|
||||
}, request=self.context['request'], format=self.context['format']
|
||||
def update(self, instance, validated_data):
|
||||
role = self.get_external_object()
|
||||
|
||||
if role:
|
||||
validated_data['role'] = role
|
||||
|
||||
return super(AccessControlListSerializer, self).update(
|
||||
instance=instance, validated_data=validated_data
|
||||
)
|
||||
|
||||
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(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
|
||||
|
||||
@@ -1,47 +1,73 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
from mayan.apps.permissions.models import Role
|
||||
from mayan.apps.permissions.tests.literals import TEST_ROLE_LABEL
|
||||
from mayan.apps.user_management.tests import (
|
||||
TEST_ADMIN_EMAIL, TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME,
|
||||
TEST_GROUP_NAME, TEST_USER_EMAIL, TEST_USER_PASSWORD, TEST_USER_USERNAME
|
||||
from mayan.apps.common.tests.mixins import TestModelTestMixin
|
||||
from mayan.apps.permissions.tests.mixins import (
|
||||
PermissionTestMixin, RoleTestCaseMixin, RoleTestMixin
|
||||
)
|
||||
from mayan.apps.user_management.tests.mixins import UserTestCaseMixin
|
||||
|
||||
from ..classes import ModelPermission
|
||||
from ..models import AccessControlList
|
||||
from ..permissions import permission_acl_edit, permission_acl_view
|
||||
|
||||
|
||||
class ACLBaseTestMixin(object):
|
||||
auto_create_group = True
|
||||
auto_create_users = True
|
||||
|
||||
class ACLTestCaseMixin(RoleTestCaseMixin, UserTestCaseMixin):
|
||||
def setUp(self):
|
||||
super(ACLBaseTestMixin, self).setUp()
|
||||
if self.auto_create_users:
|
||||
self.admin_user = get_user_model().objects.create_superuser(
|
||||
username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL,
|
||||
password=TEST_ADMIN_PASSWORD
|
||||
)
|
||||
|
||||
self.user = get_user_model().objects.create_user(
|
||||
username=TEST_USER_USERNAME, email=TEST_USER_EMAIL,
|
||||
password=TEST_USER_PASSWORD
|
||||
)
|
||||
|
||||
if self.auto_create_group:
|
||||
self.group = Group.objects.create(name=TEST_GROUP_NAME)
|
||||
self.role = Role.objects.create(label=TEST_ROLE_LABEL)
|
||||
self.group.user_set.add(self.user)
|
||||
self.role.groups.add(self.group)
|
||||
super(ACLTestCaseMixin, self).setUp()
|
||||
if hasattr(self, '_test_case_user'):
|
||||
self._test_case_role.groups.add(self._test_case_group)
|
||||
|
||||
def grant_access(self, obj, permission):
|
||||
return AccessControlList.objects.grant(
|
||||
obj=obj, permission=permission, role=self.role
|
||||
if not hasattr(self, '_test_case_role'):
|
||||
raise ImproperlyConfigured(
|
||||
'Enable the creation of the test case user, group, and role '
|
||||
'in order to enable the usage of ACLs in tests.'
|
||||
)
|
||||
|
||||
self._test_case_acl = AccessControlList.objects.grant(
|
||||
obj=obj, permission=permission, role=self._test_case_role
|
||||
)
|
||||
|
||||
def grant_permission(self, permission):
|
||||
self.role.permissions.add(
|
||||
permission.stored_permission
|
||||
|
||||
class ACLTestMixin(PermissionTestMixin, RoleTestMixin, TestModelTestMixin):
|
||||
auto_create_test_role = True
|
||||
|
||||
def _create_test_acl(self):
|
||||
self.test_acl = AccessControlList.objects.create(
|
||||
content_object=self.test_object, role=self.test_role
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
super(ACLTestMixin, self).setUp()
|
||||
if self.auto_create_test_role:
|
||||
self._create_test_role()
|
||||
|
||||
def _inject_test_object_content_type(self):
|
||||
self.test_object_content_type = ContentType.objects.get_for_model(self.test_object)
|
||||
|
||||
self.test_content_object_view_kwargs = {
|
||||
'app_label': self.test_object_content_type.app_label,
|
||||
'model_name': self.test_object_content_type.model,
|
||||
'object_id': self.test_object.pk
|
||||
}
|
||||
|
||||
def _setup_test_object(self):
|
||||
self._create_test_model()
|
||||
self._create_test_object()
|
||||
ModelPermission.register(
|
||||
model=self.test_object._meta.model, permissions=(
|
||||
permission_acl_edit, permission_acl_view,
|
||||
)
|
||||
)
|
||||
|
||||
self._create_test_permission()
|
||||
ModelPermission.register(
|
||||
model=self.test_object._meta.model, permissions=(
|
||||
self.test_permission,
|
||||
)
|
||||
)
|
||||
|
||||
self._inject_test_object_content_type()
|
||||
|
||||
@@ -9,16 +9,13 @@ from ..workflow_actions import GrantAccessAction, RevokeAccessAction
|
||||
|
||||
|
||||
class ACLActionTestCase(ActionTestCase):
|
||||
def setUp(self):
|
||||
super(ACLActionTestCase, self).setUp()
|
||||
|
||||
def test_grant_access_action(self):
|
||||
action = GrantAccessAction(
|
||||
form_data={
|
||||
'content_type': ContentType.objects.get_for_model(model=self.document).pk,
|
||||
'object_id': self.document.pk,
|
||||
'roles': [self.role.pk],
|
||||
'permissions': [permission_document_view.uuid],
|
||||
'roles': [self._test_case_role.pk],
|
||||
'permissions': [permission_document_view.pk],
|
||||
}
|
||||
)
|
||||
action.execute(context={'entry_log': self.entry_log})
|
||||
@@ -28,7 +25,7 @@ class ACLActionTestCase(ActionTestCase):
|
||||
list(self.document.acls.first().permissions.all()),
|
||||
[permission_document_view.stored_permission]
|
||||
)
|
||||
self.assertEqual(self.document.acls.first().role, self.role)
|
||||
self.assertEqual(self.document.acls.first().role, self._test_case_role)
|
||||
|
||||
def test_revoke_access_action(self):
|
||||
self.grant_access(
|
||||
@@ -39,8 +36,8 @@ class ACLActionTestCase(ActionTestCase):
|
||||
form_data={
|
||||
'content_type': ContentType.objects.get_for_model(model=self.document).pk,
|
||||
'object_id': self.document.pk,
|
||||
'roles': [self.role.pk],
|
||||
'permissions': [permission_document_view.uuid],
|
||||
'roles': [self._test_case_role.pk],
|
||||
'permissions': [permission_document_view.pk],
|
||||
}
|
||||
)
|
||||
action.execute(context={'entry_log': self.entry_log})
|
||||
|
||||
@@ -1,205 +1,189 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from rest_framework import status
|
||||
|
||||
from mayan.apps.documents.permissions import permission_document_view
|
||||
from mayan.apps.documents.tests import DocumentTestMixin
|
||||
from mayan.apps.permissions.tests.literals import TEST_ROLE_LABEL
|
||||
from mayan.apps.rest_api.tests import BaseAPITestCase
|
||||
|
||||
from ..models import AccessControlList
|
||||
from ..permissions import permission_acl_view
|
||||
from ..permissions import permission_acl_edit, permission_acl_view
|
||||
|
||||
from .mixins import ACLTestMixin
|
||||
|
||||
|
||||
class ACLAPITestCase(DocumentTestMixin, BaseAPITestCase):
|
||||
class ACLAPITestCase(ACLTestMixin, BaseAPITestCase):
|
||||
def setUp(self):
|
||||
super(ACLAPITestCase, self).setUp()
|
||||
self.login_admin_user()
|
||||
self._setup_test_object()
|
||||
self._create_test_acl()
|
||||
self.test_acl.permissions.add(self.test_permission.stored_permission)
|
||||
|
||||
self.document_content_type = ContentType.objects.get_for_model(
|
||||
self.document
|
||||
def _request_object_acl_list_api_view(self):
|
||||
return self.get(
|
||||
viewname='rest_api:object-acl-list',
|
||||
kwargs=self.test_content_object_view_kwargs
|
||||
)
|
||||
|
||||
def _create_acl(self):
|
||||
self.acl = AccessControlList.objects.create(
|
||||
content_object=self.document,
|
||||
role=self.role
|
||||
)
|
||||
def test_object_acl_list_api_view_no_permission(self):
|
||||
response = self._request_object_acl_list_api_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
self.acl.permissions.add(permission_document_view.stored_permission)
|
||||
def test_object_acl_list_api_view_with_access(self):
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_view)
|
||||
|
||||
def test_object_acl_list_view(self):
|
||||
self._create_acl()
|
||||
|
||||
response = self.get(
|
||||
viewname='rest_api:accesscontrollist-list',
|
||||
kwargs={
|
||||
'app_label': self.document_content_type.app_label,
|
||||
'model': self.document_content_type.model,
|
||||
'object_id': self.document.pk
|
||||
}
|
||||
)
|
||||
response = self._request_object_acl_list_api_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
response.data['results'][0]['content_type']['app_label'],
|
||||
self.document_content_type.app_label
|
||||
self.test_object_content_type.app_label
|
||||
)
|
||||
self.assertEqual(
|
||||
response.data['results'][0]['role']['label'], TEST_ROLE_LABEL
|
||||
response.data['results'][0]['role']['label'],
|
||||
self.test_acl.role.label
|
||||
)
|
||||
|
||||
def test_object_acl_delete_view(self):
|
||||
self._create_acl()
|
||||
def _request_acl_delete_api_view(self):
|
||||
kwargs = self.test_content_object_view_kwargs.copy()
|
||||
kwargs['acl_id'] = self.test_acl.pk
|
||||
|
||||
response = self.delete(
|
||||
viewname='rest_api:accesscontrollist-detail',
|
||||
kwargs={
|
||||
'app_label': self.document_content_type.app_label,
|
||||
'model': self.document_content_type.model,
|
||||
'object_id': self.document.pk,
|
||||
'acl_pk': self.acl.pk
|
||||
}
|
||||
return self.delete(
|
||||
viewname='rest_api:object-acl-detail',
|
||||
kwargs=kwargs
|
||||
)
|
||||
|
||||
def test_object_acl_delete_api_view_with_access(self):
|
||||
self.expected_content_type = None
|
||||
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
|
||||
response = self._request_acl_delete_api_view()
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||
self.assertEqual(AccessControlList.objects.count(), 0)
|
||||
self.assertTrue(self.test_acl not in AccessControlList.objects.all())
|
||||
|
||||
def test_object_acl_detail_view(self):
|
||||
self._create_acl()
|
||||
def test_object_acl_delete_api_view_no_permission(self):
|
||||
response = self._request_acl_delete_api_view()
|
||||
|
||||
response = self.get(
|
||||
viewname='rest_api:accesscontrollist-detail',
|
||||
kwargs={
|
||||
'app_label': self.document_content_type.app_label,
|
||||
'model': self.document_content_type.model,
|
||||
'object_id': self.document.pk,
|
||||
'acl_pk': self.acl.pk
|
||||
}
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
self.assertTrue(self.test_acl in AccessControlList.objects.all())
|
||||
|
||||
def _request_object_acl_detail_api_view(self):
|
||||
kwargs = self.test_content_object_view_kwargs.copy()
|
||||
kwargs['acl_id'] = self.test_acl.pk
|
||||
|
||||
return self.get(
|
||||
viewname='rest_api:object-acl-detail',
|
||||
kwargs=kwargs
|
||||
)
|
||||
|
||||
def test_object_acl_detail_api_view_with_access(self):
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_view)
|
||||
|
||||
response = self._request_object_acl_detail_api_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
response.data['content_type']['app_label'],
|
||||
self.document_content_type.app_label
|
||||
self.test_object_content_type.app_label
|
||||
)
|
||||
self.assertEqual(
|
||||
response.data['role']['label'], TEST_ROLE_LABEL
|
||||
response.data['role']['label'], self.test_acl.role.label
|
||||
)
|
||||
|
||||
def test_object_acl_permission_delete_view(self):
|
||||
self._create_acl()
|
||||
permission = self.acl.permissions.first()
|
||||
def test_object_acl_detail_api_view_no_permission(self):
|
||||
response = self._request_object_acl_detail_api_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
response = self.delete(
|
||||
viewname='rest_api:accesscontrollist-permission-detail',
|
||||
kwargs={
|
||||
'app_label': self.document_content_type.app_label,
|
||||
'model': self.document_content_type.model,
|
||||
'object_id': self.document.pk,
|
||||
'acl_pk': self.acl.pk, 'permission_pk': permission.pk
|
||||
}
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
||||
self.assertEqual(self.acl.permissions.count(), 0)
|
||||
def _request_object_acl_permission_list_api_view(self):
|
||||
kwargs = self.test_content_object_view_kwargs.copy()
|
||||
kwargs['acl_id'] = self.test_acl.pk
|
||||
|
||||
def test_object_acl_permission_detail_view(self):
|
||||
self._create_acl()
|
||||
permission = self.acl.permissions.first()
|
||||
|
||||
response = self.get(
|
||||
viewname='rest_api:accesscontrollist-permission-detail',
|
||||
kwargs={
|
||||
'app_label': self.document_content_type.app_label,
|
||||
'model': self.document_content_type.model,
|
||||
'object_id': self.document.pk, 'acl_pk': self.acl.pk,
|
||||
'permission_pk': permission.pk
|
||||
}
|
||||
return self.get(
|
||||
viewname='rest_api:object-acl-permission-list',
|
||||
kwargs=kwargs
|
||||
)
|
||||
|
||||
def test_object_acl_permission_list_api_view_with_access(self):
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_view)
|
||||
|
||||
response = self._request_object_acl_permission_list_api_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
response.data['permission_pk'], permission_document_view.pk
|
||||
response.data['results'][0]['pk'],
|
||||
self.test_permission.pk
|
||||
)
|
||||
|
||||
def test_object_acl_permission_list_view(self):
|
||||
self._create_acl()
|
||||
def test_object_acl_permission_list_api_view_no_permission(self):
|
||||
response = self._request_object_acl_permission_list_api_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
response = self.get(
|
||||
viewname='rest_api:accesscontrollist-permission-list',
|
||||
kwargs={
|
||||
'app_label': self.document_content_type.app_label,
|
||||
'model': self.document_content_type.model,
|
||||
'object_id': self.document.pk,
|
||||
'acl_pk': self.acl.pk
|
||||
}
|
||||
def _request_object_acl_permission_remove_api_view(self):
|
||||
kwargs = self.test_content_object_view_kwargs.copy()
|
||||
kwargs['acl_id'] = self.test_acl.pk
|
||||
|
||||
return self.post(
|
||||
viewname='rest_api:object-acl-permission-remove',
|
||||
kwargs=kwargs, data={'permission_id_list': self.test_permission.pk}
|
||||
)
|
||||
|
||||
def test_object_acl_permission_remove_api_view_with_access(self):
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
|
||||
|
||||
response = self._request_object_acl_permission_remove_api_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(self.test_permission.stored_permission not in self.test_acl.permissions.all())
|
||||
|
||||
def test_object_acl_permission_remove_api_view_no_permission(self):
|
||||
response = self._request_object_acl_permission_remove_api_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
self.assertTrue(self.test_permission.stored_permission in self.test_acl.permissions.all())
|
||||
|
||||
def _request_object_acl_permission_add_api_view(self):
|
||||
kwargs = self.test_content_object_view_kwargs.copy()
|
||||
kwargs['acl_id'] = self.test_acl.pk
|
||||
|
||||
return self.post(
|
||||
viewname='rest_api:object-acl-permission-add',
|
||||
kwargs=kwargs, data={'permission_id_list': self.test_permission.pk}
|
||||
)
|
||||
|
||||
def test_object_acl_permission_add_api_view_with_access(self):
|
||||
self.test_acl.permissions.clear()
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
|
||||
|
||||
response = self._request_object_acl_permission_add_api_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(self.test_permission.stored_permission in self.test_acl.permissions.all())
|
||||
|
||||
def test_object_acl_permission_add_api_view_no_permission(self):
|
||||
self.test_acl.permissions.clear()
|
||||
|
||||
response = self._request_object_acl_permission_add_api_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
self.assertTrue(self.test_permission.stored_permission not in self.test_acl.permissions.all())
|
||||
|
||||
def _request_object_acl_inherited_permission_list_api_view(self):
|
||||
kwargs = self.test_content_object_view_kwargs.copy()
|
||||
kwargs['acl_id'] = self.test_acl.pk
|
||||
|
||||
return self.get(
|
||||
viewname='rest_api:object-acl-permission-inherited-list',
|
||||
kwargs=kwargs
|
||||
)
|
||||
|
||||
def test_object_acl_inherited_permission_list_api_view_with_access(self):
|
||||
self.test_acl.permissions.clear()
|
||||
self.test_role.grant(permission=self.test_permission)
|
||||
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_view)
|
||||
|
||||
response = self._request_object_acl_inherited_permission_list_api_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
response.data['results'][0]['permission_pk'],
|
||||
permission_document_view.pk
|
||||
response.data['results'][0]['pk'],
|
||||
self.test_permission.pk
|
||||
)
|
||||
|
||||
def test_object_acl_permission_list_post_view(self):
|
||||
self._create_acl()
|
||||
def test_object_acl_inherited_permission_list_api_view_no_permission(self):
|
||||
self.test_acl.permissions.clear()
|
||||
self.test_role.grant(permission=self.test_permission)
|
||||
|
||||
response = self.post(
|
||||
viewname='rest_api:accesscontrollist-permission-list',
|
||||
kwargs={
|
||||
'app_label': self.document_content_type.app_label,
|
||||
'model': self.document_content_type.model,
|
||||
'object_id': self.document.pk, 'acl_pk': self.acl.pk
|
||||
}, data={'permission_pk': permission_acl_view.pk}
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
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.post(
|
||||
viewname='rest_api:accesscontrollist-list',
|
||||
kwargs={
|
||||
'app_label': self.document_content_type.app_label,
|
||||
'model': self.document_content_type.model,
|
||||
'object_id': self.document.pk
|
||||
}, data={'role_pk': self.role.pk}
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
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.post(
|
||||
viewname='rest_api:accesscontrollist-list',
|
||||
kwargs={
|
||||
'app_label': self.document_content_type.app_label,
|
||||
'model': self.document_content_type.model,
|
||||
'object_id': self.document.pk
|
||||
}, data={
|
||||
'role_pk': self.role.pk,
|
||||
'permissions_pk_list': permission_acl_view.pk
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
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
|
||||
)
|
||||
response = self._request_object_acl_inherited_permission_list_api_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
@@ -1,100 +1,84 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls import reverse
|
||||
|
||||
from mayan.apps.documents.tests import GenericDocumentViewTestCase
|
||||
from mayan.apps.common.tests import GenericViewTestCase
|
||||
|
||||
from ..links import (
|
||||
link_acl_create, link_acl_delete, link_acl_list, link_acl_permissions
|
||||
)
|
||||
from ..models import AccessControlList
|
||||
from ..permissions import permission_acl_edit, permission_acl_view
|
||||
|
||||
from .mixins import ACLTestMixin
|
||||
|
||||
class ACLsLinksTestCase(GenericDocumentViewTestCase):
|
||||
def test_document_acl_create_link(self):
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=self.document, role=self.role
|
||||
)
|
||||
|
||||
acl.permissions.add(permission_acl_edit.stored_permission)
|
||||
self.login_user()
|
||||
class AccessControlListLinksTestCase(ACLTestMixin, GenericViewTestCase):
|
||||
auto_create_test_role = False
|
||||
|
||||
self.add_test_view(test_object=self.document)
|
||||
def setUp(self):
|
||||
super(AccessControlListLinksTestCase, self).setUp()
|
||||
self._setup_test_object()
|
||||
|
||||
def test_object_acl_create_link(self):
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
|
||||
|
||||
self.add_test_view(test_object=self.test_object)
|
||||
context = self.get_test_view()
|
||||
resolved_link = link_acl_create.resolve(context=context)
|
||||
|
||||
self.assertNotEqual(resolved_link, None)
|
||||
|
||||
content_type = ContentType.objects.get_for_model(self.document)
|
||||
kwargs = {
|
||||
'app_label': content_type.app_label,
|
||||
'model': content_type.model,
|
||||
'object_id': self.document.pk
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
resolved_link.url, reverse(viewname='acls:acl_create', kwargs=kwargs)
|
||||
resolved_link.url, reverse(
|
||||
viewname='acls:acl_create',
|
||||
kwargs=self.test_content_object_view_kwargs
|
||||
)
|
||||
)
|
||||
|
||||
def test_document_acl_delete_link(self):
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=self.document, role=self.role
|
||||
)
|
||||
def test_object_acl_delete_link(self):
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
|
||||
|
||||
acl.permissions.add(permission_acl_edit.stored_permission)
|
||||
self.login_user()
|
||||
|
||||
self.add_test_view(test_object=acl)
|
||||
self.add_test_view(test_object=self._test_case_acl)
|
||||
context = self.get_test_view()
|
||||
resolved_link = link_acl_delete.resolve(context=context)
|
||||
|
||||
self.assertNotEqual(resolved_link, None)
|
||||
|
||||
self.assertEqual(
|
||||
resolved_link.url, reverse(viewname='acls:acl_delete', kwargs={'acl_pk': acl.pk})
|
||||
resolved_link.url, reverse(
|
||||
viewname='acls:acl_delete',
|
||||
kwargs={'acl_id': self._test_case_acl.pk}
|
||||
)
|
||||
)
|
||||
|
||||
def test_document_acl_edit_link(self):
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=self.document, role=self.role
|
||||
)
|
||||
def test_object_acl_edit_link(self):
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
|
||||
|
||||
acl.permissions.add(permission_acl_edit.stored_permission)
|
||||
self.login_user()
|
||||
|
||||
self.add_test_view(test_object=acl)
|
||||
self.add_test_view(test_object=self._test_case_acl)
|
||||
context = self.get_test_view()
|
||||
resolved_link = link_acl_permissions.resolve(context=context)
|
||||
|
||||
self.assertNotEqual(resolved_link, None)
|
||||
|
||||
self.assertEqual(
|
||||
resolved_link.url, reverse(viewname='acls:acl_permissions', kwargs={'acl_pk': acl.pk})
|
||||
resolved_link.url, reverse(
|
||||
viewname='acls:acl_permissions',
|
||||
kwargs={'acl_id': self._test_case_acl.pk}
|
||||
)
|
||||
)
|
||||
|
||||
def test_document_acl_list_link(self):
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=self.document, role=self.role
|
||||
)
|
||||
def test_object_acl_list_link(self):
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_view)
|
||||
|
||||
acl.permissions.add(permission_acl_view.stored_permission)
|
||||
self.login_user()
|
||||
|
||||
self.add_test_view(test_object=self.document)
|
||||
self.add_test_view(test_object=self.test_object)
|
||||
context = self.get_test_view()
|
||||
resolved_link = link_acl_list.resolve(context=context)
|
||||
|
||||
self.assertNotEqual(resolved_link, None)
|
||||
|
||||
content_type = ContentType.objects.get_for_model(self.document)
|
||||
kwargs = {
|
||||
'app_label': content_type.app_label,
|
||||
'model': content_type.model,
|
||||
'object_id': self.document.pk
|
||||
}
|
||||
|
||||
self.assertEqual(
|
||||
resolved_link.url, reverse(viewname='acls:acl_list', kwargs=kwargs)
|
||||
resolved_link.url, reverse(
|
||||
viewname='acls:acl_list',
|
||||
kwargs=self.test_content_object_view_kwargs
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,159 +1,401 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import models
|
||||
|
||||
from mayan.apps.common.tests import BaseTestCase
|
||||
from mayan.apps.documents.models import Document, DocumentType
|
||||
from mayan.apps.documents.permissions import permission_document_view
|
||||
from mayan.apps.documents.tests import (
|
||||
TEST_DOCUMENT_TYPE_2_LABEL, TEST_DOCUMENT_TYPE_LABEL,
|
||||
TEST_SMALL_DOCUMENT_PATH
|
||||
DocumentTestMixin, TEST_DOCUMENT_TYPE_2_LABEL, TEST_DOCUMENT_TYPE_LABEL
|
||||
)
|
||||
|
||||
from ..classes import ModelPermission
|
||||
from ..models import AccessControlList
|
||||
|
||||
from .mixins import ACLTestMixin
|
||||
|
||||
|
||||
class PermissionTestCase(DocumentTestMixin, BaseTestCase):
|
||||
auto_create_document_type = False
|
||||
|
||||
class PermissionTestCase(BaseTestCase):
|
||||
def setUp(self):
|
||||
super(PermissionTestCase, self).setUp()
|
||||
self.document_type_1 = DocumentType.objects.create(
|
||||
self.test_document_type_1 = DocumentType.objects.create(
|
||||
label=TEST_DOCUMENT_TYPE_LABEL
|
||||
)
|
||||
|
||||
self.document_type_2 = DocumentType.objects.create(
|
||||
self.test_document_type_2 = DocumentType.objects.create(
|
||||
label=TEST_DOCUMENT_TYPE_2_LABEL
|
||||
)
|
||||
|
||||
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
|
||||
self.document_1 = self.document_type_1.new_document(
|
||||
file_object=file_object
|
||||
)
|
||||
|
||||
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
|
||||
self.document_2 = self.document_type_1.new_document(
|
||||
file_object=file_object
|
||||
)
|
||||
|
||||
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
|
||||
self.document_3 = self.document_type_2.new_document(
|
||||
file_object=file_object
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
for document_type in DocumentType.objects.all():
|
||||
document_type.delete()
|
||||
super(PermissionTestCase, self).tearDown()
|
||||
self.test_document_1 = self.upload_document(
|
||||
document_type=self.test_document_type_1
|
||||
)
|
||||
self.test_document_2 = self.upload_document(
|
||||
document_type=self.test_document_type_1
|
||||
)
|
||||
self.test_document_3 = self.upload_document(
|
||||
document_type=self.test_document_type_2
|
||||
)
|
||||
|
||||
def test_check_access_without_permissions(self):
|
||||
with self.assertRaises(PermissionDenied):
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=(permission_document_view,),
|
||||
user=self.user, obj=self.document_1
|
||||
obj=self.test_document_1, permission=permission_document_view,
|
||||
user=self._test_case_user
|
||||
)
|
||||
|
||||
def test_filtering_without_permissions(self):
|
||||
self.assertQuerysetEqual(
|
||||
AccessControlList.objects.filter_by_access(
|
||||
permission=permission_document_view, user=self.user,
|
||||
queryset=Document.objects.all()
|
||||
), []
|
||||
self.assertEqual(
|
||||
AccessControlList.objects.restrict_queryset(
|
||||
permission=permission_document_view,
|
||||
queryset=Document.objects.all(), user=self._test_case_user,
|
||||
).count(), 0
|
||||
)
|
||||
|
||||
def test_check_access_with_acl(self):
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=self.document_1, role=self.role
|
||||
content_object=self.test_document_1, role=self._test_case_role
|
||||
)
|
||||
acl.permissions.add(permission_document_view.stored_permission)
|
||||
|
||||
try:
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=(permission_document_view,), user=self.user,
|
||||
obj=self.document_1
|
||||
obj=self.test_document_1, permission=permission_document_view,
|
||||
user=self._test_case_user
|
||||
)
|
||||
except PermissionDenied:
|
||||
self.fail('PermissionDenied exception was not expected.')
|
||||
|
||||
def test_filtering_with_permissions(self):
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=self.document_1, role=self.role
|
||||
content_object=self.test_document_1, role=self._test_case_role
|
||||
)
|
||||
acl.permissions.add(permission_document_view.stored_permission)
|
||||
|
||||
self.assertQuerysetEqual(
|
||||
AccessControlList.objects.filter_by_access(
|
||||
permission=permission_document_view, user=self.user,
|
||||
queryset=Document.objects.all()
|
||||
), (repr(self.document_1),)
|
||||
AccessControlList.objects.restrict_queryset(
|
||||
permission=permission_document_view,
|
||||
queryset=Document.objects.all(), user=self._test_case_user
|
||||
), (repr(self.test_document_1),)
|
||||
)
|
||||
|
||||
def test_check_access_with_inherited_acl(self):
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=self.document_type_1, role=self.role
|
||||
content_object=self.test_document_type_1, role=self._test_case_role
|
||||
)
|
||||
acl.permissions.add(permission_document_view.stored_permission)
|
||||
|
||||
try:
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=(permission_document_view,), user=self.user,
|
||||
obj=self.document_1
|
||||
obj=self.test_document_1, permission=permission_document_view,
|
||||
user=self._test_case_user
|
||||
)
|
||||
except PermissionDenied:
|
||||
self.fail('PermissionDenied exception was not expected.')
|
||||
|
||||
def test_check_access_with_inherited_acl_and_local_acl(self):
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=self.document_type_1, role=self.role
|
||||
def test_check_access_with_inherited_acl_and_direct_acl(self):
|
||||
test_acl_1 = AccessControlList.objects.create(
|
||||
content_object=self.test_document_type_1, role=self._test_case_role
|
||||
)
|
||||
acl.permissions.add(permission_document_view.stored_permission)
|
||||
test_acl_1.permissions.add(permission_document_view.stored_permission)
|
||||
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=self.document_3, role=self.role
|
||||
test_acl_2 = AccessControlList.objects.create(
|
||||
content_object=self.test_document_3, role=self._test_case_role
|
||||
)
|
||||
acl.permissions.add(permission_document_view.stored_permission)
|
||||
test_acl_2.permissions.add(permission_document_view.stored_permission)
|
||||
|
||||
try:
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=(permission_document_view,), user=self.user,
|
||||
obj=self.document_3
|
||||
obj=self.test_document_3, permission=permission_document_view,
|
||||
user=self._test_case_user
|
||||
)
|
||||
except PermissionDenied:
|
||||
self.fail('PermissionDenied exception was not expected.')
|
||||
|
||||
def test_filtering_with_inherited_permissions(self):
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=self.document_type_1, role=self.role
|
||||
content_object=self.test_document_type_1, role=self._test_case_role
|
||||
)
|
||||
acl.permissions.add(permission_document_view.stored_permission)
|
||||
|
||||
result = AccessControlList.objects.filter_by_access(
|
||||
permission=permission_document_view, user=self.user,
|
||||
queryset=Document.objects.all()
|
||||
result = AccessControlList.objects.restrict_queryset(
|
||||
permission=permission_document_view, queryset=Document.objects.all(),
|
||||
user=self._test_case_user
|
||||
)
|
||||
|
||||
# Since document_1 and document_2 are of document_type_1
|
||||
# they are the only ones that should be returned
|
||||
|
||||
self.assertTrue(self.document_1 in result)
|
||||
self.assertTrue(self.document_2 in result)
|
||||
self.assertTrue(self.document_3 not in result)
|
||||
self.assertTrue(self.test_document_1 in result)
|
||||
self.assertTrue(self.test_document_2 in result)
|
||||
self.assertTrue(self.test_document_3 not in result)
|
||||
|
||||
def test_filtering_with_inherited_permissions_and_local_acl(self):
|
||||
self.role.permissions.add(permission_document_view.stored_permission)
|
||||
self._test_case_role.permissions.add(
|
||||
permission_document_view.stored_permission
|
||||
)
|
||||
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=self.document_type_1, role=self.role
|
||||
content_object=self.test_document_type_1, role=self._test_case_role
|
||||
)
|
||||
acl.permissions.add(permission_document_view.stored_permission)
|
||||
|
||||
acl = AccessControlList.objects.create(
|
||||
content_object=self.document_3, role=self.role
|
||||
content_object=self.test_document_3, role=self._test_case_role
|
||||
)
|
||||
acl.permissions.add(permission_document_view.stored_permission)
|
||||
|
||||
result = AccessControlList.objects.filter_by_access(
|
||||
permission=permission_document_view, user=self.user,
|
||||
queryset=Document.objects.all()
|
||||
result = AccessControlList.objects.restrict_queryset(
|
||||
permission=permission_document_view, queryset=Document.objects.all(),
|
||||
user=self._test_case_user,
|
||||
)
|
||||
self.assertTrue(self.document_1 in result)
|
||||
self.assertTrue(self.document_2 in result)
|
||||
self.assertTrue(self.document_3 in result)
|
||||
self.assertTrue(self.test_document_1 in result)
|
||||
self.assertTrue(self.test_document_2 in result)
|
||||
self.assertTrue(self.test_document_3 in result)
|
||||
|
||||
|
||||
class InheritedPermissionTestCase(ACLTestMixin, BaseTestCase):
|
||||
def test_retrieve_inherited_role_permission_not_model_applicable(self):
|
||||
self._create_test_model()
|
||||
self.test_object = self.TestModel.objects.create()
|
||||
self._create_test_acl()
|
||||
self._create_test_permission()
|
||||
|
||||
self.test_role.grant(permission=self.test_permission)
|
||||
|
||||
queryset = AccessControlList.objects.get_inherited_permissions(
|
||||
obj=self.test_object, role=self.test_role
|
||||
)
|
||||
self.assertTrue(self.test_permission.stored_permission not in queryset)
|
||||
|
||||
def test_retrieve_inherited_role_permission_model_applicable(self):
|
||||
self._create_test_model()
|
||||
self.test_object = self.TestModel.objects.create()
|
||||
self._create_test_acl()
|
||||
self._create_test_permission()
|
||||
|
||||
ModelPermission.register(
|
||||
model=self.test_object._meta.model, permissions=(
|
||||
self.test_permission,
|
||||
)
|
||||
)
|
||||
self.test_role.grant(permission=self.test_permission)
|
||||
|
||||
queryset = AccessControlList.objects.get_inherited_permissions(
|
||||
obj=self.test_object, role=self.test_role
|
||||
)
|
||||
self.assertTrue(self.test_permission.stored_permission in queryset)
|
||||
|
||||
def test_retrieve_inherited_related_parent_child_permission(self):
|
||||
self._create_test_permission()
|
||||
|
||||
self._create_test_model(model_name='TestModelParent')
|
||||
self._create_test_model(
|
||||
fields={
|
||||
'parent': models.ForeignKey(
|
||||
on_delete=models.CASCADE, related_name='children',
|
||||
to='TestModelParent',
|
||||
)
|
||||
}, model_name='TestModelChild'
|
||||
)
|
||||
|
||||
ModelPermission.register(
|
||||
model=self.TestModelParent, permissions=(
|
||||
self.test_permission,
|
||||
)
|
||||
)
|
||||
ModelPermission.register(
|
||||
model=self.TestModelChild, permissions=(
|
||||
self.test_permission,
|
||||
)
|
||||
)
|
||||
ModelPermission.register_inheritance(
|
||||
model=self.TestModelChild, related='parent',
|
||||
)
|
||||
|
||||
parent = self.TestModelParent.objects.create()
|
||||
child = self.TestModelChild.objects.create(parent=parent)
|
||||
|
||||
AccessControlList.objects.grant(
|
||||
obj=parent, permission=self.test_permission, role=self.test_role
|
||||
)
|
||||
|
||||
queryset = AccessControlList.objects.get_inherited_permissions(
|
||||
obj=child, role=self.test_role
|
||||
)
|
||||
|
||||
self.assertTrue(self.test_permission.stored_permission in queryset)
|
||||
|
||||
def test_retrieve_inherited_related_grandparent_parent_child_permission(self):
|
||||
self._create_test_permission()
|
||||
|
||||
self._create_test_model(model_name='TestModelGrandParent')
|
||||
self._create_test_model(
|
||||
fields={
|
||||
'parent': models.ForeignKey(
|
||||
on_delete=models.CASCADE, related_name='children',
|
||||
to='TestModelGrandParent',
|
||||
)
|
||||
}, model_name='TestModelParent'
|
||||
)
|
||||
self._create_test_model(
|
||||
fields={
|
||||
'parent': models.ForeignKey(
|
||||
on_delete=models.CASCADE, related_name='children',
|
||||
to='TestModelParent',
|
||||
)
|
||||
}, model_name='TestModelChild'
|
||||
)
|
||||
|
||||
ModelPermission.register(
|
||||
model=self.TestModelGrandParent, permissions=(
|
||||
self.test_permission,
|
||||
)
|
||||
)
|
||||
ModelPermission.register(
|
||||
model=self.TestModelParent, permissions=(
|
||||
self.test_permission,
|
||||
)
|
||||
)
|
||||
ModelPermission.register(
|
||||
model=self.TestModelChild, permissions=(
|
||||
self.test_permission,
|
||||
)
|
||||
)
|
||||
|
||||
ModelPermission.register_inheritance(
|
||||
model=self.TestModelChild, related='parent',
|
||||
)
|
||||
ModelPermission.register_inheritance(
|
||||
model=self.TestModelParent, related='parent',
|
||||
)
|
||||
|
||||
grandparent = self.TestModelGrandParent.objects.create()
|
||||
parent = self.TestModelParent.objects.create(parent=grandparent)
|
||||
child = self.TestModelChild.objects.create(parent=parent)
|
||||
|
||||
AccessControlList.objects.grant(
|
||||
obj=grandparent, permission=self.test_permission,
|
||||
role=self.test_role
|
||||
)
|
||||
|
||||
queryset = AccessControlList.objects.get_inherited_permissions(
|
||||
obj=child, role=self.test_role
|
||||
)
|
||||
|
||||
self.assertTrue(self.test_permission.stored_permission in queryset)
|
||||
|
||||
|
||||
class MultipleAccessTestCase(ACLTestMixin, BaseTestCase):
|
||||
def setUp(self):
|
||||
super(MultipleAccessTestCase, self).setUp()
|
||||
self._create_test_permission()
|
||||
self._create_test_permission_2()
|
||||
|
||||
self._create_test_model(model_name='TestModelParent1')
|
||||
self._create_test_model(model_name='TestModelParent2')
|
||||
self._create_test_model(
|
||||
fields={
|
||||
'parent_1': models.ForeignKey(
|
||||
on_delete=models.CASCADE, related_name='children1',
|
||||
to='TestModelParent1',
|
||||
),
|
||||
'parent_2': models.ForeignKey(
|
||||
on_delete=models.CASCADE, related_name='children2',
|
||||
to='TestModelParent2',
|
||||
)
|
||||
}, model_name='TestModelChild'
|
||||
)
|
||||
|
||||
ModelPermission.register(
|
||||
model=self.TestModelParent1, permissions=(
|
||||
self.test_permission,
|
||||
)
|
||||
)
|
||||
ModelPermission.register(
|
||||
model=self.TestModelParent2, permissions=(
|
||||
self.test_permission_2,
|
||||
)
|
||||
)
|
||||
|
||||
self.test_object_parent_1 = self.TestModelParent1.objects.create()
|
||||
self.test_object_parent_2 = self.TestModelParent2.objects.create()
|
||||
self.test_object_child = self.TestModelChild.objects.create(
|
||||
parent_1=self.test_object_parent_1, parent_2=self.test_object_parent_2
|
||||
)
|
||||
|
||||
ModelPermission.register_inheritance(
|
||||
model=self.TestModelChild, related='parent_1'
|
||||
)
|
||||
ModelPermission.register_inheritance(
|
||||
model=self.TestModelChild, related='parent_2'
|
||||
)
|
||||
|
||||
def test_restrict_queryset_and_operator_first_permission(self):
|
||||
self.grant_access(obj=self.test_object_parent_1, permission=self.test_permission)
|
||||
|
||||
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
|
||||
operator=AccessControlList.OPERATOR_AND,
|
||||
permissions=(self.test_permission, self.test_permission_2),
|
||||
queryset=self.TestModelChild.objects.all(),
|
||||
user=self._test_case_user
|
||||
)
|
||||
self.assertTrue(self.test_object_child not in queryset)
|
||||
|
||||
def test_restrict_queryset_and_operator_second_permission(self):
|
||||
self.grant_access(obj=self.test_object_parent_2, permission=self.test_permission_2)
|
||||
|
||||
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
|
||||
operator=AccessControlList.OPERATOR_AND,
|
||||
permissions=(self.test_permission, self.test_permission_2),
|
||||
queryset=self.TestModelChild.objects.all(),
|
||||
user=self._test_case_user
|
||||
)
|
||||
self.assertTrue(self.test_object_child not in queryset)
|
||||
|
||||
def test_restrict_queryset_and_operator_both_permissions(self):
|
||||
self.grant_access(obj=self.test_object_parent_1, permission=self.test_permission)
|
||||
self.grant_access(obj=self.test_object_parent_2, permission=self.test_permission_2)
|
||||
|
||||
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
|
||||
operator=AccessControlList.OPERATOR_AND,
|
||||
permissions=(self.test_permission, self.test_permission_2),
|
||||
queryset=self.TestModelChild.objects.all(),
|
||||
user=self._test_case_user
|
||||
)
|
||||
self.assertTrue(self.test_object_child in queryset)
|
||||
|
||||
def test_restrict_queryset_or_operator_first_permission(self):
|
||||
self.grant_access(obj=self.test_object_parent_1, permission=self.test_permission)
|
||||
|
||||
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
|
||||
operator=AccessControlList.OPERATOR_OR,
|
||||
permissions=(self.test_permission, self.test_permission_2),
|
||||
queryset=self.TestModelChild.objects.all(),
|
||||
user=self._test_case_user
|
||||
)
|
||||
self.assertTrue(self.test_object_child in queryset)
|
||||
|
||||
def test_restrict_queryset_or_operator_second_permission(self):
|
||||
self.grant_access(obj=self.test_object_parent_2, permission=self.test_permission_2)
|
||||
|
||||
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
|
||||
operator=AccessControlList.OPERATOR_OR,
|
||||
permissions=(self.test_permission, self.test_permission_2),
|
||||
queryset=self.TestModelChild.objects.all(),
|
||||
user=self._test_case_user
|
||||
)
|
||||
self.assertTrue(self.test_object_child in queryset)
|
||||
|
||||
def test_restrict_queryset_or_operator_both_permissions(self):
|
||||
self.grant_access(obj=self.test_object_parent_1, permission=self.test_permission)
|
||||
self.grant_access(obj=self.test_object_parent_2, permission=self.test_permission_2)
|
||||
|
||||
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
|
||||
operator=AccessControlList.OPERATOR_OR,
|
||||
permissions=(self.test_permission, self.test_permission_2),
|
||||
queryset=self.TestModelChild.objects.all(),
|
||||
user=self._test_case_user
|
||||
)
|
||||
self.assertTrue(self.test_object_child in queryset)
|
||||
|
||||
@@ -1,136 +1,159 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from mayan.apps.documents.tests import GenericDocumentViewTestCase
|
||||
from mayan.apps.permissions.tests.mixins import RoleTestMixin
|
||||
from mayan.apps.common.tests import GenericViewTestCase
|
||||
|
||||
from ..classes import ModelPermission
|
||||
from ..models import AccessControlList
|
||||
from ..permissions import permission_acl_edit, permission_acl_view
|
||||
|
||||
from .mixins import ACLTestMixin
|
||||
|
||||
class AccessControlListViewTestCase(RoleTestMixin, GenericDocumentViewTestCase):
|
||||
|
||||
class AccessControlListViewTestCase(ACLTestMixin, GenericViewTestCase):
|
||||
def setUp(self):
|
||||
super(AccessControlListViewTestCase, self).setUp()
|
||||
self.login_user()
|
||||
self._create_test_role()
|
||||
|
||||
self.test_object = self.document
|
||||
self._create_test_model()
|
||||
self._create_test_object()
|
||||
ModelPermission.register(
|
||||
model=self.test_object._meta.model, permissions=(
|
||||
permission_acl_edit, permission_acl_view,
|
||||
)
|
||||
)
|
||||
|
||||
content_type = ContentType.objects.get_for_model(self.test_object)
|
||||
self._create_test_permission()
|
||||
ModelPermission.register(
|
||||
model=self.test_object._meta.model, permissions=(
|
||||
self.test_permission,
|
||||
)
|
||||
)
|
||||
|
||||
self.view_content_object_arguments = {
|
||||
'app_label': content_type.app_label,
|
||||
'model': content_type.model,
|
||||
'object_id': self.test_object.pk
|
||||
}
|
||||
self._inject_test_object_content_type()
|
||||
|
||||
def _request_get_acl_create_view(self):
|
||||
self._create_test_acl()
|
||||
self.test_acl.permissions.add(self.test_permission.stored_permission)
|
||||
|
||||
def _request_acl_create_get_view(self):
|
||||
return self.get(
|
||||
viewname='acls:acl_create',
|
||||
kwargs=self.view_content_object_arguments, data={
|
||||
kwargs=self.test_content_object_view_kwargs, data={
|
||||
'role': self.test_role.pk
|
||||
}
|
||||
)
|
||||
|
||||
def test_acl_create_view_get_no_permission(self):
|
||||
response = self._request_get_acl_create_view()
|
||||
def test_acl_create_get_view_no_permission(self):
|
||||
self.test_acl.delete()
|
||||
|
||||
response = self._request_acl_create_get_view()
|
||||
self.assertEqual(response.status_code, 404)
|
||||
self.assertEqual(AccessControlList.objects.count(), 0)
|
||||
|
||||
def test_acl_create_view_get_with_document_access(self):
|
||||
self.assertFalse(self.test_object.acls.filter(role=self.test_role).exists())
|
||||
|
||||
def test_acl_create_get_view_with_object_access(self):
|
||||
self.test_acl.delete()
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
|
||||
|
||||
response = self._request_get_acl_create_view()
|
||||
|
||||
response = self._request_acl_create_get_view()
|
||||
self.assertContains(
|
||||
response=response, text=force_text(self.test_object),
|
||||
status_code=200
|
||||
)
|
||||
|
||||
def _request_post_acl_create_view(self):
|
||||
self.assertFalse(self.test_object.acls.filter(role=self.test_role).exists())
|
||||
|
||||
def _request_acl_create_post_view(self):
|
||||
return self.post(
|
||||
viewname='acls:acl_create',
|
||||
kwargs=self.view_content_object_arguments, data={
|
||||
kwargs=self.test_content_object_view_kwargs, data={
|
||||
'role': self.test_role.pk
|
||||
}
|
||||
)
|
||||
|
||||
def test_acl_create_view_post_no_permission(self):
|
||||
response = self._request_post_acl_create_view()
|
||||
self.test_acl.delete()
|
||||
|
||||
response = self._request_acl_create_post_view()
|
||||
self.assertEqual(response.status_code, 404)
|
||||
self.assertEqual(AccessControlList.objects.count(), 0)
|
||||
|
||||
def test_acl_create_view_post_with_document_access(self):
|
||||
self.assertFalse(self.test_object.acls.filter(role=self.test_role).exists())
|
||||
|
||||
def test_acl_create_view_post_with_access(self):
|
||||
self.test_acl.delete()
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
|
||||
response = self._request_post_acl_create_view()
|
||||
|
||||
response = self._request_acl_create_post_view()
|
||||
self.assertEqual(response.status_code, 302)
|
||||
# 2 ACLs: 1 created by the test and the other by the self.grant_access
|
||||
self.assertEqual(AccessControlList.objects.count(), 2)
|
||||
|
||||
def test_acl_create_duplicate_view_with_permission(self):
|
||||
self.assertTrue(self.test_object.acls.filter(role=self.test_role).exists())
|
||||
|
||||
def test_acl_create_duplicate_view_with_access(self):
|
||||
"""
|
||||
Test creating a duplicate ACL entry: same object & role
|
||||
Result: Should redirect to existing ACL for object + role combination
|
||||
"""
|
||||
self._create_test_acl()
|
||||
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
|
||||
|
||||
response = self._request_post_acl_create_view()
|
||||
response = self._request_acl_create_post_view()
|
||||
self.assertNotContains(
|
||||
response=response, text=force_text(self.test_acl.role),
|
||||
status_code=200
|
||||
)
|
||||
|
||||
# 2 ACLs: 1 created by the test and the other by the self.grant_access
|
||||
self.assertEqual(AccessControlList.objects.count(), 2)
|
||||
self.assertEqual(
|
||||
AccessControlList.objects.first().pk, self.test_acl.pk
|
||||
|
||||
# Sorted by role PK
|
||||
expected_results = sorted(
|
||||
[
|
||||
{
|
||||
# Test role, created and then requested,
|
||||
# but created only once
|
||||
'object_id': self.test_object.pk,
|
||||
'role': self.test_role.pk
|
||||
},
|
||||
{
|
||||
# Test case ACL for the test case role, ignored
|
||||
'object_id': self.test_object.pk,
|
||||
'role': self._test_case_role.pk
|
||||
},
|
||||
], key=lambda item: item['role']
|
||||
)
|
||||
|
||||
def _create_test_acl(self):
|
||||
self.test_acl = AccessControlList.objects.create(
|
||||
content_object=self.test_object, role=self.test_role
|
||||
self.assertQuerysetEqual(
|
||||
qs=AccessControlList.objects.order_by('role__id').values(
|
||||
'object_id', 'role',
|
||||
), transform=dict, values=expected_results
|
||||
)
|
||||
|
||||
def _request_acl_delete_view(self):
|
||||
return self.post(
|
||||
viewname='acls:acl_delete', kwargs={'acl_pk': self.test_acl.pk}
|
||||
viewname='acls:acl_delete', kwargs={'acl_id': self.test_acl.pk}
|
||||
)
|
||||
|
||||
def test_acl_delete_view_no_permission(self):
|
||||
self._create_test_acl()
|
||||
|
||||
response = self._request_acl_delete_view()
|
||||
self.assertNotContains(
|
||||
response=response, text=force_text(self.test_object),
|
||||
status_code=404
|
||||
)
|
||||
# 1 ACL: the test one
|
||||
self.assertQuerysetEqual(
|
||||
qs=AccessControlList.objects.all(), values=(repr(self.test_acl),)
|
||||
)
|
||||
|
||||
self.assertTrue(self.test_object.acls.filter(role=self.test_role).exists())
|
||||
|
||||
def test_acl_delete_view_with_access(self):
|
||||
self._create_test_acl()
|
||||
|
||||
acl = self.grant_access(
|
||||
self.grant_access(
|
||||
obj=self.test_object, permission=permission_acl_edit
|
||||
)
|
||||
|
||||
response = self._request_acl_delete_view()
|
||||
self.assertEqual(response.status_code, 302)
|
||||
# 1 ACL: the one created by the self.grant_access
|
||||
self.assertQuerysetEqual(
|
||||
qs=AccessControlList.objects.all(), values=(repr(acl),)
|
||||
)
|
||||
|
||||
self.assertFalse(self.test_object.acls.filter(role=self.test_role).exists())
|
||||
|
||||
def _request_acl_list_view(self):
|
||||
return self.get(
|
||||
viewname='acls:acl_list', kwargs=self.view_content_object_arguments
|
||||
viewname='acls:acl_list', kwargs=self.test_content_object_view_kwargs
|
||||
)
|
||||
|
||||
def test_acl_list_view_no_permission(self):
|
||||
@@ -151,28 +174,66 @@ class AccessControlListViewTestCase(RoleTestMixin, GenericDocumentViewTestCase):
|
||||
status_code=200
|
||||
)
|
||||
|
||||
def _request_get_acl_permissions_view(self):
|
||||
def _request_get_acl_permissions_get_view(self):
|
||||
return self.get(
|
||||
viewname='acls:acl_permissions',
|
||||
kwargs={'acl_pk': self.test_acl.pk}
|
||||
kwargs={'acl_id': self.test_acl.pk}
|
||||
)
|
||||
|
||||
def test_acl_permissions_view_get_no_permission(self):
|
||||
self._create_test_acl()
|
||||
def test_acl_permissions_get_view_no_permission(self):
|
||||
self.test_acl.permissions.clear()
|
||||
|
||||
response = self._request_get_acl_permissions_view()
|
||||
response = self._request_get_acl_permissions_get_view()
|
||||
self.assertNotContains(
|
||||
response=response, text=force_text(self.test_object),
|
||||
status_code=404
|
||||
)
|
||||
|
||||
def test_acl_permissions_view_get_with_access(self):
|
||||
self._create_test_acl()
|
||||
self.assertFalse(
|
||||
self.test_object.acls.filter(permissions=self.test_permission.stored_permission).exists()
|
||||
)
|
||||
|
||||
def test_acl_permissions_get_view_with_access(self):
|
||||
self.test_acl.permissions.clear()
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
|
||||
|
||||
response = self._request_get_acl_permissions_view()
|
||||
response = self._request_get_acl_permissions_get_view()
|
||||
self.assertContains(
|
||||
response=response, text=force_text(self.test_object),
|
||||
status_code=200
|
||||
)
|
||||
|
||||
self.assertFalse(
|
||||
self.test_object.acls.filter(permissions=self.test_permission.stored_permission).exists()
|
||||
)
|
||||
|
||||
def _request_post_acl_permissions_post_view(self):
|
||||
return self.post(
|
||||
viewname='acls:acl_permissions',
|
||||
kwargs={'acl_id': self.test_acl.pk},
|
||||
data={'available-selection': self.test_permission.stored_permission.pk}
|
||||
)
|
||||
|
||||
def test_acl_permissions_post_view_no_permission(self):
|
||||
self.test_acl.permissions.clear()
|
||||
|
||||
response = self._request_post_acl_permissions_post_view()
|
||||
self.assertNotContains(
|
||||
response=response, text=force_text(self.test_object),
|
||||
status_code=404
|
||||
)
|
||||
|
||||
self.assertFalse(
|
||||
self.test_object.acls.filter(permissions=self.test_permission.stored_permission).exists()
|
||||
)
|
||||
|
||||
def test_acl_permissions_post_view_with_access(self):
|
||||
self.test_acl.permissions.clear()
|
||||
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
|
||||
|
||||
response = self._request_post_acl_permissions_post_view()
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
self.assertTrue(
|
||||
self.test_object.acls.filter(permissions=self.test_permission.stored_permission).exists()
|
||||
)
|
||||
|
||||
@@ -2,50 +2,33 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from .api_views import (
|
||||
APIObjectACLListView, APIObjectACLPermissionListView,
|
||||
APIObjectACLPermissionView, APIObjectACLView
|
||||
)
|
||||
from .api_views import ObjectACLAPIViewSet
|
||||
from .views import (
|
||||
ACLCreateView, ACLDeleteView, ACLListView, ACLPermissionsView
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
url(
|
||||
regex=r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/create/$',
|
||||
regex=r'^objects/(?P<app_label>[-\w]+)/(?P<model_name>[-\w]+)/(?P<object_id>\d+)/create/$',
|
||||
name='acl_create', view=ACLCreateView.as_view()
|
||||
),
|
||||
url(
|
||||
regex=r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/list/$',
|
||||
regex=r'^objects/(?P<app_label>[-\w]+)/(?P<model_name>[-\w]+)/(?P<object_id>\d+)/list/$',
|
||||
name='acl_list', view=ACLListView.as_view()
|
||||
),
|
||||
url(
|
||||
regex=r'^acls/(?P<acl_pk>\d+)/delete/$', name='acl_delete',
|
||||
regex=r'^acls/(?P<acl_id>\d+)/delete/$', name='acl_delete',
|
||||
view=ACLDeleteView.as_view()
|
||||
),
|
||||
url(
|
||||
regex=r'^acls/(?P<acl_pk>\d+)/permissions/$', name='acl_permissions',
|
||||
regex=r'^acls/(?P<acl_id>\d+)/permissions/$', name='acl_permissions',
|
||||
view=ACLPermissionsView.as_view()
|
||||
),
|
||||
]
|
||||
|
||||
api_urls = [
|
||||
url(
|
||||
regex=r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/acls/$',
|
||||
name='accesscontrollist-list', view=APIObjectACLListView.as_view()
|
||||
),
|
||||
url(
|
||||
regex=r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/acls/(?P<acl_pk>\d+)/$',
|
||||
name='accesscontrollist-detail', view=APIObjectACLView.as_view()
|
||||
),
|
||||
url(
|
||||
regex=r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/acls/(?P<acl_pk>\d+)/permissions/$',
|
||||
name='accesscontrollist-permission-list',
|
||||
view=APIObjectACLPermissionListView.as_view()
|
||||
),
|
||||
url(
|
||||
regex=r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/acls/(?P<acl_pk>\d+)/permissions/(?P<permission_pk>\d+)/$',
|
||||
name='accesscontrollist-permission-detail',
|
||||
view=APIObjectACLPermissionView.as_view()
|
||||
),
|
||||
]
|
||||
api_router_entries = (
|
||||
{
|
||||
'prefix': r'apps/(?P<app_label>[^/.]+)/models/(?P<model_name>[^/.]+)/objects/(?P<object_id>[^/.]+)/acls',
|
||||
'viewset': ObjectACLAPIViewSet, 'basename': 'object-acl'
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.template import RequestContext
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from mayan.apps.common.mixins import (
|
||||
ContentTypeViewMixin, ExternalObjectViewMixin
|
||||
ContentTypeViewMixin, ExternalObjectMixin
|
||||
)
|
||||
from mayan.apps.common.views import (
|
||||
AssignRemoveView, SingleObjectCreateView, SingleObjectDeleteView,
|
||||
from mayan.apps.common.generics import (
|
||||
AddRemoveView, SingleObjectCreateView, SingleObjectDeleteView,
|
||||
SingleObjectListView
|
||||
)
|
||||
from mayan.apps.permissions import Permission, PermissionNamespace
|
||||
from mayan.apps.permissions.models import Role, StoredPermission
|
||||
from mayan.apps.permissions.models import Role
|
||||
|
||||
from .classes import ModelPermission
|
||||
from .forms import ACLCreateForm
|
||||
@@ -30,7 +26,11 @@ from .permissions import permission_acl_edit, permission_acl_view
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ACLCreateView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectCreateView):
|
||||
class ACLCreateView(ContentTypeViewMixin, ExternalObjectMixin, SingleObjectCreateView):
|
||||
content_type_url_kw_args = {
|
||||
'app_label': 'app_label',
|
||||
'model': 'model_name'
|
||||
}
|
||||
external_object_permission = permission_acl_edit
|
||||
external_object_pk_url_kwarg = 'object_id'
|
||||
form_class = ACLCreateForm
|
||||
@@ -44,7 +44,7 @@ class ACLCreateView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectC
|
||||
def get_external_object_queryset(self):
|
||||
# Here we get a queryset the object model for which an ACL will be
|
||||
# created.
|
||||
return self.get_content_type().model_class().objects.all()
|
||||
return self.get_content_type().get_all_objects_for_this_type()
|
||||
|
||||
def get_extra_context(self):
|
||||
return {
|
||||
@@ -61,7 +61,8 @@ class ACLCreateView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectC
|
||||
'queryset': Role.objects.exclude(
|
||||
pk__in=self.get_external_object().acls.values('role')
|
||||
),
|
||||
'widget_attributes': {'class': 'select2'}
|
||||
'widget_attributes': {'class': 'select2'},
|
||||
'user': self.request.user
|
||||
}
|
||||
|
||||
def get_instance_extra_data(self):
|
||||
@@ -77,15 +78,17 @@ class ACLCreateView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectC
|
||||
|
||||
|
||||
class ACLDeleteView(SingleObjectDeleteView):
|
||||
object_permission = permission_acl_edit
|
||||
object_permission_related = 'content_object'
|
||||
object_permission_raise_404 = True
|
||||
model = AccessControlList
|
||||
pk_url_kwarg = 'acl_pk'
|
||||
object_permission = permission_acl_edit
|
||||
pk_url_kwarg = 'acl_id'
|
||||
|
||||
def get_extra_context(self):
|
||||
acl = self.get_object()
|
||||
|
||||
return {
|
||||
'object': self.get_object().content_object,
|
||||
'acl': acl,
|
||||
'object': acl.content_object,
|
||||
'navigation_object_list': ('object', 'acl'),
|
||||
'title': _('Delete ACL: %s') % self.get_object(),
|
||||
}
|
||||
|
||||
@@ -94,20 +97,24 @@ class ACLDeleteView(SingleObjectDeleteView):
|
||||
return reverse(
|
||||
'acls:acl_list', kwargs={
|
||||
'app_label': instance.content_type.app_label,
|
||||
'model': instance.content_type.model,
|
||||
'model_name': instance.content_type.model,
|
||||
'object_id': instance.object_id
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ACLListView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectListView):
|
||||
class ACLListView(ContentTypeViewMixin, ExternalObjectMixin, SingleObjectListView):
|
||||
content_type_url_kw_args = {
|
||||
'app_label': 'app_label',
|
||||
'model': 'model_name'
|
||||
}
|
||||
external_object_permission = permission_acl_view
|
||||
external_object_pk_url_kwarg = 'object_id'
|
||||
|
||||
def get_external_object_queryset(self):
|
||||
# Here we get a queryset the object model for which an ACL will be
|
||||
# created.
|
||||
return self.get_content_type().model_class().objects.all()
|
||||
return self.get_content_type().get_all_objects_for_this_type()
|
||||
|
||||
def get_extra_context(self):
|
||||
return {
|
||||
@@ -135,118 +142,88 @@ class ACLListView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectLis
|
||||
),
|
||||
}
|
||||
|
||||
def get_object_list(self):
|
||||
def get_source_queryset(self):
|
||||
return self.get_external_object().acls.all()
|
||||
|
||||
|
||||
class ACLPermissionsView(AssignRemoveView):
|
||||
grouped = True
|
||||
left_list_title = _('Available permissions')
|
||||
right_list_title = _('Granted permissions')
|
||||
class ACLPermissionsView(AddRemoveView):
|
||||
action_add_method = 'permissions_add'
|
||||
action_remove_method = 'permissions_remove'
|
||||
main_object_model = AccessControlList
|
||||
main_object_permission = permission_acl_edit
|
||||
main_object_pk_url_kwarg = 'acl_id'
|
||||
list_added_title = _('Granted permissions')
|
||||
list_available_title = _('Available permissions')
|
||||
related_field = 'permissions'
|
||||
|
||||
@staticmethod
|
||||
def generate_choices(entries):
|
||||
results = []
|
||||
def generate_choices(self, queryset):
|
||||
namespaces_dictionary = {}
|
||||
|
||||
entries = sorted(
|
||||
entries, key=lambda x: (
|
||||
x.volatile_permission.namespace.label,
|
||||
x.volatile_permission.label
|
||||
)
|
||||
# Sort permissions by their translatable label
|
||||
object_list = sorted(
|
||||
queryset, key=lambda permission: permission.volatile_permission.label
|
||||
)
|
||||
|
||||
for namespace, permissions in itertools.groupby(entries, lambda entry: entry.namespace):
|
||||
permission_options = [
|
||||
(force_text(permission.pk), permission) for permission in permissions
|
||||
]
|
||||
results.append(
|
||||
(PermissionNamespace.get(name=namespace), permission_options)
|
||||
# Group permissions by namespace
|
||||
for permission in object_list:
|
||||
namespaces_dictionary.setdefault(
|
||||
permission.volatile_permission.namespace.label,
|
||||
[]
|
||||
)
|
||||
namespaces_dictionary[permission.volatile_permission.namespace.label].append(
|
||||
(permission.pk, force_text(permission))
|
||||
)
|
||||
|
||||
return results
|
||||
# Sort permissions by their translatable namespace label
|
||||
return sorted(namespaces_dictionary.items())
|
||||
|
||||
def add(self, item):
|
||||
permission = get_object_or_404(klass=StoredPermission, pk=item)
|
||||
self.get_object().permissions.add(permission)
|
||||
|
||||
def get_available_list(self):
|
||||
return ModelPermission.get_for_instance(
|
||||
instance=self.get_object().content_object
|
||||
).exclude(id__in=self.get_granted_list().values_list('pk', flat=True))
|
||||
def get_actions_extra_kwargs(self):
|
||||
return {'_user': self.request.user}
|
||||
|
||||
def get_disabled_choices(self):
|
||||
"""
|
||||
Get permissions from a parent's acls but remove the permissions we
|
||||
already hold for this object
|
||||
Get permissions from a parent's ACLs or directly granted to the role.
|
||||
We return a list since that is what the form widget's can process.
|
||||
"""
|
||||
return map(
|
||||
str, set(
|
||||
self.get_object().get_inherited_permissions().values_list(
|
||||
'pk', flat=True
|
||||
)
|
||||
).difference(
|
||||
self.get_object().permissions.values_list('pk', flat=True)
|
||||
)
|
||||
)
|
||||
return self.main_object.get_inherited_permissions().values_list('pk', flat=True)
|
||||
|
||||
def get_extra_context(self):
|
||||
return {
|
||||
'object': self.get_object().content_object,
|
||||
'title': _('Role "%(role)s" permission\'s for "%(object)s"') % {
|
||||
'role': self.get_object().role,
|
||||
'object': self.get_object().content_object,
|
||||
},
|
||||
'acl': self.main_object,
|
||||
'object': self.main_object.content_object,
|
||||
'navigation_object_list': ('object', 'acl'),
|
||||
'title': _('Role "%(role)s" permission\'s for "%(object)s".') % {
|
||||
'role': self.main_object.role,
|
||||
'object': self.main_object.content_object,
|
||||
}
|
||||
}
|
||||
|
||||
def get_granted_list(self):
|
||||
def get_list_added_help_text(self):
|
||||
if self.main_object.get_inherited_permissions():
|
||||
return _(
|
||||
'Disabled permissions are inherited from a parent object or '
|
||||
'directly granted to the role and can\'t be removed from this '
|
||||
'view. Inherited permissions need to be removed from the '
|
||||
'parent object\'s ACL or from them role via the Setup menu.'
|
||||
)
|
||||
|
||||
def get_list_added_queryset(self):
|
||||
"""
|
||||
Merge of permissions we hold for this object and the permissions we
|
||||
hold for this object's parent via another ACL.
|
||||
hold for this object's parents via another ACL. .distinct() is added
|
||||
in case the permission was added to the ACL and then added to a
|
||||
parent ACL's and thus inherited and would appear twice. If
|
||||
order to remove the double permission from the ACL it would need to be
|
||||
remove from the parent first to enable the choice in the form,
|
||||
remove it from the ACL and then re-add it to the parent ACL.
|
||||
"""
|
||||
merged_pks = self.get_object().permissions.values_list(
|
||||
'pk', flat=True
|
||||
) | self.get_object().get_inherited_permissions().values_list(
|
||||
'pk', flat=True
|
||||
queryset_acl = super(ACLPermissionsView, self).get_list_added_queryset()
|
||||
|
||||
return (
|
||||
queryset_acl | self.main_object.get_inherited_permissions()
|
||||
).distinct()
|
||||
|
||||
def get_secondary_object_source_queryset(self):
|
||||
return ModelPermission.get_for_instance(
|
||||
instance=self.main_object.content_object
|
||||
)
|
||||
return StoredPermission.objects.filter(pk__in=merged_pks)
|
||||
|
||||
def get_object(self):
|
||||
acl = get_object_or_404(
|
||||
klass=AccessControlList, pk=self.kwargs['acl_pk']
|
||||
)
|
||||
|
||||
# Get the ACL, from this get the object of the ACL, from the object
|
||||
# get all ACLs it holds as a filtered queryset by access.
|
||||
|
||||
try:
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=(permission_acl_edit,), obj=acl.content_object,
|
||||
user=self.request.user
|
||||
)
|
||||
except PermissionDenied:
|
||||
queryset = AccessControlList.objects.none()
|
||||
else:
|
||||
queryset = acl.content_object.acls.all()
|
||||
|
||||
return get_object_or_404(klass=queryset, pk=self.kwargs['acl_pk'])
|
||||
|
||||
def get_right_list_help_text(self):
|
||||
if self.get_object().get_inherited_permissions():
|
||||
return _(
|
||||
'Disabled permissions are inherited from a parent object and '
|
||||
'can\'t be removed from this view, they need to be removed '
|
||||
'from the parent object\'s ACL view.'
|
||||
)
|
||||
|
||||
return self.right_list_help_text
|
||||
|
||||
def left_list(self):
|
||||
Permission.refresh()
|
||||
return ACLPermissionsView.generate_choices(self.get_available_list())
|
||||
|
||||
def remove(self, item):
|
||||
permission = get_object_or_404(klass=StoredPermission, pk=item)
|
||||
self.get_object().permissions.remove(permission)
|
||||
|
||||
def right_list(self):
|
||||
return ACLPermissionsView.generate_choices(self.get_granted_list())
|
||||
|
||||
@@ -89,7 +89,8 @@ class GrantAccessAction(WorkflowAction):
|
||||
|
||||
try:
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=permission_acl_edit, user=request.user, obj=obj
|
||||
obj=obj, permissions=permission_acl_edit,
|
||||
user=request.user
|
||||
)
|
||||
except Exception as exception:
|
||||
raise ValidationError(exception)
|
||||
|
||||
@@ -1 +1 @@
|
||||
DEFAULT_MAXIMUM_TITLE_LENGTH = 80
|
||||
DEFAULT_MAXIMUM_TITLE_LENGTH = 120
|
||||
|
||||
@@ -8,10 +8,10 @@ class MayanApp {
|
||||
ajaxMenusOptions: []
|
||||
}
|
||||
|
||||
this.ajaxSpinnerSeletor = '#ajax-spinner';
|
||||
this.ajaxExecuting = false;
|
||||
this.ajaxMenusOptions = options.ajaxMenusOptions;
|
||||
this.ajaxMenuHashes = {};
|
||||
this.ajaxSpinnerSeletor = '#ajax-spinner';
|
||||
this.window = $(window);
|
||||
}
|
||||
|
||||
@@ -29,29 +29,6 @@ class MayanApp {
|
||||
}
|
||||
}
|
||||
|
||||
static mayanNotificationBadge (options, data) {
|
||||
// Callback to add the notifications count inside a badge markup
|
||||
var notifications = data[options.attributeName];
|
||||
|
||||
if (notifications > 0) {
|
||||
// Save the original link text before adding the initial badge markup
|
||||
if (!options.element.data('mn-saved-text')) {
|
||||
options.element.data('mn-saved-text', options.element.html());
|
||||
}
|
||||
|
||||
options.element.html(
|
||||
options.element.data('mn-saved-text') + ' <span class="badge">' + notifications + '</span>'
|
||||
);
|
||||
} else {
|
||||
if (options.element.data('mn-saved-text')) {
|
||||
// If there is a saved original link text, restore it
|
||||
options.element.html(
|
||||
options.element.data('mn-saved-text')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static setupMultiItemActions () {
|
||||
$('body').on('change', '.check-all-slave', function () {
|
||||
MayanApp.countChecked();
|
||||
@@ -81,22 +58,6 @@ class MayanApp {
|
||||
});
|
||||
}
|
||||
|
||||
static tagSelectionTemplate (tag, container) {
|
||||
var $tag = $(
|
||||
'<span class="label label-tag" style="background: ' + tag.element.dataset.color + ';"> ' + tag.text + '</span>'
|
||||
);
|
||||
container[0].style.background = tag.element.dataset.color;
|
||||
return $tag;
|
||||
}
|
||||
|
||||
static tagResultTemplate (tag) {
|
||||
if (!tag.element) { return ''; }
|
||||
var $tag = $(
|
||||
'<span class="label label-tag" style="background: ' + tag.element.dataset.color + ';"> ' + tag.text + '</span>'
|
||||
);
|
||||
return $tag;
|
||||
}
|
||||
|
||||
static updateNavbarState () {
|
||||
var uri = new URI(window.location.hash);
|
||||
var uriFragment = uri.fragment();
|
||||
@@ -110,35 +71,6 @@ class MayanApp {
|
||||
|
||||
// Instance methods
|
||||
|
||||
AJAXperiodicWorker (options) {
|
||||
var app = this;
|
||||
|
||||
$.ajax({
|
||||
complete: function() {
|
||||
if (!options.app) {
|
||||
// Preserve the app reference between consecutive calls
|
||||
options.app = app;
|
||||
}
|
||||
setTimeout(options.app.AJAXperiodicWorker, options.interval, options);
|
||||
},
|
||||
success: function(data) {
|
||||
if (options.callback) {
|
||||
// Convert the callback string to an actual function
|
||||
var callbackFunction = window;
|
||||
|
||||
$.each(options.callback.split('.'), function (index, value) {
|
||||
callbackFunction = callbackFunction[value]
|
||||
});
|
||||
|
||||
callbackFunction(options, data);
|
||||
} else {
|
||||
options.element.text(data[options.attributeName]);
|
||||
}
|
||||
},
|
||||
url: options.APIURL
|
||||
});
|
||||
}
|
||||
|
||||
callbackAJAXSpinnerUpdate () {
|
||||
if (this.ajaxExecuting) {
|
||||
$(this.ajaxSpinnerSeletor).fadeIn(50);
|
||||
@@ -239,7 +171,6 @@ class MayanApp {
|
||||
initialize () {
|
||||
var self = this;
|
||||
|
||||
this.setupAJAXPeriodicWorkers();
|
||||
this.setupAJAXSpinner();
|
||||
this.setupFormHotkeys();
|
||||
this.setupFullHeightResizing();
|
||||
@@ -256,22 +187,6 @@ class MayanApp {
|
||||
partialNavigation.initialize();
|
||||
}
|
||||
|
||||
setupAJAXPeriodicWorkers () {
|
||||
var app = this;
|
||||
|
||||
$('a[data-apw-url]').each(function() {
|
||||
var $this = $(this);
|
||||
|
||||
app.AJAXperiodicWorker({
|
||||
attributeName: $this.data('apw-attribute'),
|
||||
APIURL: $this.data('apw-url'),
|
||||
callback: $this.data('apw-callback'),
|
||||
element: $this,
|
||||
interval: $this.data('apw-interval'),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setupAJAXSpinner () {
|
||||
var self = this;
|
||||
|
||||
@@ -445,12 +360,6 @@ class MayanApp {
|
||||
dropdownAutoWidth: true,
|
||||
width: '100%'
|
||||
});
|
||||
|
||||
$('.select2-tags').select2({
|
||||
templateSelection: MayanApp.tagSelectionTemplate,
|
||||
templateResult: MayanApp.tagResultTemplate,
|
||||
width: '100%'
|
||||
});
|
||||
}
|
||||
|
||||
resizeFullHeight () {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% get_menus_links names='facet,list facet' sort_results=True as links_facet %}
|
||||
{% navigation_resolve_menus names='facet,list facet' sort_results=True as facet_menus_link_results %}
|
||||
|
||||
<style>
|
||||
|
||||
@@ -45,12 +45,12 @@
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12 {% if links_facet %}has-sidebar{% endif %}" id="viewport">
|
||||
<div class="col-xs-12 {% if facet_menus_link_results %}has-sidebar{% endif %}" id="viewport">
|
||||
{% include 'appearance/calculate_form_title.html' %}
|
||||
|
||||
{# action menu #}
|
||||
{% get_menus_links names='object,sidebar,secondary' sort_results=True as links_actions %}
|
||||
{% if links_actions %}
|
||||
{% navigation_resolve_menus names='object,secondary' sort_results=True as action_menus_link_results %}
|
||||
{% if action_menus_link_results %}
|
||||
<div class="pull-right btn-group" id="menu-actions">
|
||||
<button aria-expanded="true" class="btn btn-danger btn-sm dropdown-toggle" data-toggle="dropdown" type="button">
|
||||
{% trans 'Actions' %}
|
||||
@@ -58,19 +58,39 @@
|
||||
<span class="sr-only">{% trans 'Toggle Dropdown' %}</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
{% for object_navigation_links in links_actions %}
|
||||
{% with 'true' as as_li %}
|
||||
{% with 'true' as hide_active_anchor %}
|
||||
{% with 'btn-sm' as link_classes %}
|
||||
{% include 'navigation/generic_navigation.html' %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% for menus_link_result in action_menus_link_results %}
|
||||
{% if action_menus_link_results|length > 1 %}
|
||||
<li class="dropdown-header">{{ menus_link_result.menu.label }}</li>
|
||||
{% endif %}
|
||||
|
||||
{% if not forloop.last and object_navigation_links %}
|
||||
{% for link_group in menus_link_result.link_groups %}
|
||||
{% if navigation_object_list %}
|
||||
|
||||
{% ifchanged link_group.object %}
|
||||
<li class="dropdown-header">{% common_get_object_verbose_name obj=link_group.object %}</li>
|
||||
{% endifchanged %}
|
||||
{% endif %}
|
||||
|
||||
{% with link_group.links as object_navigation_links %}
|
||||
{% with 'true' as as_li %}
|
||||
{% with 'true' as hide_active_anchor %}
|
||||
{% with 'btn-sm' as link_classes %}
|
||||
{% include 'navigation/generic_navigation.html' %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
|
||||
{% if not forloop.last and link_group %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
{% if not forloop.last and menus_link_result %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
@@ -80,17 +100,21 @@
|
||||
{% block footer %}{% endblock %}
|
||||
</div>
|
||||
|
||||
{% if links_facet %}
|
||||
{% if facet_menus_link_results %}
|
||||
<div id="sidebar">
|
||||
<div class="pull-right list-group">
|
||||
{% for object_navigation_links in links_facet %}
|
||||
{% with 'true' as hide_active_anchor %}
|
||||
{% with 'active' as link_class_active %}
|
||||
{% with 'list-group-item btn-sm' as link_classes %}
|
||||
{% include 'navigation/generic_navigation.html' %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% for menu_link_result in facet_menus_link_results %}
|
||||
{% for link_group in menu_link_result.link_groups %}
|
||||
{% with link_group.links as object_navigation_links %}
|
||||
{% with 'true' as hide_active_anchor %}
|
||||
{% with 'active' as link_class_active %}
|
||||
{% with 'list-group-item btn-sm' as link_classes %}
|
||||
{% include 'navigation/generic_navigation.html' %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -90,6 +90,17 @@
|
||||
<i class="fa fa-times"></i> {% if cancel_label %}{{ cancel_label }}{% else %}{% trans 'Cancel' %}{% endif %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% for button in extra_buttons %}
|
||||
<button class="btn btn-default" name="{% if form.prefix %}{{ form.prefix }}-{{ button.name }}{% else %}{{ button.name }}{% endif %}" type="submit">
|
||||
{% if button.icon_class %}
|
||||
{{ button.icon_class.render }}
|
||||
{% endif %}
|
||||
{% if button.label %}{{ button.label }}{% else %}{% if object %}{% trans 'Save' %}{% else %}{% trans 'Submit' %}{% endif %}{% endif %}
|
||||
</button>
|
||||
{% endfor %}
|
||||
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -11,8 +11,10 @@
|
||||
<div class="well center-block">
|
||||
<div class="row">
|
||||
{% with 'navigation/large_button_link.html' as link_template %}
|
||||
{% for object_navigation_links in resolved_links %}
|
||||
{% include 'navigation/generic_navigation.html' %}
|
||||
{% for menu_results in resolved_links %}
|
||||
{% with menu_results.links as object_navigation_links %}
|
||||
{% include 'navigation/generic_navigation.html' %}
|
||||
{% endwith %}
|
||||
{% empty %}
|
||||
<p class="text-center">
|
||||
{% include 'appearance/no_results.html' %}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<div class="well center-block">
|
||||
{% if object_list %}
|
||||
{% if not hide_multi_item_actions %}
|
||||
{% get_menu_links name='multi item' sort_results=True source=object_list.0 as links_multi_item %}
|
||||
{% navigation_resolve_menu name='multi item' sort_results=True source=object_list.0 as links_multi_menus_results %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label for="id_indexes_0">
|
||||
{% if links_multi_item %}
|
||||
{% if links_multi_menus_results %}
|
||||
<input class="form-multi-object-action-checkbox check-all-slave checkbox" name="pk_{{ object.pk }}" type="checkbox" />
|
||||
{% endif %}
|
||||
|
||||
@@ -84,9 +84,9 @@
|
||||
{% endfor %}
|
||||
|
||||
{% if not hide_links %}
|
||||
{% get_menus_links names='list facet,object' source=object as links %}
|
||||
{% navigation_resolve_menus names='list facet,object' source=object as facet_menus_link_results %}
|
||||
|
||||
{% if links %}
|
||||
{% if facet_menus_link_results %}
|
||||
<div class="dropdown text-center">
|
||||
<button aria-expanded="false" aria-haspopup="true" class="btn btn-default btn-danger btn-sm dropdown-toggle" data-toggle="dropdown">
|
||||
{% trans 'Actions' %}
|
||||
@@ -94,18 +94,25 @@
|
||||
<span class="sr-only">{% trans 'Toggle Dropdown' %}</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
{% for object_navigation_links in links %}
|
||||
{% with 'true' as as_li %}
|
||||
{% with 'true' as hide_active_anchor %}
|
||||
{% with 'btn-sm' as link_classes %}
|
||||
{% include 'navigation/generic_navigation.html' %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% for facet_menu_link_result in facet_menus_link_results %}
|
||||
{% for link_group in facet_menu_link_result.link_groups %}
|
||||
|
||||
{% if not forloop.last and object_navigation_links %}
|
||||
{% with link_group.links as object_navigation_links %}
|
||||
{% with 'true' as as_li %}
|
||||
{% with 'true' as hide_active_anchor %}
|
||||
{% with 'btn-sm' as link_classes %}
|
||||
{% include 'navigation/generic_navigation.html' %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
{% if not forloop.last and facet_menu_link_result %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<div class="well center-block">
|
||||
{% if object_list %}
|
||||
{% if not hide_multi_item_actions %}
|
||||
{% get_menu_links name='multi item' sort_results=True source=object_list.0 as links_multi_item %}
|
||||
{% navigation_resolve_menu name='multi item' sort_results=True source=object_list.0 as links_multi_menus_results %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -42,21 +42,36 @@
|
||||
<thead>
|
||||
{% if not hide_header %}
|
||||
<tr>
|
||||
{% if links_multi_item %}
|
||||
{% if links_multi_menus_results %}
|
||||
<th class="first"></th>
|
||||
{% endif %}
|
||||
|
||||
{% if not hide_object %}
|
||||
<th>{% trans 'Identifier' %}</th>
|
||||
{% else %}
|
||||
{% get_source_columns source=object_list only_identifier=True as source_column %}
|
||||
{% if source_column %}
|
||||
<th>
|
||||
{% if source_column.is_sortable %}
|
||||
<a href="{% get_sort_field_querystring column=source_column %}">{{ source_column.label }}
|
||||
{% if source_column.get_sort_field == sort_field %}
|
||||
{% if icon_sort %}{{ icon_sort.render }}{% endif %}
|
||||
{% endif %}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ source_column.label }}
|
||||
{% endif %}
|
||||
</th>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if not hide_columns %}
|
||||
{% get_source_columns source=object_list as source_columns %}
|
||||
{% get_source_columns source=object_list exclude_identifier=True as source_columns %}
|
||||
{% for column in source_columns %}
|
||||
<th>
|
||||
{% if column.is_sortable %}
|
||||
<a href="{% get_sort_field_querystring column=column %}">{{ column.label }}
|
||||
{% if column.attribute == sort_field %}
|
||||
{% if column.get_sort_field == sort_field %}
|
||||
{% if icon_sort %}{{ icon_sort.render }}{% endif %}
|
||||
{% endif %}
|
||||
</a>
|
||||
@@ -80,7 +95,7 @@
|
||||
<tbody>
|
||||
{% for object in object_list %}
|
||||
<tr>
|
||||
{% if links_multi_item %}
|
||||
{% if links_multi_menus_results %}
|
||||
<td>
|
||||
<input class="form-multi-object-action-checkbox check-all-slave checkbox" name="pk_{{ object.pk }}" type="checkbox" value="" />
|
||||
</td>
|
||||
@@ -100,7 +115,7 @@
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if not hide_columns %}
|
||||
{% if not hide_columns %}
|
||||
{% get_source_columns source=object exclude_identifier=True as source_columns %}
|
||||
{% for column in source_columns %}
|
||||
<td>
|
||||
@@ -116,21 +131,29 @@
|
||||
{% endfor %}
|
||||
{% if not hide_links %}
|
||||
<td class="last">
|
||||
{% get_menu_links name='list facet' sort_results=True source=object as resolved_links %}
|
||||
{% for object_navigation_links in resolved_links %}
|
||||
{% with 'true' as horizontal %}
|
||||
{% with 'true' as hide_icon %}
|
||||
{% include 'navigation/generic_navigation.html' %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% navigation_resolve_menu name='list facet' sort_results=True source=object as facet_menus_results %}
|
||||
{% for facet_menu_results in facet_menus_results %}
|
||||
{% for link_group in facet_menu_results.link_groups %}
|
||||
{% with link_group.links as object_navigation_links %}
|
||||
{% with 'true' as horizontal %}
|
||||
{% with 'true' as hide_icon %}
|
||||
{% include 'navigation/generic_navigation.html' %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% get_menu_links name='object' source=object as resolved_links %}
|
||||
{% for object_navigation_links in resolved_links %}
|
||||
{% with 'true' as horizontal %}
|
||||
{% with 'true' as hide_icon %}
|
||||
{% include 'navigation/generic_navigation.html' %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% navigation_resolve_menu name='object' source=object as object_menus_results %}
|
||||
{% for object_menu_results in object_menus_results %}
|
||||
{% for link_group in object_menu_results.link_groups %}
|
||||
{% with link_group.links as object_navigation_links %}
|
||||
{% with 'true' as horizontal %}
|
||||
{% with 'true' as hide_icon %}
|
||||
{% include 'navigation/generic_navigation.html' %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</td>
|
||||
{% endif %}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="pull-left">
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group">
|
||||
{% if links_multi_item %}
|
||||
{% if links_multi_menus_results %}
|
||||
<a class="btn btn-default btn-sm check-all" data-checked=false data-icon-checked="fa fa-check-square" data-icon-unchecked="far fa-square" title="{% trans 'Select/Deselect all' %}">
|
||||
<i class="far fa-square"></i>
|
||||
</a>
|
||||
@@ -19,7 +19,7 @@
|
||||
</div>
|
||||
|
||||
|
||||
{% if links_multi_item %}
|
||||
{% if links_multi_menus_results %}
|
||||
<p class="pull-right" id="multi-item-title" style="margin-top: 4px;">{% trans 'Select items to activate bulk actions. Use Shift + click to select many.' %}</p>
|
||||
|
||||
<div class="pull-right btn-group" id="multi-item-actions" style="display: none;">
|
||||
@@ -29,16 +29,20 @@
|
||||
<span class="sr-only">{% trans 'Toggle Dropdown' %}</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
{% for object_navigation_links in links_multi_item %}
|
||||
{% with 'true' as as_li %}
|
||||
{% with 'true' as hide_active_anchor %}
|
||||
{% with 'btn-sm btn-multi-item-action' as link_classes %}
|
||||
{% include 'navigation/generic_navigation.html' %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% for multi_item_menu_results in links_multi_menus_results %}
|
||||
{% for link_group in multi_item_menu_results.link_groups %}
|
||||
{% with link_group.links as object_navigation_links %}
|
||||
{% with 'true' as as_li %}
|
||||
{% with 'true' as hide_active_anchor %}
|
||||
{% with 'btn-sm btn-multi-item-action' as link_classes %}
|
||||
{% include 'navigation/generic_navigation.html' %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
|
||||
{% if not forloop.last and object_navigation_links %}
|
||||
{% endfor %}
|
||||
{% if not forloop.last and link_group %}
|
||||
<li class="divider"></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
@@ -8,58 +8,62 @@
|
||||
|
||||
{% spaceless %}
|
||||
<div class="panel-group" id="accordion-sidebar" role="tablist" aria-multiselectable="true">
|
||||
{% get_menu_links name='main' as menu_links %}
|
||||
{% for link_set in menu_links %}
|
||||
{% for link in link_set %}
|
||||
{% with 'active' as li_class_active %}
|
||||
{% with ' ' as link_classes %}
|
||||
{% if link|get_type == "<class 'mayan.apps.navigation.classes.Menu'>" %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" role="tab" id="headingOne">
|
||||
<h4 class="panel-title">
|
||||
<a class="non-ajax collapsed" role="button" data-toggle="collapse" data-parent="#accordion-sidebar" href="#accordion-body-{{ forloop.counter }}" aria-expanded="false" aria-controls="collapseOne">
|
||||
<div class="pull-left">
|
||||
{% if link.icon %}
|
||||
<i class="hidden-xs hidden-sm hidden-md {{ link.icon }}"></i>
|
||||
{% endif %}
|
||||
{% if link.icon_class %}{{ link.icon_class.render }}{% endif %}
|
||||
{{ link.label }}
|
||||
</div>
|
||||
<div class="accordion-indicator pull-right"><span class="caret"></span></div>
|
||||
<div class="clearfix"></div>
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="accordion-body-{{ forloop.counter }}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingOne">
|
||||
<div class="panel-body">
|
||||
<ul class="list-unstyled">
|
||||
{% get_menu_links name=link.name as menu_links %}
|
||||
{% for linkset in menu_links %}
|
||||
{% with '' as link_class_active %}
|
||||
{% with 'a-main-menu-accordion-link' as link_classes %}
|
||||
{% with 'true' as as_li %}
|
||||
{% with linkset as object_navigation_links %}
|
||||
{% include 'navigation/generic_navigation.html' %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% navigation_resolve_menu name='main' as main_menus_results %}
|
||||
{% for main_menu_results in main_menus_results %}
|
||||
{% for link_group in main_menu_results.link_groups %}
|
||||
{% for link in link_group.links %}
|
||||
{% with 'active' as li_class_active %}
|
||||
{% with ' ' as link_classes %}
|
||||
{% if link|get_type == "<class 'mayan.apps.navigation.classes.Menu'>" %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" role="tab" id="headingOne">
|
||||
<h4 class="panel-title">
|
||||
<a class="non-ajax collapsed" role="button" data-toggle="collapse" data-parent="#accordion-sidebar" href="#accordion-body-{{ forloop.counter }}" aria-expanded="false" aria-controls="collapseOne">
|
||||
<div class="pull-left">
|
||||
{% if link.icon %}
|
||||
<i class="hidden-xs hidden-sm hidden-md {{ link.icon }}"></i>
|
||||
{% endif %}
|
||||
{% if link.icon_class %}{{ link.icon_class.render }}{% endif %}
|
||||
{{ link.label }}
|
||||
</div>
|
||||
<div class="accordion-indicator pull-right"><span class="caret"></span></div>
|
||||
<div class="clearfix"></div>
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="accordion-body-{{ forloop.counter }}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingOne">
|
||||
<div class="panel-body">
|
||||
<ul class="list-unstyled">
|
||||
{% navigation_resolve_menu name=link.name as sub_menus_results %}
|
||||
{% for sub_menu_results in sub_menus_results %}
|
||||
{% for link_group in sub_menu_results.link_groups %}
|
||||
{% with '' as link_class_active %}
|
||||
{% with 'a-main-menu-accordion-link' as link_classes %}
|
||||
{% with 'true' as as_li %}
|
||||
{% with link_group.links as object_navigation_links %}
|
||||
{% include 'navigation/generic_navigation.html' %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" role="tab" id="headingOne">
|
||||
<h4 class="panel-title">
|
||||
{% include 'navigation/generic_link_instance.html' %}
|
||||
</h4>
|
||||
{% else %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" role="tab" id="headingOne">
|
||||
<h4 class="panel-title">
|
||||
{% include 'navigation/generic_link_instance.html' %}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -18,20 +18,22 @@
|
||||
|
||||
<div id="navbar" class="navbar-collapse collapse">
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
{% get_menu_links name='topbar' as menu_links %}
|
||||
{% for link_set in menu_links %}
|
||||
{% for link in link_set %}
|
||||
{% with 'true' as as_li %}
|
||||
{% with 'true' as hide_active_anchor %}
|
||||
{% with 'active' as li_class_active %}
|
||||
{% with 'first' as li_class_first %}
|
||||
{% with ' ' as link_classes %}
|
||||
{% include 'navigation/generic_subnavigation.html' %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% navigation_resolve_menu name='topbar' as topbar_menus_results %}
|
||||
{% for tobpar_menu_result in topbar_menus_results %}
|
||||
{% for link_group in tobpar_menu_result.link_groups %}
|
||||
{% for link in link_group.links %}
|
||||
{% with 'true' as as_li %}
|
||||
{% with 'true' as hide_active_anchor %}
|
||||
{% with 'active' as li_class_active %}
|
||||
{% with 'first' as li_class_first %}
|
||||
{% with ' ' as link_classes %}
|
||||
{% include 'navigation/generic_subnavigation.html' %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
@@ -28,4 +28,4 @@ def get_form_media_js(form):
|
||||
|
||||
@register.simple_tag
|
||||
def get_icon(icon_path):
|
||||
return import_string(icon_path).render()
|
||||
return import_string(dotted_path=icon_path).render()
|
||||
|
||||
@@ -23,8 +23,10 @@ class EmailAuthenticationForm(forms.Form):
|
||||
remember_me = forms.BooleanField(label=_('Remember me'), required=False)
|
||||
|
||||
error_messages = {
|
||||
'invalid_login': _('Please enter a correct email and password. '
|
||||
'Note that the password field is case-sensitive.'),
|
||||
'invalid_login': _(
|
||||
'Please enter a correct email and password. Note that the '
|
||||
'password field is case-sensitive.'
|
||||
),
|
||||
'inactive': _('This account is inactive.'),
|
||||
}
|
||||
|
||||
@@ -56,8 +58,10 @@ class EmailAuthenticationForm(forms.Form):
|
||||
return self.cleaned_data
|
||||
|
||||
def check_for_test_cookie(self):
|
||||
warnings.warn('check_for_test_cookie is deprecated; ensure your login '
|
||||
'view is CSRF-protected.', DeprecationWarning)
|
||||
warnings.warn(
|
||||
'check_for_test_cookie is deprecated; ensure your login view '
|
||||
'is CSRF-protected.', DeprecationWarning
|
||||
)
|
||||
|
||||
def get_user_id(self):
|
||||
if self.user_cache:
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
<div class="col-xs-10 col-xs-offset-1 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3 col-lg-4 col-lg-offset-4">
|
||||
<div class="alert alert-success" role="alert">{% trans 'Password reset complete! Click the link below to login.' %}</div>
|
||||
|
||||
<div class="text-center"><a class="btn btn-primary" href="{% url 'authentication:logout_view' %}">{% trans 'Login page' %}</a></div>
|
||||
<div class="text-center"><a class="btn btn-primary" href="{% url 'authentication:login_view' %}">{% trans 'Login page' %}</a></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock content_plain %}
|
||||
|
||||
@@ -8,8 +8,7 @@ from django.urls import reverse
|
||||
from mayan.apps.common.tests import GenericViewTestCase
|
||||
from mayan.apps.smart_settings.classes import Namespace
|
||||
from mayan.apps.user_management.tests.literals import (
|
||||
TEST_ADMIN_EMAIL, TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME,
|
||||
TEST_USER_PASSWORD_EDITED
|
||||
TEST_CASE_USER_EMAIL, TEST_CASE_USER_PASSWORD, TEST_CASE_USER_USERNAME,
|
||||
)
|
||||
|
||||
from ..settings import setting_maximum_session_length
|
||||
@@ -19,11 +18,12 @@ from .literals import TEST_EMAIL_AUTHENTICATION_BACKEND
|
||||
|
||||
class UserLoginTestCase(GenericViewTestCase):
|
||||
"""
|
||||
Test that users can login via the supported authentication methods
|
||||
Test that users can login using the supported authentication methods
|
||||
"""
|
||||
authenticated_url = '{}?next={}'.format(
|
||||
reverse(settings.LOGIN_URL), reverse(viewname='documents:document_list')
|
||||
)
|
||||
auto_login_user = False
|
||||
|
||||
def setUp(self):
|
||||
super(UserLoginTestCase, self).setUp()
|
||||
@@ -42,7 +42,7 @@ class UserLoginTestCase(GenericViewTestCase):
|
||||
@override_settings(AUTHENTICATION_LOGIN_METHOD='username')
|
||||
def test_username_login(self):
|
||||
logged_in = self.login(
|
||||
username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD
|
||||
username=TEST_CASE_USER_USERNAME, password=TEST_CASE_USER_PASSWORD
|
||||
)
|
||||
self.assertTrue(logged_in)
|
||||
response = self._request_authenticated_view()
|
||||
@@ -53,12 +53,12 @@ class UserLoginTestCase(GenericViewTestCase):
|
||||
def test_email_login(self):
|
||||
with self.settings(AUTHENTICATION_BACKENDS=(TEST_EMAIL_AUTHENTICATION_BACKEND,)):
|
||||
logged_in = self.login(
|
||||
username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD
|
||||
username=TEST_CASE_USER_USERNAME, password=TEST_CASE_USER_PASSWORD
|
||||
)
|
||||
self.assertFalse(logged_in)
|
||||
|
||||
logged_in = self.login(
|
||||
email=TEST_ADMIN_EMAIL, password=TEST_ADMIN_PASSWORD
|
||||
email=TEST_CASE_USER_EMAIL, password=TEST_CASE_USER_PASSWORD
|
||||
)
|
||||
self.assertTrue(logged_in)
|
||||
|
||||
@@ -75,8 +75,8 @@ class UserLoginTestCase(GenericViewTestCase):
|
||||
|
||||
response = self.post(
|
||||
viewname=settings.LOGIN_URL, data={
|
||||
'username': TEST_ADMIN_USERNAME,
|
||||
'password': TEST_ADMIN_PASSWORD
|
||||
'username': TEST_CASE_USER_USERNAME,
|
||||
'password': TEST_CASE_USER_PASSWORD
|
||||
}
|
||||
)
|
||||
response = self._request_authenticated_view()
|
||||
@@ -93,7 +93,7 @@ class UserLoginTestCase(GenericViewTestCase):
|
||||
|
||||
response = self.post(
|
||||
viewname=settings.LOGIN_URL, data={
|
||||
'email': TEST_ADMIN_EMAIL, 'password': TEST_ADMIN_PASSWORD
|
||||
'email': TEST_CASE_USER_EMAIL, 'password': TEST_CASE_USER_PASSWORD
|
||||
}, follow=True
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -106,8 +106,8 @@ class UserLoginTestCase(GenericViewTestCase):
|
||||
def test_username_remember_me(self):
|
||||
response = self.post(
|
||||
viewname=settings.LOGIN_URL, data={
|
||||
'username': TEST_ADMIN_USERNAME,
|
||||
'password': TEST_ADMIN_PASSWORD,
|
||||
'username': TEST_CASE_USER_USERNAME,
|
||||
'password': TEST_CASE_USER_PASSWORD,
|
||||
'remember_me': True
|
||||
}, follow=True
|
||||
)
|
||||
@@ -125,8 +125,8 @@ class UserLoginTestCase(GenericViewTestCase):
|
||||
def test_username_dont_remember_me(self):
|
||||
response = self.post(
|
||||
viewname=settings.LOGIN_URL, data={
|
||||
'username': TEST_ADMIN_USERNAME,
|
||||
'password': TEST_ADMIN_PASSWORD,
|
||||
'username': TEST_CASE_USER_USERNAME,
|
||||
'password': TEST_CASE_USER_PASSWORD,
|
||||
'remember_me': False
|
||||
}, follow=True
|
||||
)
|
||||
@@ -141,8 +141,8 @@ class UserLoginTestCase(GenericViewTestCase):
|
||||
with self.settings(AUTHENTICATION_BACKENDS=(TEST_EMAIL_AUTHENTICATION_BACKEND,)):
|
||||
response = self.post(
|
||||
viewname=settings.LOGIN_URL, data={
|
||||
'email': TEST_ADMIN_EMAIL,
|
||||
'password': TEST_ADMIN_PASSWORD,
|
||||
'email': TEST_CASE_USER_EMAIL,
|
||||
'password': TEST_CASE_USER_PASSWORD,
|
||||
'remember_me': True
|
||||
}, follow=True
|
||||
)
|
||||
@@ -161,8 +161,8 @@ class UserLoginTestCase(GenericViewTestCase):
|
||||
with self.settings(AUTHENTICATION_BACKENDS=(TEST_EMAIL_AUTHENTICATION_BACKEND,)):
|
||||
response = self.post(
|
||||
viewname=settings.LOGIN_URL, data={
|
||||
'email': TEST_ADMIN_EMAIL,
|
||||
'password': TEST_ADMIN_PASSWORD,
|
||||
'email': TEST_CASE_USER_EMAIL,
|
||||
'password': TEST_CASE_USER_PASSWORD,
|
||||
'remember_me': False
|
||||
}
|
||||
)
|
||||
@@ -176,7 +176,7 @@ class UserLoginTestCase(GenericViewTestCase):
|
||||
def test_password_reset(self):
|
||||
response = self.post(
|
||||
viewname='authentication:password_reset_view', data={
|
||||
'email': TEST_ADMIN_EMAIL,
|
||||
'email': TEST_CASE_USER_EMAIL,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -188,15 +188,15 @@ class UserLoginTestCase(GenericViewTestCase):
|
||||
response = self.post(
|
||||
viewname='authentication:password_reset_confirm_view',
|
||||
args=uid_token[-3:-1], data={
|
||||
'new_password1': TEST_USER_PASSWORD_EDITED,
|
||||
'new_password2': TEST_USER_PASSWORD_EDITED,
|
||||
'new_password1': TEST_CASE_USER_PASSWORD,
|
||||
'new_password2': TEST_CASE_USER_PASSWORD,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
self.login(
|
||||
username=TEST_ADMIN_USERNAME, password=TEST_USER_PASSWORD_EDITED
|
||||
username=TEST_CASE_USER_USERNAME, password=TEST_CASE_USER_PASSWORD
|
||||
)
|
||||
|
||||
response = self._request_authenticated_view()
|
||||
@@ -209,8 +209,8 @@ class UserLoginTestCase(GenericViewTestCase):
|
||||
path='{}?next={}'.format(
|
||||
reverse(settings.LOGIN_URL), TEST_REDIRECT_URL
|
||||
), data={
|
||||
'username': TEST_ADMIN_USERNAME,
|
||||
'password': TEST_ADMIN_PASSWORD,
|
||||
'username': TEST_CASE_USER_USERNAME,
|
||||
'password': TEST_CASE_USER_PASSWORD,
|
||||
'remember_me': False
|
||||
}, follow=True
|
||||
)
|
||||
|
||||
@@ -1,43 +1,44 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import url
|
||||
from django.contrib.auth.views import logout
|
||||
|
||||
from .views import (
|
||||
login_view, password_change_done, password_change_view,
|
||||
password_reset_complete_view, password_reset_confirm_view,
|
||||
password_reset_done_view, password_reset_view
|
||||
MayanLoginView, MayanLogoutView, MayanPasswordChangeDoneView,
|
||||
MayanPasswordChangeView, MayanPasswordResetCompleteView,
|
||||
MayanPasswordResetConfirmView, MayanPasswordResetDoneView,
|
||||
MayanPasswordResetView
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url(regex=r'^login/$', name='login_view', view=login_view),
|
||||
url(regex=r'^login/$', name='login_view', view=MayanLoginView.as_view()),
|
||||
url(
|
||||
regex=r'^logout/$', kwargs={'next_page': settings.LOGIN_REDIRECT_URL},
|
||||
name='logout_view', view=logout
|
||||
regex=r'^logout/$', name='logout_view', view=MayanLogoutView.as_view()
|
||||
),
|
||||
url(
|
||||
regex=r'^password/change/$', name='password_change_view',
|
||||
view=password_change_view
|
||||
view=MayanPasswordChangeView.as_view()
|
||||
),
|
||||
url(
|
||||
regex=r'^password/change/done/$', name='password_change_done',
|
||||
view=password_change_done
|
||||
view=MayanPasswordChangeDoneView.as_view()
|
||||
),
|
||||
url(
|
||||
regex=r'^password/reset/$', name='password_reset_view',
|
||||
view=password_reset_view
|
||||
view=MayanPasswordResetView.as_view()
|
||||
),
|
||||
url(
|
||||
regex=r'^password/reset/confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
|
||||
name='password_reset_confirm_view', view=password_reset_confirm_view
|
||||
name='password_reset_confirm_view',
|
||||
view=MayanPasswordResetConfirmView.as_view()
|
||||
),
|
||||
url(
|
||||
regex=r'^password/reset/complete/$',
|
||||
name='password_reset_complete_view', view=password_reset_complete_view
|
||||
name='password_reset_complete_view',
|
||||
view=MayanPasswordResetCompleteView.as_view()
|
||||
),
|
||||
url(
|
||||
regex=r'^password/reset/done/$', name='password_reset_done_view',
|
||||
view=password_reset_done_view
|
||||
view=MayanPasswordResetDoneView.as_view()
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
from django.contrib.auth.views import (
|
||||
login, password_change, password_reset, password_reset_complete,
|
||||
password_reset_confirm, password_reset_done
|
||||
LoginView, LogoutView, PasswordChangeDoneView, PasswordChangeView,
|
||||
PasswordResetCompleteView, PasswordResetConfirmView, PasswordResetDoneView,
|
||||
PasswordResetView
|
||||
)
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import redirect, resolve_url
|
||||
from django.urls import reverse
|
||||
from django.utils.http import is_safe_url
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from stronghold.decorators import public
|
||||
from stronghold.views import StrongholdPublicMixin
|
||||
|
||||
import mayan
|
||||
from mayan.apps.common.settings import (
|
||||
@@ -24,143 +22,108 @@ from .forms import EmailAuthenticationForm, UsernameAuthenticationForm
|
||||
from .settings import setting_login_method, setting_maximum_session_length
|
||||
|
||||
|
||||
@public
|
||||
def login_view(request):
|
||||
"""
|
||||
Control how the use is to be authenticated, options are 'email' and
|
||||
'username'
|
||||
"""
|
||||
success_url_allowed_hosts = set()
|
||||
kwargs = {'template_name': 'authentication/login.html'}
|
||||
class MayanLoginView(StrongholdPublicMixin, LoginView):
|
||||
extra_context = {
|
||||
'appearance_type': 'plain'
|
||||
}
|
||||
template_name = 'authentication/login.html'
|
||||
redirect_authenticated_user = True
|
||||
|
||||
if setting_login_method.value == 'email':
|
||||
kwargs['authentication_form'] = EmailAuthenticationForm
|
||||
else:
|
||||
kwargs['authentication_form'] = UsernameAuthenticationForm
|
||||
def form_valid(self, form):
|
||||
result = super(MayanLoginView, self).form_valid(form=form)
|
||||
remember_me = form.cleaned_data.get('remember_me')
|
||||
|
||||
allowed_hosts = {request.get_host()}
|
||||
allowed_hosts.update(success_url_allowed_hosts)
|
||||
# remember_me values:
|
||||
# True - long session
|
||||
# False - short session
|
||||
# None - Form has no remember_me value and we let the session
|
||||
# expiration default.
|
||||
|
||||
redirect_to = request.POST.get(
|
||||
REDIRECT_FIELD_NAME, request.GET.get(REDIRECT_FIELD_NAME, '')
|
||||
)
|
||||
|
||||
url_is_safe = is_safe_url(
|
||||
url=redirect_to,
|
||||
allowed_hosts=allowed_hosts,
|
||||
require_https=request.is_secure(),
|
||||
)
|
||||
|
||||
url = redirect_to if url_is_safe else ''
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
extra_context = {
|
||||
'appearance_type': 'plain',
|
||||
REDIRECT_FIELD_NAME: url or resolve_url(settings.LOGIN_REDIRECT_URL)
|
||||
}
|
||||
|
||||
result = login(request, extra_context=extra_context, **kwargs)
|
||||
if request.method == 'POST':
|
||||
form = kwargs['authentication_form'](request, data=request.POST)
|
||||
if form.is_valid():
|
||||
if form.cleaned_data['remember_me']:
|
||||
request.session.set_expiry(
|
||||
setting_maximum_session_length.value
|
||||
)
|
||||
else:
|
||||
request.session.set_expiry(0)
|
||||
return result
|
||||
else:
|
||||
return HttpResponseRedirect(resolve_url(settings.LOGIN_REDIRECT_URL))
|
||||
|
||||
|
||||
def password_change_view(request):
|
||||
"""
|
||||
Password change wrapper for better control
|
||||
"""
|
||||
extra_context = {'title': _('Current user password change')}
|
||||
|
||||
if request.user.user_options.block_password_change:
|
||||
messages.error(
|
||||
request, _(
|
||||
'Changing the password is not allowed for this account.'
|
||||
if remember_me is True:
|
||||
self.request.session.set_expiry(
|
||||
setting_maximum_session_length.value
|
||||
)
|
||||
elif remember_me is False:
|
||||
self.request.session.set_expiry(0)
|
||||
|
||||
return result
|
||||
|
||||
def get_form_class(self):
|
||||
if setting_login_method.value == 'email':
|
||||
return EmailAuthenticationForm
|
||||
else:
|
||||
return UsernameAuthenticationForm
|
||||
|
||||
|
||||
class MayanLogoutView(LogoutView):
|
||||
"""No current change or overrides, left here for future expansion"""
|
||||
|
||||
|
||||
class MayanPasswordChangeDoneView(PasswordChangeDoneView):
|
||||
def dispatch(self, *args, **kwargs):
|
||||
messages.success(
|
||||
message=_('Your password has been successfully changed.'),
|
||||
request=self.request
|
||||
)
|
||||
return HttpResponseRedirect(reverse(setting_home_view.view))
|
||||
|
||||
return password_change(
|
||||
request, extra_context=extra_context,
|
||||
template_name='appearance/generic_form.html',
|
||||
post_change_redirect=reverse(viewname='authentication:password_change_done'),
|
||||
)
|
||||
return redirect(to='common:current_user_details')
|
||||
|
||||
|
||||
def password_change_done(request):
|
||||
"""
|
||||
View called when the new user password has been accepted
|
||||
"""
|
||||
messages.success(
|
||||
request, _('Your password has been successfully changed.')
|
||||
)
|
||||
return redirect('common:current_user_details')
|
||||
class MayanPasswordChangeView(PasswordChangeView):
|
||||
extra_context = {'title': _('Current user password change')}
|
||||
success_url = reverse_lazy(viewname='authentication:password_change_done')
|
||||
template_name = 'appearance/generic_form.html'
|
||||
|
||||
def dispatch(self, *args, **kwargs):
|
||||
if self.request.user.user_options.block_password_change:
|
||||
messages.error(
|
||||
message=_(
|
||||
'Changing the password is not allowed for this account.'
|
||||
), request=self.request
|
||||
)
|
||||
return HttpResponseRedirect(
|
||||
redirect_to=reverse(viewname=setting_home_view.view)
|
||||
)
|
||||
|
||||
return super(MayanPasswordChangeView, self).dispatch(*args, **kwargs)
|
||||
|
||||
|
||||
@public
|
||||
def password_reset_complete_view(request):
|
||||
class MayanPasswordResetCompleteView(StrongholdPublicMixin, PasswordResetCompleteView):
|
||||
extra_context = {
|
||||
'appearance_type': 'plain'
|
||||
}
|
||||
|
||||
return password_reset_complete(
|
||||
request=request, extra_context=extra_context,
|
||||
template_name='authentication/password_reset_complete.html'
|
||||
)
|
||||
template_name = 'authentication/password_reset_complete.html'
|
||||
|
||||
|
||||
@public
|
||||
def password_reset_confirm_view(request, uidb64=None, token=None):
|
||||
class MayanPasswordResetConfirmView(StrongholdPublicMixin, PasswordResetConfirmView):
|
||||
extra_context = {
|
||||
'appearance_type': 'plain'
|
||||
}
|
||||
|
||||
return password_reset_confirm(
|
||||
request=request, extra_context=extra_context,
|
||||
template_name='authentication/password_reset_confirm.html',
|
||||
post_reset_redirect=reverse(
|
||||
'authentication:password_reset_complete_view'
|
||||
), uidb64=uidb64, token=token
|
||||
success_url = reverse_lazy(
|
||||
viewname='authentication:password_reset_complete_view'
|
||||
)
|
||||
template_name = 'authentication/password_reset_confirm.html'
|
||||
|
||||
|
||||
@public
|
||||
def password_reset_done_view(request):
|
||||
class MayanPasswordResetDoneView(StrongholdPublicMixin, PasswordResetDoneView):
|
||||
extra_context = {
|
||||
'appearance_type': 'plain'
|
||||
}
|
||||
|
||||
return password_reset_done(
|
||||
request=request, extra_context=extra_context,
|
||||
template_name='authentication/password_reset_done.html'
|
||||
)
|
||||
template_name = 'authentication/password_reset_done.html'
|
||||
|
||||
|
||||
@public
|
||||
def password_reset_view(request):
|
||||
class MayanPasswordResetView(StrongholdPublicMixin, PasswordResetView):
|
||||
email_template_name = 'authentication/password_reset_email.html'
|
||||
extra_context = {
|
||||
'appearance_type': 'plain'
|
||||
}
|
||||
|
||||
return password_reset(
|
||||
request=request, extra_context=extra_context,
|
||||
email_template_name='authentication/password_reset_email.html',
|
||||
extra_email_context={
|
||||
'project_title': setting_project_title.value,
|
||||
'project_website': setting_project_url.value,
|
||||
'project_copyright': mayan.__copyright__,
|
||||
'project_license': mayan.__license__,
|
||||
}, subject_template_name='authentication/password_reset_subject.txt',
|
||||
template_name='authentication/password_reset_form.html',
|
||||
post_reset_redirect=reverse(
|
||||
'authentication:password_reset_done_view'
|
||||
)
|
||||
extra_email_context = {
|
||||
'project_copyright': mayan.__copyright__,
|
||||
'project_license': mayan.__license__,
|
||||
'project_title': setting_project_title.value,
|
||||
'project_website': setting_project_url.value
|
||||
}
|
||||
subject_template_name = 'authentication/password_reset_subject.txt'
|
||||
success_url = reverse_lazy(
|
||||
viewname='authentication:password_reset_done_view'
|
||||
)
|
||||
template_name = 'authentication/password_reset_form.html'
|
||||
|
||||
@@ -12,8 +12,10 @@ from .literals import TEST_FIRST_TIME_LOGIN_TEXT, TEST_MOCK_VIEW_TEXT
|
||||
class AutoAdminViewCase(GenericViewTestCase):
|
||||
auto_create_group = False
|
||||
auto_create_users = False
|
||||
auto_login_user = False
|
||||
|
||||
def setUp(self):
|
||||
super(AutoAdminViewCase, self).setUp()
|
||||
with mute_stdout():
|
||||
AutoAdminSingleton.objects.create_autoadmin()
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ class APICabinetDocumentListView(generics.ListCreateAPIView):
|
||||
def get_queryset(self):
|
||||
cabinet = self.get_cabinet()
|
||||
|
||||
return AccessControlList.objects.filter_by_access(
|
||||
return AccessControlList.objects.restrict_queryset(
|
||||
permission_document_view, self.request.user,
|
||||
queryset=cabinet.documents.all()
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ from mayan.apps.acls.permissions import (
|
||||
)
|
||||
from mayan.apps.common import (
|
||||
MayanAppConfig, menu_facet, menu_main, menu_multi_item, menu_object,
|
||||
menu_sidebar
|
||||
menu_secondary
|
||||
)
|
||||
from mayan.apps.common.classes import ModelAttribute
|
||||
from mayan.apps.documents.search import document_page_search, document_search
|
||||
@@ -75,9 +75,9 @@ class CabinetsApp(MayanAppConfig):
|
||||
permission_cabinet_remove_document
|
||||
)
|
||||
)
|
||||
ModelPermission.register_inheritance(
|
||||
model=Cabinet, related='get_root',
|
||||
)
|
||||
#ModelPermission.register_inheritance(
|
||||
# model=Cabinet, related='get_root',
|
||||
#)
|
||||
|
||||
SourceColumn(
|
||||
func=lambda context: widget_document_cabinets(
|
||||
@@ -123,7 +123,7 @@ class CabinetsApp(MayanAppConfig):
|
||||
link_cabinet_delete
|
||||
), sources=(Cabinet,)
|
||||
)
|
||||
menu_sidebar.bind_links(
|
||||
menu_secondary.bind_links(
|
||||
links=(link_document_cabinet_add, link_document_cabinet_remove),
|
||||
sources=(
|
||||
'cabinets:document_cabinet_list',
|
||||
|
||||
@@ -6,7 +6,9 @@ from mayan.apps.common.forms import FilteredSelectionForm
|
||||
|
||||
|
||||
class CabinetListForm(FilteredSelectionForm):
|
||||
_field_name = 'cabinets'
|
||||
_label = _('Cabinets')
|
||||
_widget_attributes = {'class': 'select2'}
|
||||
_allow_multiple = True
|
||||
class Meta:
|
||||
allow_multiple = True
|
||||
field_name = 'cabinets'
|
||||
label = _('Cabinets')
|
||||
required = False
|
||||
widget_attributes = {'class': 'select2'}
|
||||
|
||||
@@ -24,17 +24,17 @@ from .permissions import (
|
||||
|
||||
link_document_cabinet_list = Link(
|
||||
args='resolved_object.pk', icon_class=icon_cabinet_list,
|
||||
permissions=(permission_document_view,),
|
||||
text=_('Cabinets'), view='cabinets:document_cabinet_list',
|
||||
permission=permission_document_view, text=_('Cabinets'),
|
||||
view='cabinets:document_cabinet_list',
|
||||
)
|
||||
link_document_cabinet_remove = Link(
|
||||
args='resolved_object.pk', icon_class=icon_document_cabinet_remove,
|
||||
permissions=(permission_cabinet_remove_document,),
|
||||
permission=permission_cabinet_remove_document,
|
||||
text=_('Remove from cabinets'), view='cabinets:document_cabinet_remove'
|
||||
)
|
||||
link_document_cabinet_add = Link(
|
||||
args='object.pk', icon_class=icon_document_cabinet_add,
|
||||
permissions=(permission_cabinet_add_document,), text=_('Add to cabinets'),
|
||||
permission=permission_cabinet_add_document, text=_('Add to cabinets'),
|
||||
view='cabinets:document_cabinet_add',
|
||||
)
|
||||
link_document_multiple_cabinet_add = Link(
|
||||
@@ -61,21 +61,21 @@ link_custom_acl_list.condition = cabinet_is_root
|
||||
|
||||
link_cabinet_child_add = Link(
|
||||
args='object.pk', icon_class=icon_cabinet_child_add,
|
||||
permissions=(permission_cabinet_create,), text=_('Add new level'),
|
||||
permission=permission_cabinet_create, text=_('Add new level'),
|
||||
view='cabinets:cabinet_child_add'
|
||||
)
|
||||
link_cabinet_create = Link(
|
||||
icon_class=icon_cabinet_create, permissions=(permission_cabinet_create,),
|
||||
icon_class=icon_cabinet_create, permission=permission_cabinet_create,
|
||||
text=_('Create cabinet'), view='cabinets:cabinet_create'
|
||||
)
|
||||
link_cabinet_delete = Link(
|
||||
args='object.pk', icon_class=icon_cabinet_delete,
|
||||
permissions=(permission_cabinet_delete,), tags='dangerous',
|
||||
permission=permission_cabinet_delete, tags='dangerous',
|
||||
text=_('Delete'), view='cabinets:cabinet_delete'
|
||||
)
|
||||
link_cabinet_edit = Link(
|
||||
args='object.pk', icon_class=icon_cabinet_edit,
|
||||
permissions=(permission_cabinet_edit,), text=_('Edit'),
|
||||
permission=permission_cabinet_edit, text=_('Edit'),
|
||||
view='cabinets:cabinet_edit'
|
||||
)
|
||||
link_cabinet_list = Link(
|
||||
@@ -87,6 +87,6 @@ link_cabinet_list = Link(
|
||||
)
|
||||
link_cabinet_view = Link(
|
||||
args='object.pk', icon_class=icon_cabinet_view,
|
||||
permissions=(permission_cabinet_view,), text=_('Details'),
|
||||
permission=permission_cabinet_view, text=_('Details'),
|
||||
view='cabinets:cabinet_view'
|
||||
)
|
||||
|
||||
@@ -73,7 +73,7 @@ class Cabinet(MPTTModel):
|
||||
Provide a queryset of the documents in a cabinet. The queryset is
|
||||
filtered by access.
|
||||
"""
|
||||
return AccessControlList.objects.filter_by_access(
|
||||
return AccessControlList.objects.restrict_queryset(
|
||||
permission=permission_document_view, queryset=self.documents,
|
||||
user=user
|
||||
)
|
||||
|
||||
@@ -9,7 +9,7 @@ from .permissions import permission_cabinet_view
|
||||
cabinet_search = SearchModel(
|
||||
app_label='cabinets', model_name='Cabinet',
|
||||
permission=permission_cabinet_view,
|
||||
serializer_string='mayan.apps.cabinets.serializers.CabinetSerializer'
|
||||
serializer_path='mayan.apps.cabinets.serializers.CabinetSerializer'
|
||||
)
|
||||
|
||||
cabinet_search.add_model_field(
|
||||
|
||||
@@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ungettext
|
||||
|
||||
from mayan.apps.acls.models import AccessControlList
|
||||
from mayan.apps.common.views import (
|
||||
from mayan.apps.common.generics import (
|
||||
MultipleObjectFormActionView, SingleObjectCreateView,
|
||||
SingleObjectDeleteView, SingleObjectEditView, SingleObjectListView
|
||||
)
|
||||
@@ -64,7 +64,7 @@ class CabinetChildAddView(SingleObjectCreateView):
|
||||
cabinet = super(CabinetChildAddView, self).get_object(*args, **kwargs)
|
||||
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=permission_cabinet_edit, obj=cabinet.get_root(),
|
||||
obj=cabinet.get_root(), permission=permission_cabinet_edit,
|
||||
user=self.request.user, raise_404=True
|
||||
)
|
||||
|
||||
@@ -96,7 +96,7 @@ class CabinetDetailView(DocumentListView):
|
||||
template_name = 'cabinets/cabinet_details.html'
|
||||
|
||||
def get_document_queryset(self):
|
||||
queryset = AccessControlList.objects.filter_by_access(
|
||||
queryset = AccessControlList.objects.restrict_queryset(
|
||||
permission=permission_document_view,
|
||||
queryset=self.get_object().documents.all(),
|
||||
user=self.request.user
|
||||
@@ -146,7 +146,7 @@ class CabinetDetailView(DocumentListView):
|
||||
permission_object = cabinet.get_root()
|
||||
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=permission_cabinet_view, obj=permission_object,
|
||||
obj=permission_object, permission=permission_cabinet_view,
|
||||
user=self.request.user, raise_404=True
|
||||
)
|
||||
|
||||
@@ -187,7 +187,7 @@ class CabinetListView(SingleObjectListView):
|
||||
'no_results_title': _('No cabinets available'),
|
||||
}
|
||||
|
||||
def get_object_list(self):
|
||||
def get_source_queryset(self):
|
||||
# Add explicit ordering of root nodes since the queryset returned
|
||||
# is not affected by the model's order Meta option.
|
||||
return Cabinet.objects.root_nodes().order_by('label')
|
||||
@@ -200,8 +200,8 @@ class DocumentCabinetListView(CabinetListView):
|
||||
)
|
||||
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=permission_document_view, user=request.user,
|
||||
obj=self.document, raise_404=True
|
||||
obj=self.document, permission=permission_document_view,
|
||||
user=request.user, raise_404=True
|
||||
)
|
||||
|
||||
return super(DocumentCabinetListView, self).dispatch(
|
||||
@@ -227,7 +227,7 @@ class DocumentCabinetListView(CabinetListView):
|
||||
'title': _('Cabinets containing document: %s') % self.document,
|
||||
}
|
||||
|
||||
def get_object_list(self):
|
||||
def get_source_queryset(self):
|
||||
return self.document.get_cabinets().all()
|
||||
|
||||
|
||||
@@ -236,7 +236,7 @@ class DocumentAddToCabinetView(MultipleObjectFormActionView):
|
||||
model = Document
|
||||
object_permission = permission_cabinet_add_document
|
||||
pk_url_kwarg = 'document_pk'
|
||||
success_message = _(
|
||||
success_message_singular = _(
|
||||
'Add to cabinet request performed on %(count)d document'
|
||||
)
|
||||
success_message_plural = _(
|
||||
@@ -295,7 +295,7 @@ class DocumentAddToCabinetView(MultipleObjectFormActionView):
|
||||
|
||||
for cabinet in form.cleaned_data['cabinets']:
|
||||
AccessControlList.objects.check_access(
|
||||
obj=cabinet, permissions=permission_cabinet_add_document,
|
||||
obj=cabinet, permission=permission_cabinet_add_document,
|
||||
user=self.request.user, raise_404=True
|
||||
)
|
||||
if cabinet in cabinet_membership:
|
||||
@@ -326,7 +326,7 @@ class DocumentRemoveFromCabinetView(MultipleObjectFormActionView):
|
||||
model = Document
|
||||
object_permission = permission_cabinet_remove_document
|
||||
pk_url_kwarg = 'document_pk'
|
||||
success_message = _(
|
||||
success_message_singular = _(
|
||||
'Remove from cabinet request performed on %(count)d document'
|
||||
)
|
||||
success_message_plural = _(
|
||||
@@ -383,7 +383,7 @@ class DocumentRemoveFromCabinetView(MultipleObjectFormActionView):
|
||||
|
||||
for cabinet in form.cleaned_data['cabinets']:
|
||||
AccessControlList.objects.check_access(
|
||||
obj=cabinet, permissions=permission_cabinet_remove_document,
|
||||
obj=cabinet, permission=permission_cabinet_remove_document,
|
||||
user=self.request.user, raise_404=True
|
||||
)
|
||||
|
||||
|
||||
@@ -42,8 +42,9 @@ def widget_document_cabinets(document, user):
|
||||
app_label='acls', model_name='AccessControlList'
|
||||
)
|
||||
|
||||
cabinets = AccessControlList.objects.filter_by_access(
|
||||
permission_cabinet_view, user, queryset=document.get_cabinets().all()
|
||||
cabinets = AccessControlList.objects.restrict_queryset(
|
||||
queryset=document.get_cabinets().all(),
|
||||
permission=permission_cabinet_view, user=user
|
||||
)
|
||||
|
||||
return format_html_join(
|
||||
|
||||
@@ -9,6 +9,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from mayan.apps.sources.wizards import WizardStep
|
||||
|
||||
from .forms import CabinetListForm
|
||||
from .models import Cabinet
|
||||
from .permissions import permission_cabinet_add_document
|
||||
|
||||
|
||||
@@ -41,6 +42,7 @@ class WizardStepCabinets(WizardStep):
|
||||
return {
|
||||
'help_text': _('Cabinets to which the document will be added.'),
|
||||
'permission': permission_cabinet_add_document,
|
||||
'queryset': Cabinet.objects.all(),
|
||||
'user': wizard.request.user
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from mayan.apps.documents.permissions import permission_document_view
|
||||
|
||||
from .models import DocumentCheckout
|
||||
from .permissions import (
|
||||
permission_document_checkin, permission_document_checkin_override,
|
||||
permission_document_check_in, permission_document_check_in_override,
|
||||
permission_document_checkout_detail_view
|
||||
)
|
||||
from .serializers import (
|
||||
@@ -33,11 +33,11 @@ class APICheckedoutDocumentListView(generics.ListCreateAPIView):
|
||||
return DocumentCheckoutSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
filtered_documents = AccessControlList.objects.filter_by_access(
|
||||
filtered_documents = AccessControlList.objects.restrict_queryset(
|
||||
permission=permission_document_view, user=self.request.user,
|
||||
queryset=DocumentCheckout.objects.checked_out_documents()
|
||||
)
|
||||
filtered_documents = AccessControlList.objects.filter_by_access(
|
||||
filtered_documents = AccessControlList.objects.restrict_queryset(
|
||||
permission=permission_document_checkout_detail_view, user=self.request.user,
|
||||
queryset=filtered_documents
|
||||
)
|
||||
@@ -56,11 +56,11 @@ class APICheckedoutDocumentView(generics.RetrieveDestroyAPIView):
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.method == 'GET':
|
||||
filtered_documents = AccessControlList.objects.filter_by_access(
|
||||
filtered_documents = AccessControlList.objects.restrict_queryset(
|
||||
permission=permission_document_view, user=self.request.user,
|
||||
queryset=DocumentCheckout.objects.checked_out_documents()
|
||||
)
|
||||
filtered_documents = AccessControlList.objects.filter_by_access(
|
||||
filtered_documents = AccessControlList.objects.restrict_queryset(
|
||||
permission=permission_document_checkout_detail_view, user=self.request.user,
|
||||
queryset=filtered_documents
|
||||
)
|
||||
@@ -78,12 +78,12 @@ class APICheckedoutDocumentView(generics.RetrieveDestroyAPIView):
|
||||
|
||||
if document.checkout_info().user == request.user:
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=permission_document_checkin, user=request.user,
|
||||
permissions=permission_document_check_in, user=request.user,
|
||||
obj=document
|
||||
)
|
||||
else:
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=permission_document_checkin_override,
|
||||
permissions=permission_document_check_in_override,
|
||||
user=request.user, obj=document
|
||||
)
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from mayan.apps.acls import ModelPermission
|
||||
from mayan.apps.common import (
|
||||
MayanAppConfig, menu_facet, menu_main, menu_sidebar
|
||||
MayanAppConfig, menu_facet, menu_main, menu_multi_item, menu_secondary
|
||||
)
|
||||
from mayan.apps.dashboards.dashboards import dashboard_main
|
||||
from mayan.apps.events import ModelEventType
|
||||
@@ -22,9 +22,11 @@ from .events import (
|
||||
event_document_check_out, event_document_forceful_check_in
|
||||
)
|
||||
from .handlers import handler_check_new_version_creation
|
||||
from .hooks import hook_is_new_version_allowed
|
||||
from .links import (
|
||||
link_checkin_document, link_checkout_document, link_checkout_info,
|
||||
link_checkout_list
|
||||
link_document_check_in, link_document_checkout, link_document_checkout_info,
|
||||
link_document_checkout_list, link_document_multiple_check_in,
|
||||
link_document_multiple_checkout
|
||||
)
|
||||
from .literals import CHECK_EXPIRED_CHECK_OUTS_INTERVAL
|
||||
from .methods import (
|
||||
@@ -32,7 +34,7 @@ from .methods import (
|
||||
method_is_checked_out
|
||||
)
|
||||
from .permissions import (
|
||||
permission_document_checkin, permission_document_checkin_override,
|
||||
permission_document_check_in, permission_document_check_in_override,
|
||||
permission_document_checkout, permission_document_checkout_detail_view
|
||||
)
|
||||
from .queues import * # NOQA
|
||||
@@ -69,6 +71,10 @@ class CheckoutsApp(MayanAppConfig):
|
||||
name='is_checked_out', value=method_is_checked_out
|
||||
)
|
||||
|
||||
DocumentVersion.register_pre_save_hook(
|
||||
func=hook_is_new_version_allowed
|
||||
)
|
||||
|
||||
ModelEventType.register(
|
||||
model=Document, event_types=(
|
||||
event_document_auto_check_in, event_document_check_in,
|
||||
@@ -79,8 +85,8 @@ class CheckoutsApp(MayanAppConfig):
|
||||
ModelPermission.register(
|
||||
model=Document, permissions=(
|
||||
permission_document_checkout,
|
||||
permission_document_checkin,
|
||||
permission_document_checkin_override,
|
||||
permission_document_check_in,
|
||||
permission_document_check_in_override,
|
||||
permission_document_checkout_detail_view
|
||||
)
|
||||
)
|
||||
@@ -115,13 +121,18 @@ class CheckoutsApp(MayanAppConfig):
|
||||
widget=DashboardWidgetTotalCheckouts, order=-1
|
||||
)
|
||||
|
||||
menu_facet.bind_links(links=(link_checkout_info,), sources=(Document,))
|
||||
menu_main.bind_links(links=(link_checkout_list,), position=98)
|
||||
menu_sidebar.bind_links(
|
||||
links=(link_checkout_document, link_checkin_document),
|
||||
menu_facet.bind_links(links=(link_document_checkout_info,), sources=(Document,))
|
||||
menu_main.bind_links(links=(link_document_checkout_list,), position=98)
|
||||
menu_multi_item.bind_links(
|
||||
links=(
|
||||
link_document_multiple_check_in, link_document_multiple_checkout
|
||||
), sources=(Document,)
|
||||
)
|
||||
menu_secondary.bind_links(
|
||||
links=(link_document_checkout, link_document_check_in),
|
||||
sources=(
|
||||
'checkouts:checkout_info', 'checkouts:checkout_document',
|
||||
'checkouts:checkin_document'
|
||||
'checkouts:document_checkout_info', 'checkouts:document_checkout',
|
||||
'checkouts:document_check_in'
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from .permissions import permission_document_checkout_detail_view
|
||||
class DashboardWidgetTotalCheckouts(DashboardWidgetNumeric):
|
||||
icon_class = icon_dashboard_checkouts
|
||||
label = _('Checkedout documents')
|
||||
link = reverse_lazy('checkouts:checkout_list')
|
||||
link = reverse_lazy(viewname='checkouts:document_checkout_list')
|
||||
|
||||
def render(self, request):
|
||||
AccessControlList = apps.get_model(
|
||||
@@ -23,14 +23,16 @@ class DashboardWidgetTotalCheckouts(DashboardWidgetNumeric):
|
||||
DocumentCheckout = apps.get_model(
|
||||
app_label='checkouts', model_name='DocumentCheckout'
|
||||
)
|
||||
queryset = AccessControlList.objects.filter_by_access(
|
||||
queryset = AccessControlList.objects.restrict_queryset(
|
||||
permission=permission_document_checkout_detail_view,
|
||||
queryset=DocumentCheckout.objects.checked_out_documents(),
|
||||
user=request.user,
|
||||
queryset=DocumentCheckout.objects.checked_out_documents()
|
||||
)
|
||||
queryset = AccessControlList.objects.filter_by_access(
|
||||
permission=permission_document_view, user=request.user,
|
||||
queryset=queryset
|
||||
queryset = AccessControlList.objects.restrict_queryset(
|
||||
permission=permission_document_view, queryset=queryset,
|
||||
user=request.user
|
||||
)
|
||||
self.count = queryset.count()
|
||||
return super(DashboardWidgetTotalCheckouts, self).render(request)
|
||||
return super(DashboardWidgetTotalCheckouts, self).render(
|
||||
request=request
|
||||
)
|
||||
|
||||
@@ -4,19 +4,19 @@ from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from mayan.apps.events import EventTypeNamespace
|
||||
|
||||
namespace = EventTypeNamespace(name='checkouts', label=_('Checkouts'))
|
||||
namespace = EventTypeNamespace(label=_('Checkouts'), name='checkouts')
|
||||
|
||||
event_document_auto_check_in = namespace.add_event_type(
|
||||
name='document_auto_check_in',
|
||||
label=_('Document automatically checked in')
|
||||
label=_('Document automatically checked in'),
|
||||
name='document_auto_check_in'
|
||||
)
|
||||
event_document_check_in = namespace.add_event_type(
|
||||
name='document_check_in', label=_('Document checked in')
|
||||
label=_('Document checked in'), name='document_check_in'
|
||||
)
|
||||
event_document_check_out = namespace.add_event_type(
|
||||
name='document_check_out', label=_('Document checked out')
|
||||
label=_('Document checked out'), name='document_check_out'
|
||||
)
|
||||
event_document_forceful_check_in = namespace.add_event_type(
|
||||
name='document_forceful_check_in',
|
||||
label=_('Document forcefully checked in')
|
||||
label=_('Document forcefully checked in'),
|
||||
name='document_forceful_check_in'
|
||||
)
|
||||
|
||||
@@ -13,6 +13,6 @@ def handler_check_new_version_creation(sender, instance, **kwargs):
|
||||
app_label='checkouts', model_name='NewVersionBlock'
|
||||
)
|
||||
|
||||
if NewVersionBlock.objects.is_blocked(instance.document) and not instance.pk:
|
||||
if NewVersionBlock.objects.is_blocked(document=instance.document) and not instance.pk:
|
||||
# Block only new versions (no pk), not existing version being updated.
|
||||
raise NewDocumentVersionNotAllowed
|
||||
|
||||
13
mayan/apps/checkouts/hooks.py
Normal file
13
mayan/apps/checkouts/hooks.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.apps import apps
|
||||
|
||||
|
||||
def hook_is_new_version_allowed(document_version):
|
||||
NewVersionBlock = apps.get_model(
|
||||
app_label='checkouts', model_name='NewVersionBlock'
|
||||
)
|
||||
|
||||
NewVersionBlock.objects.new_versions_allowed(
|
||||
document=document_version.document
|
||||
)
|
||||
@@ -2,6 +2,14 @@ from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from mayan.apps.appearance.classes import Icon
|
||||
|
||||
icon_checkin_document = Icon(
|
||||
driver_name='fontawesome-dual', primary_symbol='shopping-cart',
|
||||
secondary_symbol='minus'
|
||||
)
|
||||
icon_checkout_document = Icon(
|
||||
driver_name='fontawesome-dual', primary_symbol='shopping-cart',
|
||||
secondary_symbol='plus'
|
||||
)
|
||||
icon_checkout_info = Icon(driver_name='fontawesome', symbol='shopping-cart')
|
||||
icon_dashboard_checkouts = Icon(
|
||||
driver_name='fontawesome', symbol='shopping-cart'
|
||||
|
||||
@@ -4,10 +4,12 @@ from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from mayan.apps.navigation import Link
|
||||
|
||||
from .icons import icon_checkout_info
|
||||
from .icons import (
|
||||
icon_checkin_document, icon_checkout_document, icon_checkout_info
|
||||
)
|
||||
from .permissions import (
|
||||
permission_document_checkin, permission_document_checkin_override,
|
||||
permission_document_checkout
|
||||
permission_document_check_in, permission_document_checkout,
|
||||
permission_document_checkout_detail_view
|
||||
)
|
||||
|
||||
|
||||
@@ -27,24 +29,32 @@ def is_not_checked_out(context):
|
||||
return True
|
||||
|
||||
|
||||
link_checkout_list = Link(
|
||||
link_document_checkout_list = Link(
|
||||
icon_class=icon_checkout_info, text=_('Checkouts'),
|
||||
view='checkouts:checkout_list'
|
||||
view='checkouts:document_checkout_list'
|
||||
)
|
||||
link_checkout_document = Link(
|
||||
args='object.pk', condition=is_not_checked_out,
|
||||
permissions=(permission_document_checkout,),
|
||||
text=_('Check out document'), view='checkouts:checkout_document',
|
||||
link_document_checkout = Link(
|
||||
condition=is_not_checked_out, icon_class=icon_checkout_document,
|
||||
kwargs={'document_id': 'object.pk'},
|
||||
permission=permission_document_checkout, text=_('Check out document'),
|
||||
view='checkouts:document_checkout',
|
||||
)
|
||||
link_checkin_document = Link(
|
||||
args='object.pk', condition=is_checked_out, permissions=(
|
||||
permission_document_checkin, permission_document_checkin_override
|
||||
), text=_('Check in document'), view='checkouts:checkin_document',
|
||||
|
||||
link_document_multiple_checkout = Link(
|
||||
icon_class=icon_checkout_document,
|
||||
permission=permission_document_checkout, text=_('Check out'),
|
||||
view='checkouts:document_multiple_checkout',
|
||||
)
|
||||
link_checkout_info = Link(
|
||||
args='resolved_object.pk', icon_class=icon_checkout_info, permissions=(
|
||||
permission_document_checkin, permission_document_checkin_override,
|
||||
permission_document_checkout
|
||||
), text=_('Check in/out'), view='checkouts:checkout_info',
|
||||
link_document_check_in = Link(
|
||||
condition=is_checked_out, icon_class=icon_checkin_document,
|
||||
kwargs={'document_id': 'object.pk'}, permission=permission_document_check_in,
|
||||
text=_('Check in document'), view='checkouts:document_check_in',
|
||||
)
|
||||
link_document_multiple_check_in = Link(
|
||||
icon_class=icon_checkin_document, permission=permission_document_check_in,
|
||||
text=_('Check in'), view='checkouts:document_multiple_check_in',
|
||||
)
|
||||
link_document_checkout_info = Link(
|
||||
icon_class=icon_checkout_info, kwargs={'document_id': 'resolved_object.pk'},
|
||||
permission=permission_document_checkout_detail_view,
|
||||
text=_('Check in/out'), view='checkouts:document_checkout_info',
|
||||
)
|
||||
|
||||
@@ -2,48 +2,54 @@ from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
from django.apps import apps
|
||||
from django.db import models
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import models, transaction
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from mayan.apps.acls.models import AccessControlList
|
||||
from mayan.apps.documents.models import Document
|
||||
|
||||
from .events import (
|
||||
event_document_auto_check_in, event_document_check_in,
|
||||
event_document_forceful_check_in
|
||||
)
|
||||
from .exceptions import DocumentNotCheckedOut
|
||||
from .exceptions import DocumentNotCheckedOut, NewDocumentVersionNotAllowed
|
||||
from .literals import STATE_CHECKED_IN, STATE_CHECKED_OUT
|
||||
from .permissions import permission_document_check_in_override
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DocumentCheckoutManager(models.Manager):
|
||||
def are_document_new_versions_allowed(self, document, user=None):
|
||||
try:
|
||||
checkout_info = self.document_checkout_info(document)
|
||||
except DocumentNotCheckedOut:
|
||||
return True
|
||||
else:
|
||||
return not checkout_info.block_new_version
|
||||
|
||||
def check_in_document(self, document, user=None):
|
||||
try:
|
||||
document_checkout = self.model.objects.get(document=document)
|
||||
except self.model.DoesNotExist:
|
||||
raise DocumentNotCheckedOut
|
||||
raise DocumentNotCheckedOut(
|
||||
_('Document not checked out.')
|
||||
)
|
||||
else:
|
||||
if user:
|
||||
if self.get_document_checkout_info(document).user != user:
|
||||
event_document_forceful_check_in.commit(
|
||||
actor=user, target=document
|
||||
)
|
||||
with transaction.atomic():
|
||||
if user:
|
||||
if self.get_document_checkout_info(document=document).user != user:
|
||||
try:
|
||||
AccessControlList.objects.check_access(
|
||||
obj=document, permission=permission_document_check_in_override,
|
||||
user=user
|
||||
)
|
||||
except PermissionDenied:
|
||||
return
|
||||
else:
|
||||
event_document_forceful_check_in.commit(
|
||||
actor=user, target=document
|
||||
)
|
||||
else:
|
||||
event_document_check_in.commit(actor=user, target=document)
|
||||
else:
|
||||
event_document_check_in.commit(actor=user, target=document)
|
||||
else:
|
||||
event_document_auto_check_in.commit(target=document)
|
||||
event_document_auto_check_in.commit(target=document)
|
||||
|
||||
document_checkout.delete()
|
||||
document_checkout.delete()
|
||||
|
||||
def check_in_expired_check_outs(self):
|
||||
for document in self.get_expired_check_outs():
|
||||
@@ -51,21 +57,16 @@ class DocumentCheckoutManager(models.Manager):
|
||||
|
||||
def checkout_document(self, document, expiration_datetime, user, block_new_version=True):
|
||||
return self.create(
|
||||
document=document, expiration_datetime=expiration_datetime,
|
||||
user=user, block_new_version=block_new_version
|
||||
block_new_version=block_new_version, document=document,
|
||||
expiration_datetime=expiration_datetime, user=user
|
||||
)
|
||||
|
||||
def checked_out_documents(self):
|
||||
return Document.objects.filter(
|
||||
pk__in=self.model.objects.all().values_list(
|
||||
'document__pk', flat=True
|
||||
)
|
||||
pk__in=self.model.objects.values('document__id')
|
||||
)
|
||||
|
||||
def get_by_natural_key(self, document_natural_key):
|
||||
Document = apps.get_model(
|
||||
app_label='documents', model_name='Document'
|
||||
)
|
||||
try:
|
||||
document = Document.objects.get_by_natural_key(document_natural_key)
|
||||
except Document.DoesNotExist:
|
||||
@@ -80,22 +81,22 @@ class DocumentCheckoutManager(models.Manager):
|
||||
raise DocumentNotCheckedOut
|
||||
|
||||
def get_document_checkout_state(self, document):
|
||||
if self.is_document_checked_out(document):
|
||||
if self.is_document_checked_out(document=document):
|
||||
return STATE_CHECKED_OUT
|
||||
else:
|
||||
return STATE_CHECKED_IN
|
||||
|
||||
def get_expired_check_outs(self):
|
||||
expired_list = Document.objects.filter(
|
||||
pk__in=self.model.objects.filter(
|
||||
pk__in=self.filter(
|
||||
expiration_datetime__lte=now()
|
||||
).values_list('document__pk', flat=True)
|
||||
).values('document__id')
|
||||
)
|
||||
logger.debug('expired_list: %s', expired_list)
|
||||
return expired_list
|
||||
|
||||
def is_document_checked_out(self, document):
|
||||
if self.model.objects.filter(document=document):
|
||||
if self.filter(document=document).exists():
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
@@ -105,13 +106,7 @@ class NewVersionBlockManager(models.Manager):
|
||||
def block(self, document):
|
||||
self.get_or_create(document=document)
|
||||
|
||||
def is_blocked(self, document):
|
||||
return self.filter(document=document).exists()
|
||||
|
||||
def get_by_natural_key(self, document_natural_key):
|
||||
Document = apps.get_model(
|
||||
app_label='documents', model_name='Document'
|
||||
)
|
||||
try:
|
||||
document = Document.objects.get_by_natural_key(document_natural_key)
|
||||
except Document.DoesNotExist:
|
||||
@@ -119,5 +114,12 @@ class NewVersionBlockManager(models.Manager):
|
||||
|
||||
return self.get(document__pk=document.pk)
|
||||
|
||||
def is_blocked(self, document):
|
||||
return self.filter(document=document).exists()
|
||||
|
||||
def new_versions_allowed(self, document):
|
||||
if self.filter(document=document).exists():
|
||||
raise NewDocumentVersionNotAllowed
|
||||
|
||||
def unblock(self, document):
|
||||
self.filter(document=document).delete()
|
||||
|
||||
@@ -4,7 +4,7 @@ import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db import models, transaction
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.timezone import now
|
||||
@@ -68,37 +68,42 @@ class DocumentCheckout(models.Model):
|
||||
)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
# TODO: enclose in transaction
|
||||
NewVersionBlock.objects.unblock(self.document)
|
||||
super(DocumentCheckout, self).delete(*args, **kwargs)
|
||||
with transaction.atomic():
|
||||
NewVersionBlock.objects.unblock(document=self.document)
|
||||
super(DocumentCheckout, self).delete(*args, **kwargs)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('checkout:checkout_info', args=(self.document.pk,))
|
||||
return reverse(
|
||||
viewname='checkout:checkout_info',
|
||||
kwargs={'document_id': self.document.pk}
|
||||
)
|
||||
|
||||
def natural_key(self):
|
||||
return self.document.natural_key()
|
||||
natural_key.dependencies = ['documents.Document']
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# TODO: enclose in transaction
|
||||
new_checkout = not self.pk
|
||||
if not new_checkout or self.document.is_checked_out():
|
||||
raise DocumentAlreadyCheckedOut
|
||||
|
||||
result = super(DocumentCheckout, self).save(*args, **kwargs)
|
||||
if new_checkout:
|
||||
event_document_check_out.commit(
|
||||
actor=self.user, target=self.document
|
||||
)
|
||||
if self.block_new_version:
|
||||
NewVersionBlock.objects.block(self.document)
|
||||
|
||||
logger.info(
|
||||
'Document "%s" checked out by user "%s"',
|
||||
self.document, self.user
|
||||
raise DocumentAlreadyCheckedOut(
|
||||
_('Document already checked out.')
|
||||
)
|
||||
|
||||
return result
|
||||
with transaction.atomic():
|
||||
result = super(DocumentCheckout, self).save(*args, **kwargs)
|
||||
if new_checkout:
|
||||
event_document_check_out.commit(
|
||||
actor=self.user, target=self.document
|
||||
)
|
||||
if self.block_new_version:
|
||||
NewVersionBlock.objects.block(self.document)
|
||||
|
||||
logger.info(
|
||||
'Document "%s" checked out by user "%s"',
|
||||
self.document, self.user
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class NewVersionBlock(models.Model):
|
||||
|
||||
@@ -6,15 +6,15 @@ from mayan.apps.permissions import PermissionNamespace
|
||||
|
||||
namespace = PermissionNamespace(label=_('Document checkout'), name='checkouts')
|
||||
|
||||
permission_document_checkin = namespace.add_permission(
|
||||
name='checkin_document', label=_('Check in documents')
|
||||
permission_document_check_in = namespace.add_permission(
|
||||
label=_('Check in documents'), name='checkin_document'
|
||||
)
|
||||
permission_document_checkin_override = namespace.add_permission(
|
||||
name='checkin_document_override', label=_('Forcefully check in documents')
|
||||
permission_document_check_in_override = namespace.add_permission(
|
||||
label=_('Forcefully check in documents'), name='checkin_document_override'
|
||||
)
|
||||
permission_document_checkout = namespace.add_permission(
|
||||
name='checkout_document', label=_('Check out documents')
|
||||
label=_('Check out documents'), name='checkout_document'
|
||||
)
|
||||
permission_document_checkout_detail_view = namespace.add_permission(
|
||||
name='checkout_detail_view', label=_('Check out details view')
|
||||
label=_('Check out details view'), name='checkout_detail_view'
|
||||
)
|
||||
|
||||
@@ -5,9 +5,9 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from mayan.apps.task_manager.classes import CeleryQueue
|
||||
|
||||
queue_checkouts_periodic = CeleryQueue(
|
||||
name='checkouts_periodic', label=_('Checkouts periodic'), transient=True
|
||||
label=_('Checkouts periodic'), name='checkouts_periodic', transient=True
|
||||
)
|
||||
queue_checkouts_periodic.add_task_type(
|
||||
name='mayan.apps.task_check_expired_check_outs',
|
||||
label=_('Check expired checkouts')
|
||||
label=_('Check expired checkouts'),
|
||||
name='mayan.apps.task_check_expired_check_outs'
|
||||
)
|
||||
|
||||
@@ -42,8 +42,8 @@ class NewDocumentCheckoutSerializer(serializers.ModelSerializer):
|
||||
document = Document.objects.get(pk=validated_data.pop('document_pk'))
|
||||
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=permission_document_checkout,
|
||||
user=self.context['request'].user, obj=document
|
||||
obj=document, permissions=permission_document_checkout,
|
||||
user=self.context['request'].user
|
||||
)
|
||||
|
||||
validated_data['document'] = document
|
||||
|
||||
@@ -25,7 +25,7 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
|
||||
def _request_checkedout_document_view(self):
|
||||
return self.get(
|
||||
viewname='rest_api:checkedout-document-view',
|
||||
args=(self.checkout.pk,)
|
||||
kwargs={'document_pk': self.checkout.pk}
|
||||
)
|
||||
|
||||
def _checkout_document(self):
|
||||
@@ -44,7 +44,8 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
|
||||
def test_checkedout_document_view_with_checkout_access(self):
|
||||
self._checkout_document()
|
||||
self.grant_access(
|
||||
permission=permission_document_checkout_detail_view, obj=self.document
|
||||
obj=self.document,
|
||||
permission=permission_document_checkout_detail_view
|
||||
)
|
||||
response = self._request_checkedout_document_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
@@ -52,7 +53,7 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
|
||||
def test_checkedout_document_view_with_document_access(self):
|
||||
self._checkout_document()
|
||||
self.grant_access(
|
||||
permission=permission_document_view, obj=self.document
|
||||
obj=self.document, permission=permission_document_view
|
||||
)
|
||||
response = self._request_checkedout_document_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
@@ -60,14 +61,17 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
|
||||
def test_checkedout_document_view_with_access(self):
|
||||
self._checkout_document()
|
||||
self.grant_access(
|
||||
permission=permission_document_view, obj=self.document
|
||||
obj=self.document, permission=permission_document_view
|
||||
)
|
||||
self.grant_access(
|
||||
permission=permission_document_checkout_detail_view, obj=self.document
|
||||
obj=self.document,
|
||||
permission=permission_document_checkout_detail_view
|
||||
)
|
||||
response = self._request_checkedout_document_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data['document']['uuid'], force_text(self.document.uuid))
|
||||
self.assertEqual(
|
||||
response.data['document']['uuid'], force_text(self.document.uuid)
|
||||
)
|
||||
|
||||
def _request_document_checkout_view(self):
|
||||
return self.post(
|
||||
@@ -83,7 +87,9 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
|
||||
self.assertEqual(DocumentCheckout.objects.count(), 0)
|
||||
|
||||
def test_document_checkout_with_access(self):
|
||||
self.grant_access(permission=permission_document_checkout, obj=self.document)
|
||||
self.grant_access(
|
||||
obj=self.document, permission=permission_document_checkout
|
||||
)
|
||||
response = self._request_document_checkout_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEqual(
|
||||
@@ -102,7 +108,7 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
|
||||
def test_checkout_list_view_with_document_access(self):
|
||||
self._checkout_document()
|
||||
self.grant_access(
|
||||
permission=permission_document_view, obj=self.document
|
||||
obj=self.document, permission=permission_document_view
|
||||
)
|
||||
response = self._request_checkout_list_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -111,7 +117,8 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
|
||||
def test_checkout_list_view_with_checkout_access(self):
|
||||
self._checkout_document()
|
||||
self.grant_access(
|
||||
permission=permission_document_checkout_detail_view, obj=self.document
|
||||
obj=self.document,
|
||||
permission=permission_document_checkout_detail_view
|
||||
)
|
||||
response = self._request_checkout_list_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -120,10 +127,11 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
|
||||
def test_checkout_list_view_with_access(self):
|
||||
self._checkout_document()
|
||||
self.grant_access(
|
||||
permission=permission_document_view, obj=self.document
|
||||
obj=self.document, permission=permission_document_view
|
||||
)
|
||||
self.grant_access(
|
||||
permission=permission_document_checkout_detail_view, obj=self.document
|
||||
obj=self.document,
|
||||
permission=permission_document_checkout_detail_view
|
||||
)
|
||||
response = self._request_checkout_list_view()
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
@@ -23,7 +23,7 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase):
|
||||
|
||||
DocumentCheckout.objects.checkout_document(
|
||||
document=self.document, expiration_datetime=expiration_datetime,
|
||||
user=self.admin_user, block_new_version=True
|
||||
user=self._test_case_user, block_new_version=True
|
||||
)
|
||||
|
||||
self.assertTrue(self.document.is_checked_out())
|
||||
@@ -33,29 +33,12 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase):
|
||||
)
|
||||
)
|
||||
|
||||
def test_version_creation_blocking(self):
|
||||
expiration_datetime = now() + datetime.timedelta(days=1)
|
||||
|
||||
# Silence unrelated logging
|
||||
logging.getLogger('mayan.apps.documents.models').setLevel(
|
||||
level=logging.CRITICAL
|
||||
)
|
||||
|
||||
DocumentCheckout.objects.checkout_document(
|
||||
document=self.document, expiration_datetime=expiration_datetime,
|
||||
user=self.admin_user, block_new_version=True
|
||||
)
|
||||
|
||||
with self.assertRaises(NewDocumentVersionNotAllowed):
|
||||
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
|
||||
self.document.new_version(file_object=file_object)
|
||||
|
||||
def test_checkin_in(self):
|
||||
expiration_datetime = now() + datetime.timedelta(days=1)
|
||||
|
||||
DocumentCheckout.objects.checkout_document(
|
||||
document=self.document, expiration_datetime=expiration_datetime,
|
||||
user=self.admin_user, block_new_version=True
|
||||
user=self._test_case_user, block_new_version=True
|
||||
)
|
||||
|
||||
self.document.check_in()
|
||||
@@ -72,13 +55,13 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase):
|
||||
|
||||
DocumentCheckout.objects.checkout_document(
|
||||
document=self.document, expiration_datetime=expiration_datetime,
|
||||
user=self.admin_user, block_new_version=True
|
||||
user=self._test_case_user, block_new_version=True
|
||||
)
|
||||
|
||||
with self.assertRaises(DocumentAlreadyCheckedOut):
|
||||
DocumentCheckout.objects.checkout_document(
|
||||
document=self.document,
|
||||
expiration_datetime=expiration_datetime, user=self.admin_user,
|
||||
expiration_datetime=expiration_datetime, user=self._test_case_user,
|
||||
block_new_version=True
|
||||
)
|
||||
|
||||
@@ -91,7 +74,7 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase):
|
||||
|
||||
DocumentCheckout.objects.checkout_document(
|
||||
document=self.document, expiration_datetime=expiration_datetime,
|
||||
user=self.admin_user, block_new_version=True
|
||||
user=self._test_case_user, block_new_version=True
|
||||
)
|
||||
|
||||
time.sleep(.11)
|
||||
@@ -100,18 +83,6 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase):
|
||||
|
||||
self.assertFalse(self.document.is_checked_out())
|
||||
|
||||
def test_blocking_new_versions(self):
|
||||
# Silence unrelated logging
|
||||
logging.getLogger('mayan.apps.documents.models').setLevel(
|
||||
level=logging.CRITICAL
|
||||
)
|
||||
|
||||
NewVersionBlock.objects.block(document=self.document)
|
||||
|
||||
with self.assertRaises(NewDocumentVersionNotAllowed):
|
||||
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
|
||||
self.document.new_version(file_object=file_object)
|
||||
|
||||
|
||||
class NewVersionBlockTestCase(DocumentTestMixin, BaseTestCase):
|
||||
def test_blocking(self):
|
||||
@@ -141,3 +112,32 @@ class NewVersionBlockTestCase(DocumentTestMixin, BaseTestCase):
|
||||
self.assertFalse(
|
||||
NewVersionBlock.objects.is_blocked(document=self.document)
|
||||
)
|
||||
|
||||
def test_blocking_new_versions(self):
|
||||
# Silence unrelated logging
|
||||
logging.getLogger('mayan.apps.documents.models').setLevel(
|
||||
level=logging.CRITICAL
|
||||
)
|
||||
|
||||
NewVersionBlock.objects.block(document=self.document)
|
||||
|
||||
with self.assertRaises(NewDocumentVersionNotAllowed):
|
||||
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
|
||||
self.document.new_version(file_object=file_object)
|
||||
|
||||
def test_version_creation_blocking(self):
|
||||
expiration_datetime = now() + datetime.timedelta(days=1)
|
||||
|
||||
# Silence unrelated logging
|
||||
logging.getLogger('mayan.apps.documents.models').setLevel(
|
||||
level=logging.CRITICAL
|
||||
)
|
||||
|
||||
DocumentCheckout.objects.checkout_document(
|
||||
document=self.document, expiration_datetime=expiration_datetime,
|
||||
user=self._test_case_user, block_new_version=True
|
||||
)
|
||||
|
||||
with self.assertRaises(NewDocumentVersionNotAllowed):
|
||||
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
|
||||
self.document.new_version(file_object=file_object)
|
||||
|
||||
@@ -8,61 +8,53 @@ from django.utils.timezone import now
|
||||
from mayan.apps.common.literals import TIME_DELTA_UNIT_DAYS
|
||||
from mayan.apps.documents.tests import GenericDocumentViewTestCase
|
||||
from mayan.apps.sources.links import link_upload_version
|
||||
from mayan.apps.user_management.tests import (
|
||||
TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, TEST_USER_PASSWORD,
|
||||
TEST_USER_USERNAME
|
||||
)
|
||||
|
||||
from ..models import DocumentCheckout
|
||||
from ..permissions import (
|
||||
permission_document_checkin, permission_document_checkin_override,
|
||||
permission_document_check_in, permission_document_check_in_override,
|
||||
permission_document_checkout, permission_document_checkout_detail_view
|
||||
)
|
||||
|
||||
|
||||
class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
|
||||
create_test_case_superuser = True
|
||||
|
||||
def _checkout_document(self):
|
||||
expiration_datetime = now() + datetime.timedelta(days=1)
|
||||
|
||||
DocumentCheckout.objects.checkout_document(
|
||||
document=self.document, expiration_datetime=expiration_datetime,
|
||||
user=self._test_case_user, block_new_version=True
|
||||
)
|
||||
self.assertTrue(self.document.is_checked_out())
|
||||
|
||||
def _request_document_check_in_view(self):
|
||||
return self.post(
|
||||
viewname='checkouts:checkin_document', args=(self.document.pk,),
|
||||
viewname='checkouts:document_check_in',
|
||||
kwargs={'document_id': self.document.pk}
|
||||
)
|
||||
|
||||
def test_checkin_document_view_no_permission(self):
|
||||
self.login_user()
|
||||
|
||||
expiration_datetime = now() + datetime.timedelta(days=1)
|
||||
|
||||
DocumentCheckout.objects.checkout_document(
|
||||
document=self.document, expiration_datetime=expiration_datetime,
|
||||
user=self.user, block_new_version=True
|
||||
)
|
||||
|
||||
self.assertTrue(self.document.is_checked_out())
|
||||
def test_document_check_in_view_no_permission(self):
|
||||
self._checkout_document()
|
||||
|
||||
response = self._request_document_check_in_view()
|
||||
self.assertEquals(response.status_code, 403)
|
||||
self.assertEquals(response.status_code, 404)
|
||||
|
||||
self.assertTrue(self.document.is_checked_out())
|
||||
|
||||
def test_checkin_document_view_with_access(self):
|
||||
self.login_user()
|
||||
|
||||
expiration_datetime = now() + datetime.timedelta(days=1)
|
||||
|
||||
DocumentCheckout.objects.checkout_document(
|
||||
document=self.document, expiration_datetime=expiration_datetime,
|
||||
user=self.user, block_new_version=True
|
||||
)
|
||||
self.assertTrue(self.document.is_checked_out())
|
||||
def test_document_check_in_view_with_access(self):
|
||||
self._checkout_document()
|
||||
|
||||
self.grant_access(
|
||||
obj=self.document, permission=permission_document_checkin
|
||||
obj=self.document, permission=permission_document_check_in
|
||||
)
|
||||
self.grant_access(
|
||||
obj=self.document,
|
||||
permission=permission_document_checkout_detail_view
|
||||
)
|
||||
|
||||
response = self._request_document_check_in_view()
|
||||
self.assertEquals(response.status_code, 302)
|
||||
|
||||
self.assertFalse(self.document.is_checked_out())
|
||||
self.assertFalse(
|
||||
DocumentCheckout.objects.is_document_checked_out(
|
||||
@@ -72,7 +64,8 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
|
||||
|
||||
def _request_document_checkout_view(self):
|
||||
return self.post(
|
||||
viewname='checkouts:checkout_document', args=(self.document.pk,),
|
||||
viewname='checkouts:document_checkout',
|
||||
kwargs={'document_id': self.document.pk},
|
||||
data={
|
||||
'expiration_datetime_0': 2,
|
||||
'expiration_datetime_1': TIME_DELTA_UNIT_DAYS,
|
||||
@@ -81,14 +74,11 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
|
||||
)
|
||||
|
||||
def test_checkout_document_view_no_permission(self):
|
||||
self.login_user()
|
||||
|
||||
response = self._request_document_checkout_view()
|
||||
self.assertEquals(response.status_code, 403)
|
||||
self.assertEquals(response.status_code, 404)
|
||||
self.assertFalse(self.document.is_checked_out())
|
||||
|
||||
def test_checkout_document_view_with_access(self):
|
||||
self.login_user()
|
||||
self.grant_access(
|
||||
obj=self.document, permission=permission_document_checkout
|
||||
)
|
||||
@@ -96,9 +86,9 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
|
||||
obj=self.document,
|
||||
permission=permission_document_checkout_detail_view
|
||||
)
|
||||
|
||||
response = self._request_document_checkout_view()
|
||||
self.assertEquals(response.status_code, 302)
|
||||
|
||||
self.assertTrue(self.document.is_checked_out())
|
||||
|
||||
def test_document_new_version_after_checkout(self):
|
||||
@@ -111,31 +101,23 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
|
||||
- Link to upload version view should not resolve
|
||||
- Upload version view should reject request
|
||||
"""
|
||||
self.login(
|
||||
username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD
|
||||
)
|
||||
self.login_superuser()
|
||||
|
||||
expiration_datetime = now() + datetime.timedelta(days=1)
|
||||
|
||||
DocumentCheckout.objects.checkout_document(
|
||||
document=self.document, expiration_datetime=expiration_datetime,
|
||||
user=self.admin_user, block_new_version=True
|
||||
)
|
||||
|
||||
self.assertTrue(self.document.is_checked_out())
|
||||
self._checkout_document()
|
||||
|
||||
response = self.post(
|
||||
'sources:upload_version', args=(self.document.pk,),
|
||||
viewname='sources:upload_version',
|
||||
kwargs={'document_id': self.document.pk},
|
||||
follow=True
|
||||
)
|
||||
|
||||
self.assertContains(
|
||||
response, text='blocked from uploading',
|
||||
status_code=200
|
||||
)
|
||||
|
||||
response = self.get(
|
||||
'documents:document_version_list', args=(self.document.pk,),
|
||||
viewname='documents:document_version_list',
|
||||
kwargs={'document_id': self.document.pk},
|
||||
follow=True
|
||||
)
|
||||
|
||||
@@ -159,26 +141,22 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
|
||||
|
||||
DocumentCheckout.objects.checkout_document(
|
||||
document=self.document, expiration_datetime=expiration_datetime,
|
||||
user=self.admin_user, block_new_version=True
|
||||
user=self._test_case_superuser, block_new_version=True
|
||||
)
|
||||
|
||||
self.assertTrue(self.document.is_checked_out())
|
||||
|
||||
self.login(
|
||||
username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD
|
||||
self.grant_access(
|
||||
obj=self.document, permission=permission_document_check_in
|
||||
)
|
||||
|
||||
self.role.permissions.add(
|
||||
permission_document_checkin.stored_permission
|
||||
self.grant_access(
|
||||
obj=self.document, permission=permission_document_checkout
|
||||
)
|
||||
self.role.permissions.add(
|
||||
permission_document_checkout.stored_permission
|
||||
)
|
||||
|
||||
response = self.post(
|
||||
'checkouts:checkin_document', args=(self.document.pk,), follow=True
|
||||
viewname='checkouts:document_check_in',
|
||||
kwargs={'document_id': self.document.pk},
|
||||
follow=True
|
||||
)
|
||||
|
||||
self.assertContains(
|
||||
response, text='Insufficient permissions', status_code=403
|
||||
)
|
||||
@@ -186,33 +164,21 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
|
||||
self.assertTrue(self.document.is_checked_out())
|
||||
|
||||
def test_forcefull_check_in_document_view_with_permission(self):
|
||||
expiration_datetime = now() + datetime.timedelta(days=1)
|
||||
self._checkout_document()
|
||||
|
||||
DocumentCheckout.objects.checkout_document(
|
||||
document=self.document, expiration_datetime=expiration_datetime,
|
||||
user=self.admin_user, block_new_version=True
|
||||
self.grant_access(
|
||||
obj=self.document, permission=permission_document_check_in
|
||||
)
|
||||
|
||||
self.assertTrue(self.document.is_checked_out())
|
||||
|
||||
self.login(
|
||||
username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD
|
||||
self.grant_access(
|
||||
obj=self.document, permission=permission_document_check_in_override
|
||||
)
|
||||
|
||||
self.role.permissions.add(
|
||||
permission_document_checkin.stored_permission
|
||||
)
|
||||
self.role.permissions.add(
|
||||
permission_document_checkin.stored_permission
|
||||
)
|
||||
self.role.permissions.add(
|
||||
permission_document_checkin_override.stored_permission
|
||||
)
|
||||
self.role.permissions.add(
|
||||
permission_document_checkout_detail_view.stored_permission
|
||||
self.grant_access(
|
||||
obj=self.document, permission=permission_document_checkout_detail_view
|
||||
)
|
||||
response = self.post(
|
||||
'checkouts:checkin_document', args=(self.document.pk,), follow=True
|
||||
viewname='checkouts:document_check_in',
|
||||
kwargs={'document_id': self.document.pk},
|
||||
follow=True
|
||||
)
|
||||
|
||||
self.assertContains(
|
||||
|
||||
@@ -4,33 +4,45 @@ from django.conf.urls import url
|
||||
|
||||
from .api_views import APICheckedoutDocumentListView, APICheckedoutDocumentView
|
||||
from .views import (
|
||||
CheckoutDetailView, CheckoutDocumentView, CheckoutListView,
|
||||
DocumentCheckinView
|
||||
DocumentCheckinView, DocumentCheckoutView, DocumentCheckoutDetailView,
|
||||
DocumentCheckoutListView
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^list/$', CheckoutListView.as_view(), name='checkout_list'),
|
||||
url(
|
||||
r'^(?P<pk>\d+)/check/out/$', CheckoutDocumentView.as_view(),
|
||||
name='checkout_document'
|
||||
regex=r'^documents/$', name='document_checkout_list',
|
||||
view=DocumentCheckoutListView.as_view()
|
||||
),
|
||||
url(
|
||||
r'^(?P<pk>\d+)/check/in/$', DocumentCheckinView.as_view(),
|
||||
name='checkin_document'
|
||||
regex=r'^documents/(?P<document_id>\d+)/check_in/$',
|
||||
name='document_check_in', view=DocumentCheckinView.as_view()
|
||||
),
|
||||
url(
|
||||
r'^(?P<pk>\d+)/check/info/$', CheckoutDetailView.as_view(),
|
||||
name='checkout_info'
|
||||
regex=r'^documents/multiple/check_in/$',
|
||||
name='document_multiple_check_in', view=DocumentCheckinView.as_view()
|
||||
),
|
||||
url(
|
||||
regex=r'^documents/(?P<document_id>\d+)/checkout/$',
|
||||
name='document_checkout', view=DocumentCheckoutView.as_view()
|
||||
),
|
||||
url(
|
||||
regex=r'^documents/multiple/checkout/$',
|
||||
name='document_multiple_checkout', view=DocumentCheckoutView.as_view()
|
||||
),
|
||||
url(
|
||||
regex=r'^documents/(?P<document_id>\d+)/checkout/info/$',
|
||||
name='document_checkout_info', view=DocumentCheckoutDetailView.as_view()
|
||||
),
|
||||
]
|
||||
|
||||
api_urls = [
|
||||
url(
|
||||
r'^checkouts/$', APICheckedoutDocumentListView.as_view(),
|
||||
name='checkout-document-list'
|
||||
regex=r'^checkouts/$', name='checkout-document-list',
|
||||
view=APICheckedoutDocumentListView.as_view()
|
||||
),
|
||||
url(
|
||||
r'^checkouts/(?P<pk>[0-9]+)/checkout_info/$', APICheckedoutDocumentView.as_view(),
|
||||
name='checkedout-document-view'
|
||||
regex=r'^checkouts/(?P<document_id>\d+)/checkout_info/$',
|
||||
name='checkedout-document-view',
|
||||
view=APICheckedoutDocumentView.as_view()
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,85 +1,154 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ugettext_lazy as _, ungettext
|
||||
|
||||
from mayan.apps.acls.models import AccessControlList
|
||||
from mayan.apps.common.generics import (
|
||||
ConfirmView, SingleObjectCreateView, SingleObjectDetailView
|
||||
MultipleObjectConfirmActionView, MultipleObjectFormActionView,
|
||||
SingleObjectDetailView
|
||||
)
|
||||
from mayan.apps.common.utils import encapsulate
|
||||
from mayan.apps.documents.models import Document
|
||||
from mayan.apps.documents.views import DocumentListView
|
||||
|
||||
from .exceptions import DocumentAlreadyCheckedOut, DocumentNotCheckedOut
|
||||
from .forms import DocumentCheckoutDefailForm, DocumentCheckoutForm
|
||||
from .icons import icon_checkout_info
|
||||
from .models import DocumentCheckout
|
||||
from .permissions import (
|
||||
permission_document_checkin, permission_document_checkin_override,
|
||||
permission_document_checkout, permission_document_checkout_detail_view
|
||||
permission_document_check_in, permission_document_checkout,
|
||||
permission_document_checkout_detail_view
|
||||
)
|
||||
|
||||
|
||||
class CheckoutDocumentView(SingleObjectCreateView):
|
||||
form_class = DocumentCheckoutForm
|
||||
class DocumentCheckinView(MultipleObjectConfirmActionView):
|
||||
error_message = 'Unable to check in document "%(instance)s". %(exception)s'
|
||||
model = Document
|
||||
object_permission = permission_document_check_in
|
||||
pk_url_kwarg = 'document_id'
|
||||
success_message_singular = '%(count)d document checked in.'
|
||||
success_message_plural = '%(count)d documents checked in.'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.document = get_object_or_404(klass=Document, pk=self.kwargs['pk'])
|
||||
def get_extra_context(self):
|
||||
queryset = self.get_object_list()
|
||||
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=permission_document_checkout, user=request.user,
|
||||
obj=self.document
|
||||
)
|
||||
result = {
|
||||
'title': ungettext(
|
||||
singular='Check in %(count)d document',
|
||||
plural='Check in %(count)d documents',
|
||||
number=queryset.count()
|
||||
) % {
|
||||
'count': queryset.count(),
|
||||
}
|
||||
}
|
||||
|
||||
return super(
|
||||
CheckoutDocumentView, self
|
||||
).dispatch(request, *args, **kwargs)
|
||||
if queryset.count() == 1:
|
||||
result.update(
|
||||
{
|
||||
'object': queryset.first(),
|
||||
'title': _(
|
||||
'Check in document: %s'
|
||||
) % queryset.first()
|
||||
}
|
||||
)
|
||||
|
||||
def form_valid(self, form):
|
||||
try:
|
||||
instance = form.save(commit=False)
|
||||
instance.user = self.request.user
|
||||
instance.document = self.document
|
||||
instance.save()
|
||||
except DocumentAlreadyCheckedOut:
|
||||
messages.error(self.request, _('Document already checked out.'))
|
||||
except Exception as exception:
|
||||
messages.error(
|
||||
self.request,
|
||||
_('Error trying to check out document; %s') % exception
|
||||
return result
|
||||
|
||||
def get_post_object_action_url(self):
|
||||
if self.action_count == 1:
|
||||
return reverse(
|
||||
viewname='checkouts:document_checkout_info',
|
||||
kwargs={'document_id': self.action_id_list[0]}
|
||||
)
|
||||
else:
|
||||
messages.success(
|
||||
self.request,
|
||||
_('Document "%s" checked out successfully.') % self.document
|
||||
super(DocumentCheckinView, self).get_post_action_redirect()
|
||||
|
||||
def object_action(self, form, instance):
|
||||
DocumentCheckout.objects.check_in_document(
|
||||
document=instance, user=self.request.user
|
||||
)
|
||||
|
||||
|
||||
class DocumentCheckoutView(MultipleObjectFormActionView):
|
||||
error_message = 'Unable to checkout document "%(instance)s". %(exception)s'
|
||||
form_class = DocumentCheckoutForm
|
||||
model = Document
|
||||
object_permission = permission_document_checkout
|
||||
pk_url_kwarg = 'document_id'
|
||||
success_message_singular = '%(count)d document checked out.'
|
||||
success_message_plural = '%(count)d documents checked out.'
|
||||
|
||||
def get_extra_context(self):
|
||||
queryset = self.get_object_list()
|
||||
|
||||
result = {
|
||||
'title': ungettext(
|
||||
singular='Checkout %(count)d document',
|
||||
plural='Checkout %(count)d documents',
|
||||
number=queryset.count()
|
||||
) % {
|
||||
'count': queryset.count(),
|
||||
}
|
||||
}
|
||||
|
||||
if queryset.count() == 1:
|
||||
result.update(
|
||||
{
|
||||
'object': queryset.first(),
|
||||
'title': _(
|
||||
'Check out document: %s'
|
||||
) % queryset.first()
|
||||
}
|
||||
)
|
||||
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
return result
|
||||
|
||||
def get_post_object_action_url(self):
|
||||
if self.action_count == 1:
|
||||
return reverse(
|
||||
viewname='checkouts:document_checkout_info',
|
||||
kwargs={'document_id': self.action_id_list[0]}
|
||||
)
|
||||
else:
|
||||
super(DocumentCheckoutView, self).get_post_action_redirect()
|
||||
|
||||
def object_action(self, form, instance):
|
||||
DocumentCheckout.objects.checkout_document(
|
||||
block_new_version=form.cleaned_data['block_new_version'],
|
||||
document=instance,
|
||||
expiration_datetime=form.cleaned_data['expiration_datetime'],
|
||||
user=self.request.user,
|
||||
)
|
||||
|
||||
|
||||
class DocumentCheckoutDetailView(SingleObjectDetailView):
|
||||
form_class = DocumentCheckoutDefailForm
|
||||
model = Document
|
||||
object_permission = permission_document_checkout_detail_view
|
||||
|
||||
def get_extra_context(self):
|
||||
return {
|
||||
'object': self.document,
|
||||
'title': _('Check out document: %s') % self.document
|
||||
'object': self.get_object(),
|
||||
'title': _(
|
||||
'Check out details for document: %s'
|
||||
) % self.get_object()
|
||||
}
|
||||
|
||||
def get_post_action_redirect(self):
|
||||
return reverse('checkouts:checkout_info', args=(self.document.pk,))
|
||||
def get_object(self):
|
||||
return get_object_or_404(klass=Document, pk=self.kwargs['document_id'])
|
||||
|
||||
|
||||
class CheckoutListView(DocumentListView):
|
||||
class DocumentCheckoutListView(DocumentListView):
|
||||
def get_document_queryset(self):
|
||||
return AccessControlList.objects.filter_by_access(
|
||||
return AccessControlList.objects.restrict_queryset(
|
||||
permission=permission_document_checkout_detail_view,
|
||||
user=self.request.user,
|
||||
queryset=DocumentCheckout.objects.checked_out_documents()
|
||||
queryset=DocumentCheckout.objects.checked_out_documents(),
|
||||
user=self.request.user
|
||||
)
|
||||
|
||||
def get_extra_context(self):
|
||||
context = super(CheckoutListView, self).get_extra_context()
|
||||
context = super(DocumentCheckoutListView, self).get_extra_context()
|
||||
context.update(
|
||||
{
|
||||
'extra_columns': (
|
||||
@@ -113,76 +182,3 @@ class CheckoutListView(DocumentListView):
|
||||
}
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
class CheckoutDetailView(SingleObjectDetailView):
|
||||
form_class = DocumentCheckoutDefailForm
|
||||
model = Document
|
||||
object_permission = permission_document_checkout_detail_view
|
||||
|
||||
def get_extra_context(self):
|
||||
return {
|
||||
'object': self.get_object(),
|
||||
'title': _(
|
||||
'Check out details for document: %s'
|
||||
) % self.get_object()
|
||||
}
|
||||
|
||||
def get_object(self):
|
||||
return get_object_or_404(klass=Document, pk=self.kwargs['pk'])
|
||||
|
||||
|
||||
class DocumentCheckinView(ConfirmView):
|
||||
def get_extra_context(self):
|
||||
document = self.get_object()
|
||||
|
||||
context = {
|
||||
'object': document,
|
||||
}
|
||||
|
||||
if document.get_checkout_info().user != self.request.user:
|
||||
context['title'] = _(
|
||||
'You didn\'t originally checked out this document. '
|
||||
'Forcefully check in the document: %s?'
|
||||
) % document
|
||||
else:
|
||||
context['title'] = _('Check in the document: %s?') % document
|
||||
|
||||
return context
|
||||
|
||||
def get_object(self):
|
||||
return get_object_or_404(klass=Document, pk=self.kwargs['pk'])
|
||||
|
||||
def get_post_action_redirect(self):
|
||||
return reverse('checkouts:checkout_info', args=(self.get_object().pk,))
|
||||
|
||||
def view_action(self):
|
||||
document = self.get_object()
|
||||
|
||||
if document.get_checkout_info().user == self.request.user:
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=permission_document_checkin,
|
||||
user=self.request.user, obj=document
|
||||
)
|
||||
else:
|
||||
AccessControlList.objects.check_access(
|
||||
permissions=permission_document_checkin_override,
|
||||
user=self.request.user, obj=document
|
||||
)
|
||||
|
||||
try:
|
||||
document.check_in(user=self.request.user)
|
||||
except DocumentNotCheckedOut:
|
||||
messages.error(
|
||||
self.request, _('Document has not been checked out.')
|
||||
)
|
||||
except Exception as exception:
|
||||
messages.error(
|
||||
self.request,
|
||||
_('Error trying to check in document; %s') % exception
|
||||
)
|
||||
else:
|
||||
messages.success(
|
||||
self.request,
|
||||
_('Document "%s" checked in successfully.') % document
|
||||
)
|
||||
|
||||
@@ -2,42 +2,42 @@ from __future__ import unicode_literals
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
||||
from rest_framework import generics
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from .classes import Template
|
||||
from .serializers import ContentTypeSerializer, TemplateSerializer
|
||||
|
||||
|
||||
class APIContentTypeList(generics.ListAPIView):
|
||||
class ContentTypeAPIViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
Returns a list of all the available content types.
|
||||
list:
|
||||
Return a list of all the available content types.
|
||||
|
||||
retrieve:
|
||||
Return the given content type details.
|
||||
"""
|
||||
serializer_class = ContentTypeSerializer
|
||||
lookup_url_kwarg = 'content_type_id'
|
||||
queryset = ContentType.objects.order_by('app_label', 'model')
|
||||
serializer_class = ContentTypeSerializer
|
||||
|
||||
|
||||
class APITemplateListView(generics.ListAPIView):
|
||||
class TemplateAPIViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
"""
|
||||
Returns a list of partial templates.
|
||||
get: Returns a list of partial templates.
|
||||
list:
|
||||
Return a list of partial templates.
|
||||
|
||||
retrieve:
|
||||
Return the given partial template details.
|
||||
"""
|
||||
serializer_class = TemplateSerializer
|
||||
lookup_url_kwarg = 'template_name'
|
||||
permission_classes = (IsAuthenticated,)
|
||||
serializer_class = TemplateSerializer
|
||||
|
||||
def get_object(self):
|
||||
return Template.get(name=self.kwargs['template_name']).render(
|
||||
request=self.request
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
return Template.all(rendered=True, request=self.request)
|
||||
|
||||
|
||||
class APITemplateView(generics.RetrieveAPIView):
|
||||
"""
|
||||
Returns the selected partial template details.
|
||||
get: Retrieve the details of the partial template.
|
||||
"""
|
||||
serializer_class = TemplateSerializer
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
def get_object(self):
|
||||
return Template.get(name=self.kwargs['name']).render(
|
||||
request=self.request
|
||||
)
|
||||
|
||||
@@ -4,6 +4,8 @@ import logging
|
||||
import os
|
||||
import warnings
|
||||
from datetime import timedelta
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from kombu import Exchange, Queue
|
||||
|
||||
@@ -41,6 +43,7 @@ from .settings import (
|
||||
from .signals import pre_initial_setup, pre_upgrade
|
||||
from .tasks import task_delete_stale_uploads # NOQA - Force task registration
|
||||
from .utils import check_for_sqlite
|
||||
from .warnings import DatabaseWarning
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -74,6 +77,8 @@ class MayanAppConfig(apps.AppConfig):
|
||||
'Import time error when running AppConfig.ready() of app '
|
||||
'"%s".', self.name
|
||||
)
|
||||
exc_info = sys.exc_info()
|
||||
traceback.print_exception(*exc_info)
|
||||
raise exception
|
||||
|
||||
|
||||
@@ -88,7 +93,9 @@ class CommonApp(MayanAppConfig):
|
||||
def ready(self):
|
||||
super(CommonApp, self).ready()
|
||||
if check_for_sqlite():
|
||||
warnings.warn(force_text(MESSAGE_SQLITE_WARNING))
|
||||
warnings.warn(
|
||||
category=DatabaseWarning, message=force_text(MESSAGE_SQLITE_WARNING)
|
||||
)
|
||||
|
||||
Template(
|
||||
name='menu_main', template_name='appearance/menu_main.html'
|
||||
|
||||
@@ -72,16 +72,6 @@ class ErrorLogNamespace(object):
|
||||
return ErrorLogEntry.objects.filter(namespace=self.name)
|
||||
|
||||
|
||||
class FakeStorageSubclass(object):
|
||||
"""
|
||||
Placeholder class to allow serializing the real storage subclass to
|
||||
support migrations.
|
||||
"""
|
||||
|
||||
def __eq__(self, other):
|
||||
return True
|
||||
|
||||
|
||||
class MissingItem(object):
|
||||
_registry = []
|
||||
|
||||
@@ -302,7 +292,7 @@ class Template(object):
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
viewname='rest_api:template-detail', kwargs={'template_pk': self.name}
|
||||
viewname='rest_api:template-detail', kwargs={'template_name': self.name}
|
||||
)
|
||||
|
||||
def render(self, request):
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,12 @@ from __future__ import absolute_import, unicode_literals
|
||||
from mayan.apps.appearance.classes import Icon
|
||||
|
||||
icon_about = Icon(driver_name='fontawesome', symbol='info')
|
||||
icon_add_all = Icon(
|
||||
driver_name='fontawesome-layers', data=[
|
||||
{'class': 'far fa-circle'},
|
||||
{'class': 'fas fa-plus', 'transform': 'shrink-6'}
|
||||
]
|
||||
)
|
||||
icon_assign_remove_add = Icon(driver_name='fontawesome', symbol='plus')
|
||||
icon_assign_remove_remove = Icon(driver_name='fontawesome', symbol='minus')
|
||||
icon_check_version = Icon(driver_name='fontawesome', symbol='sync')
|
||||
@@ -43,6 +49,12 @@ icon_ok = Icon(
|
||||
icon_packages_licenses = Icon(
|
||||
driver_name='fontawesome', symbol='certificate'
|
||||
)
|
||||
icon_remove_all = Icon(
|
||||
driver_name='fontawesome-layers', data=[
|
||||
{'class': 'far fa-circle'},
|
||||
{'class': 'fas fa-minus', 'transform': 'shrink-6'}
|
||||
]
|
||||
)
|
||||
icon_setup = Icon(
|
||||
driver_name='fontawesome', symbol='cog'
|
||||
)
|
||||
|
||||
@@ -57,12 +57,12 @@ link_documentation = Link(
|
||||
link_object_error_list = Link(
|
||||
icon_class=icon_object_error_list,
|
||||
kwargs=get_kwargs_factory('resolved_object'),
|
||||
permissions=(permission_error_log_view,), text=_('Errors'),
|
||||
permission=permission_error_log_view, text=_('Errors'),
|
||||
view='common:object_error_list',
|
||||
)
|
||||
link_object_error_list_clear = Link(
|
||||
kwargs=get_kwargs_factory('resolved_object'),
|
||||
permissions=(permission_error_log_view,), text=_('Clear all'),
|
||||
permission=permission_error_log_view, text=_('Clear all'),
|
||||
view='common:object_error_list_clear',
|
||||
)
|
||||
link_forum = Link(
|
||||
|
||||
@@ -11,7 +11,7 @@ MESSAGE_SQLITE_WARNING = _(
|
||||
'for development and testing, not for production.'
|
||||
)
|
||||
PYPI_URL = 'https://pypi.python.org/pypi'
|
||||
|
||||
PK_LIST_SEPARATOR = ','
|
||||
TEXT_LIST_AS_ITEMS_PARAMETER = '_list_mode'
|
||||
TEXT_LIST_AS_ITEMS_VARIABLE_NAME = 'list_as_items'
|
||||
TEXT_CHOICE_ITEMS = 'items'
|
||||
|
||||
@@ -11,8 +11,8 @@ from django.core.management.base import CommandError
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from mayan.apps.common.utils import fs_cleanup
|
||||
from mayan.apps.documents.models import DocumentType
|
||||
from mayan.apps.storage.utils import fs_cleanup
|
||||
|
||||
CONVERTDB_FOLDER = 'convertdb'
|
||||
CONVERTDB_OUTPUT_FILENAME = 'migrate.json'
|
||||
|
||||
@@ -8,21 +8,20 @@ from .icons import icon_menu_about, icon_menu_user
|
||||
|
||||
__all__ = (
|
||||
'menu_about', 'menu_facet', 'menu_list_facet', 'menu_main', 'menu_object',
|
||||
'menu_multi_item', 'menu_secondary', 'menu_setup', 'menu_sidebar',
|
||||
'menu_multi_item', 'menu_secondary', 'menu_setup', 'menu_secondary',
|
||||
'menu_tools', 'menu_topbar', 'menu_user'
|
||||
)
|
||||
|
||||
menu_about = Menu(
|
||||
icon_class=icon_menu_about, label=_('System'), name='about'
|
||||
)
|
||||
menu_facet = Menu(name='facet')
|
||||
menu_list_facet = Menu(name='list facet')
|
||||
menu_facet = Menu(label=_('Facet'), name='facet')
|
||||
menu_list_facet = Menu(label=_('Facet'), name='list facet')
|
||||
menu_main = Menu(name='main')
|
||||
menu_multi_item = Menu(name='multi item')
|
||||
menu_object = Menu(name='object')
|
||||
menu_secondary = Menu(name='secondary')
|
||||
menu_object = Menu(label=_('Actions'), name='object')
|
||||
menu_secondary = Menu(label=_('Secondary'), name='secondary')
|
||||
menu_setup = Menu(name='setup')
|
||||
menu_sidebar = Menu(name='sidebar')
|
||||
menu_tools = Menu(name='tools')
|
||||
menu_topbar = Menu(name='topbar')
|
||||
menu_user = Menu(
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.16 on 2018-12-29 07:38
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import mayan.apps.common.classes
|
||||
import mayan.apps.common.models
|
||||
import mayan.apps.storage.classes
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('common', '0010_auto_20180403_0702_squashed_0011_auto_20180429_0758'),
|
||||
]
|
||||
@@ -17,6 +16,10 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='shareduploadedfile',
|
||||
name='file',
|
||||
field=models.FileField(storage=mayan.apps.common.classes.FakeStorageSubclass(), upload_to=mayan.apps.common.models.upload_to, verbose_name='File'),
|
||||
field=models.FileField(
|
||||
storage=mayan.apps.storage.classes.FakeStorageSubclass(),
|
||||
upload_to=mayan.apps.common.models.upload_to,
|
||||
verbose_name='File'
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@ from .storages import storage_sharedupload
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# TODO: move outside of models.py or as a static method of SharedUploadedFile
|
||||
def upload_to(instance, filename):
|
||||
return 'shared-file-{}'.format(uuid.uuid4().hex)
|
||||
|
||||
|
||||
@@ -48,8 +48,8 @@ class PurePaginator(Paginator):
|
||||
self.allow_empty_first_page = allow_empty_first_page
|
||||
self.object_list = object_list
|
||||
self.orphans = orphans
|
||||
self.per_page = per_page
|
||||
self.page_kwarg = page_kwarg
|
||||
self.per_page = per_page
|
||||
self.request = request
|
||||
|
||||
def page(self, number):
|
||||
|
||||
@@ -7,5 +7,5 @@ from mayan.apps.permissions import PermissionNamespace
|
||||
namespace = PermissionNamespace(label=_('Common'), name='common')
|
||||
|
||||
permission_error_log_view = namespace.add_permission(
|
||||
name='error_log_view', label=_('View error log')
|
||||
label=_('View error log'), name='error_log_view'
|
||||
)
|
||||
|
||||
@@ -5,13 +5,13 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from mayan.apps.task_manager.classes import CeleryQueue
|
||||
|
||||
queue_default = CeleryQueue(
|
||||
name='default', label=_('Default'), is_default_queue=True
|
||||
is_default_queue=True, label=_('Default'), name='default'
|
||||
)
|
||||
queue_tools = CeleryQueue(name='tools', label=_('Tools'))
|
||||
queue_tools = CeleryQueue(label=_('Tools'), name='tools')
|
||||
queue_common_periodic = CeleryQueue(
|
||||
name='common_periodic', label=_('Common periodic'), transient=True
|
||||
label=_('Common periodic'), name='common_periodic', transient=True
|
||||
)
|
||||
queue_common_periodic.add_task_type(
|
||||
name='mayan.apps.common.tasks.task_delete_stale_uploads',
|
||||
label=_('Delete stale uploads')
|
||||
label=_('Delete stale uploads'),
|
||||
name='mayan.apps.common.tasks.task_delete_stale_uploads'
|
||||
)
|
||||
|
||||
@@ -5,9 +5,15 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class ContentTypeSerializer(serializers.ModelSerializer):
|
||||
class ContentTypeSerializer(serializers.HyperlinkedModelSerializer):
|
||||
class Meta:
|
||||
fields = ('app_label', 'id', 'model')
|
||||
extra_kwargs = {
|
||||
'url': {
|
||||
'lookup_url_kwarg': 'content_type_id',
|
||||
'view_name': 'rest_api:content_type-detail'
|
||||
}
|
||||
}
|
||||
fields = ('app_label', 'id', 'model', 'url')
|
||||
model = ContentType
|
||||
|
||||
|
||||
@@ -15,3 +21,7 @@ class TemplateSerializer(serializers.Serializer):
|
||||
hex_hash = serializers.CharField(read_only=True)
|
||||
name = serializers.CharField(read_only=True)
|
||||
html = serializers.CharField(read_only=True)
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
lookup_field='name', lookup_url_kwarg='template_name',
|
||||
view_name='rest_api:template-detail'
|
||||
)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
@@ -11,7 +10,7 @@ from mayan.apps.smart_settings import Namespace
|
||||
|
||||
from .literals import DEFAULT_COMMON_HOME_VIEW
|
||||
|
||||
namespace = Namespace(name='common', label=_('Common'))
|
||||
namespace = Namespace(label=_('Common'), name='common')
|
||||
|
||||
setting_auto_logging = namespace.add_setting(
|
||||
global_name='COMMON_AUTO_LOGGING',
|
||||
@@ -53,8 +52,7 @@ setting_production_error_log_path = namespace.add_setting(
|
||||
global_name='COMMON_PRODUCTION_ERROR_LOG_PATH',
|
||||
default=os.path.join(settings.MEDIA_ROOT, 'error.log'), help_text=_(
|
||||
'Path to the logfile that will track errors during production.'
|
||||
),
|
||||
is_path=True
|
||||
)
|
||||
)
|
||||
setting_project_title = namespace.add_setting(
|
||||
global_name='COMMON_PROJECT_TITLE',
|
||||
@@ -77,16 +75,8 @@ setting_shared_storage_arguments = namespace.add_setting(
|
||||
global_name='COMMON_SHARED_STORAGE_ARGUMENTS',
|
||||
default={'location': os.path.join(settings.MEDIA_ROOT, 'shared_files')}
|
||||
)
|
||||
setting_temporary_directory = namespace.add_setting(
|
||||
global_name='COMMON_TEMPORARY_DIRECTORY', default=tempfile.gettempdir(),
|
||||
help_text=_(
|
||||
'Temporary directory used site wide to store thumbnails, previews '
|
||||
'and temporary files.'
|
||||
),
|
||||
is_path=True
|
||||
)
|
||||
|
||||
namespace = Namespace(name='django', label=_('Django'))
|
||||
namespace = Namespace(label=_('Django'), name='django')
|
||||
|
||||
setting_django_allowed_hosts = namespace.add_setting(
|
||||
global_name='ALLOWED_HOSTS', default=settings.ALLOWED_HOSTS,
|
||||
@@ -357,7 +347,20 @@ setting_django_login_redirect_url = namespace.add_setting(
|
||||
'for example. This setting also accepts named URL patterns which '
|
||||
'can be used to reduce configuration duplication since you don\'t '
|
||||
'have to define the URL in two places (settings and URLconf).'
|
||||
),
|
||||
)
|
||||
)
|
||||
setting_django_logout_redirect_url = namespace.add_setting(
|
||||
global_name='LOGOUT_REDIRECT_URL',
|
||||
default=settings.LOGOUT_REDIRECT_URL,
|
||||
help_text=_(
|
||||
'Default: None. The URL where requests are redirected after a user '
|
||||
'logs out using LogoutView (if the view doesn\'t get a next_page '
|
||||
'argument). If None, no redirect will be performed and the logout '
|
||||
'view will be rendered. This setting also accepts named URL '
|
||||
'patterns which can be used to reduce configuration duplication '
|
||||
'since you don\'t have to define the URL in two places (settings '
|
||||
'and URLconf).'
|
||||
)
|
||||
)
|
||||
setting_django_static_url = namespace.add_setting(
|
||||
global_name='STATIC_URL',
|
||||
@@ -402,7 +405,7 @@ setting_django_wsgi_application = namespace.add_setting(
|
||||
),
|
||||
)
|
||||
|
||||
namespace = Namespace(name='celery', label=_('Celery'))
|
||||
namespace = Namespace(label=_('Celery'), name='celery')
|
||||
|
||||
setting_celery_always_eager = namespace.add_setting(
|
||||
global_name='CELERY_TASK_ALWAYS_EAGER',
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from mayan.apps.storage.utils import get_storage_subclass
|
||||
|
||||
from .settings import setting_shared_storage, setting_shared_storage_arguments
|
||||
from .utils import get_storage_subclass
|
||||
|
||||
storage_sharedupload = get_storage_subclass(
|
||||
dotted_path=setting_shared_storage.value
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
from django.template import Context, Library, VariableDoesNotExist, Variable
|
||||
from django.template.defaultfilters import truncatechars
|
||||
from django.template.loader import get_template
|
||||
@@ -14,6 +16,7 @@ from ..icons import icon_list_mode_items, icon_list_mode_list
|
||||
from ..literals import MESSAGE_SQLITE_WARNING
|
||||
from ..utils import check_for_sqlite, resolve_attribute
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
register = Library()
|
||||
|
||||
|
||||
@@ -48,6 +51,14 @@ def common_calculate_title(context):
|
||||
return _('Create')
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def common_get_object_verbose_name(obj):
|
||||
try:
|
||||
return obj._meta.verbose_name
|
||||
except AttributeError:
|
||||
return type(obj)
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def get_collections():
|
||||
return Collection.get_all()
|
||||
|
||||
@@ -1,30 +1,20 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.http import HttpResponse
|
||||
from django.template import Context, Template
|
||||
from django.test import TestCase
|
||||
from django.test.utils import ContextList
|
||||
from django.urls import clear_url_caches, reverse
|
||||
from django_downloadview import assert_download_response
|
||||
|
||||
from mayan.apps.acls.tests.mixins import ACLBaseTestMixin
|
||||
from mayan.apps.acls.tests.mixins import ACLTestCaseMixin
|
||||
from mayan.apps.permissions.classes import Permission
|
||||
from mayan.apps.smart_settings.classes import Namespace
|
||||
from mayan.apps.user_management.tests import (
|
||||
TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, TEST_USER_PASSWORD,
|
||||
TEST_USER_USERNAME
|
||||
)
|
||||
|
||||
from .literals import TEST_VIEW_NAME, TEST_VIEW_URL
|
||||
from .mixins import (
|
||||
ContentTypeCheckMixin, DatabaseConversionMixin, OpenFileCheckMixin,
|
||||
TempfileCheckMixin
|
||||
ClientMethodsTestCaseMixin, ContentTypeCheckMixin, DatabaseConversionMixin,
|
||||
OpenFileCheckTestCaseMixin, RandomPrimaryKeyModelMonkeyPatchMixin,
|
||||
TempfileCheckTestCaseMixin, TestViewTestCaseMixin
|
||||
)
|
||||
|
||||
|
||||
class BaseTestCase(DatabaseConversionMixin, ACLBaseTestMixin, ContentTypeCheckMixin, OpenFileCheckMixin, TempfileCheckMixin, TestCase):
|
||||
class BaseTestCase(RandomPrimaryKeyModelMonkeyPatchMixin, DatabaseConversionMixin, ACLTestCaseMixin, OpenFileCheckTestCaseMixin, TempfileCheckTestCaseMixin, TestCase):
|
||||
"""
|
||||
This is the most basic test case class any test in the project should use.
|
||||
"""
|
||||
@@ -36,81 +26,9 @@ class BaseTestCase(DatabaseConversionMixin, ACLBaseTestMixin, ContentTypeCheckMi
|
||||
Permission.invalidate_cache()
|
||||
|
||||
|
||||
class GenericViewTestCase(BaseTestCase):
|
||||
def setUp(self):
|
||||
super(GenericViewTestCase, self).setUp()
|
||||
self.has_test_view = False
|
||||
|
||||
def tearDown(self):
|
||||
from mayan.urls import urlpatterns
|
||||
|
||||
self.client.logout()
|
||||
if self.has_test_view:
|
||||
urlpatterns.pop(0)
|
||||
super(GenericViewTestCase, self).tearDown()
|
||||
|
||||
def add_test_view(self, test_object):
|
||||
from mayan.urls import urlpatterns
|
||||
|
||||
def test_view(request):
|
||||
template = Template('{{ object }}')
|
||||
context = Context(
|
||||
{'object': test_object, 'resolved_object': test_object}
|
||||
)
|
||||
return HttpResponse(template.render(context=context))
|
||||
|
||||
urlpatterns.insert(0, url(TEST_VIEW_URL, test_view, name=TEST_VIEW_NAME))
|
||||
clear_url_caches()
|
||||
self.has_test_view = True
|
||||
|
||||
def get_test_view(self):
|
||||
response = self.get(TEST_VIEW_NAME)
|
||||
if isinstance(response.context, ContextList):
|
||||
# template widget rendering causes test client response to be
|
||||
# ContextList rather than RequestContext. Typecast to dictionary
|
||||
# before updating.
|
||||
result = dict(response.context).copy()
|
||||
result.update({'request': response.wsgi_request})
|
||||
return Context(result)
|
||||
else:
|
||||
response.context.update({'request': response.wsgi_request})
|
||||
return Context(response.context)
|
||||
|
||||
def get(self, viewname=None, path=None, *args, **kwargs):
|
||||
data = kwargs.pop('data', {})
|
||||
follow = kwargs.pop('follow', False)
|
||||
|
||||
if viewname:
|
||||
path = reverse(viewname=viewname, *args, **kwargs)
|
||||
|
||||
return self.client.get(
|
||||
path=path, data=data, follow=follow
|
||||
)
|
||||
|
||||
def login(self, username, password):
|
||||
logged_in = self.client.login(username=username, password=password)
|
||||
|
||||
user = get_user_model().objects.get(username=username)
|
||||
|
||||
self.assertTrue(logged_in)
|
||||
self.assertTrue(user.is_authenticated)
|
||||
|
||||
def login_user(self):
|
||||
self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD)
|
||||
|
||||
def login_admin_user(self):
|
||||
self.login(username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD)
|
||||
|
||||
def logout(self):
|
||||
self.client.logout()
|
||||
|
||||
def post(self, viewname=None, path=None, *args, **kwargs):
|
||||
data = kwargs.pop('data', {})
|
||||
follow = kwargs.pop('follow', False)
|
||||
|
||||
if viewname:
|
||||
path = reverse(viewname=viewname, *args, **kwargs)
|
||||
|
||||
return self.client.post(
|
||||
path=path, data=data, follow=follow
|
||||
)
|
||||
class GenericViewTestCase(ClientMethodsTestCaseMixin, ContentTypeCheckMixin, TestViewTestCaseMixin, BaseTestCase):
|
||||
"""
|
||||
A generic view test case built on top of the base test case providing
|
||||
a single, user customizable view to test object resolution and shorthand
|
||||
HTTP method functions.
|
||||
"""
|
||||
|
||||
@@ -2,20 +2,26 @@ from __future__ import unicode_literals
|
||||
|
||||
import glob
|
||||
import os
|
||||
import random
|
||||
|
||||
from furl import furl
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import Group
|
||||
from django.conf.urls import url
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core import management
|
||||
from django.db import connection
|
||||
from django.db import models
|
||||
from django.http import HttpResponse
|
||||
from django.template import Context, Template
|
||||
from django.test.utils import ContextList
|
||||
from django.urls import clear_url_caches, reverse
|
||||
from django.utils.encoding import force_bytes
|
||||
|
||||
from mayan.apps.user_management.tests import (
|
||||
TEST_ADMIN_EMAIL, TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME,
|
||||
TEST_GROUP_NAME, TEST_USER_EMAIL, TEST_USER_PASSWORD,
|
||||
TEST_USER_USERNAME
|
||||
)
|
||||
|
||||
from ..settings import setting_temporary_directory
|
||||
from mayan.apps.storage.settings import setting_temporary_directory
|
||||
|
||||
from .literals import TEST_VIEW_NAME, TEST_VIEW_URL
|
||||
from .utils import mute_stdout
|
||||
|
||||
|
||||
@@ -23,6 +29,56 @@ if getattr(settings, 'COMMON_TEST_FILE_HANDLES', False):
|
||||
import psutil
|
||||
|
||||
|
||||
class ClientMethodsTestCaseMixin(object):
|
||||
def _build_verb_kwargs(self, viewname=None, path=None, *args, **kwargs):
|
||||
data = kwargs.pop('data', {})
|
||||
follow = kwargs.pop('follow', False)
|
||||
query = kwargs.pop('query', {})
|
||||
|
||||
if viewname:
|
||||
path = reverse(viewname=viewname, *args, **kwargs)
|
||||
|
||||
path = furl(url=path)
|
||||
path.args.update(query)
|
||||
|
||||
return {'follow': follow, 'data': data, 'path': path.tostr()}
|
||||
|
||||
def delete(self, viewname=None, path=None, *args, **kwargs):
|
||||
return self.client.delete(
|
||||
**self._build_verb_kwargs(
|
||||
path=path, viewname=viewname, *args, **kwargs
|
||||
)
|
||||
)
|
||||
|
||||
def get(self, viewname=None, path=None, *args, **kwargs):
|
||||
return self.client.get(
|
||||
**self._build_verb_kwargs(
|
||||
path=path, viewname=viewname, *args, **kwargs
|
||||
)
|
||||
)
|
||||
|
||||
def patch(self, viewname=None, path=None, *args, **kwargs):
|
||||
return self.client.patch(
|
||||
**self._build_verb_kwargs(
|
||||
path=path, viewname=viewname, *args, **kwargs
|
||||
)
|
||||
)
|
||||
|
||||
def post(self, viewname=None, path=None, *args, **kwargs):
|
||||
return self.client.post(
|
||||
**self._build_verb_kwargs(
|
||||
path=path, viewname=viewname, *args, **kwargs
|
||||
)
|
||||
)
|
||||
|
||||
def put(self, viewname=None, path=None, *args, **kwargs):
|
||||
return self.client.put(
|
||||
**self._build_verb_kwargs(
|
||||
path=path, viewname=viewname, *args, **kwargs
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class ContentTypeCheckMixin(object):
|
||||
expected_content_type = 'text/html; charset=utf-8'
|
||||
|
||||
@@ -34,13 +90,14 @@ class ContentTypeCheckMixin(object):
|
||||
def request(self, *args, **kwargs):
|
||||
response = super(CustomClient, self).request(*args, **kwargs)
|
||||
|
||||
content_type = response._headers['content-type'][1]
|
||||
test_instance.assertEqual(
|
||||
content_type, test_instance.expected_content_type,
|
||||
msg='Unexpected response content type: {}, expected: {}.'.format(
|
||||
content_type, test_instance.expected_content_type
|
||||
content_type = response._headers.get('content-type', [None, ''])[1]
|
||||
if test_instance.expected_content_type:
|
||||
test_instance.assertEqual(
|
||||
content_type, test_instance.expected_content_type,
|
||||
msg='Unexpected response content type: {}, expected: {}.'.format(
|
||||
content_type, test_instance.expected_content_type
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
@@ -55,7 +112,7 @@ class DatabaseConversionMixin(object):
|
||||
)
|
||||
|
||||
|
||||
class OpenFileCheckMixin(object):
|
||||
class OpenFileCheckTestCaseMixin(object):
|
||||
def _get_descriptor_count(self):
|
||||
process = psutil.Process()
|
||||
return process.num_fds()
|
||||
@@ -65,7 +122,7 @@ class OpenFileCheckMixin(object):
|
||||
return process.open_files()
|
||||
|
||||
def setUp(self):
|
||||
super(OpenFileCheckMixin, self).setUp()
|
||||
super(OpenFileCheckTestCaseMixin, self).setUp()
|
||||
if getattr(settings, 'COMMON_TEST_FILE_HANDLES', False):
|
||||
self._open_files = self._get_open_files()
|
||||
|
||||
@@ -80,10 +137,61 @@ class OpenFileCheckMixin(object):
|
||||
|
||||
self._skip_file_descriptor_test = False
|
||||
|
||||
super(OpenFileCheckMixin, self).tearDown()
|
||||
super(OpenFileCheckTestCaseMixin, self).tearDown()
|
||||
|
||||
|
||||
class TempfileCheckMixin(object):
|
||||
class RandomPrimaryKeyModelMonkeyPatchMixin(object):
|
||||
random_primary_key_random_floor = 100
|
||||
random_primary_key_random_ceiling = 10000
|
||||
random_primary_key_maximum_attempts = 100
|
||||
|
||||
@staticmethod
|
||||
def get_unique_primary_key(model):
|
||||
pk_list = model._meta.default_manager.values_list('pk', flat=True)
|
||||
|
||||
attempts = 0
|
||||
while True:
|
||||
primary_key = random.randint(
|
||||
RandomPrimaryKeyModelMonkeyPatchMixin.random_primary_key_random_floor,
|
||||
RandomPrimaryKeyModelMonkeyPatchMixin.random_primary_key_random_ceiling
|
||||
)
|
||||
|
||||
if primary_key not in pk_list:
|
||||
break
|
||||
|
||||
attempts = attempts + 1
|
||||
|
||||
if attempts > RandomPrimaryKeyModelMonkeyPatchMixin.random_primary_key_maximum_attempts:
|
||||
raise Exception(
|
||||
'Maximum number of retries for an unique random primary '
|
||||
'key reached.'
|
||||
)
|
||||
|
||||
return primary_key
|
||||
|
||||
def setUp(self):
|
||||
self.method_save_original = models.Model.save
|
||||
|
||||
def method_save_new(instance, *args, **kwargs):
|
||||
if instance.pk:
|
||||
return self.method_save_original(instance, *args, **kwargs)
|
||||
else:
|
||||
instance.pk = RandomPrimaryKeyModelMonkeyPatchMixin.get_unique_primary_key(
|
||||
model=instance._meta.model
|
||||
)
|
||||
instance.id = instance.pk
|
||||
|
||||
return instance.save_base(force_insert=True)
|
||||
|
||||
setattr(models.Model, 'save', method_save_new)
|
||||
super(RandomPrimaryKeyModelMonkeyPatchMixin, self).setUp()
|
||||
|
||||
def tearDown(self):
|
||||
models.Model.save = self.method_save_original
|
||||
super(RandomPrimaryKeyModelMonkeyPatchMixin, self).tearDown()
|
||||
|
||||
|
||||
class TempfileCheckTestCaseMixin(object):
|
||||
# Ignore the jvmstat instrumentation and GitLab's CI .config files
|
||||
# Ignore LibreOffice fontconfig cache dir
|
||||
ignore_globs = ('hsperfdata_*', '.config', '.cache')
|
||||
@@ -108,7 +216,7 @@ class TempfileCheckMixin(object):
|
||||
) - set(ignored_result)
|
||||
|
||||
def setUp(self):
|
||||
super(TempfileCheckMixin, self).setUp()
|
||||
super(TempfileCheckTestCaseMixin, self).setUp()
|
||||
if getattr(settings, 'COMMON_TEST_TEMP_FILES', False):
|
||||
self._temporary_items = self._get_temporary_entries()
|
||||
|
||||
@@ -123,4 +231,101 @@ class TempfileCheckMixin(object):
|
||||
','.join(final_temporary_items - self._temporary_items)
|
||||
)
|
||||
)
|
||||
super(TempfileCheckMixin, self).tearDown()
|
||||
super(TempfileCheckTestCaseMixin, self).tearDown()
|
||||
|
||||
|
||||
class TestModelTestMixin(object):
|
||||
def _create_test_model(self, fields=None, model_name='TestModel', options=None):
|
||||
# Obtain the app_config and app_label from the test's module path
|
||||
app_config = apps.get_containing_app_config(
|
||||
object_name=self.__class__.__module__
|
||||
)
|
||||
app_label = app_config.label
|
||||
|
||||
class Meta:
|
||||
pass
|
||||
|
||||
setattr(Meta, 'app_label', app_label)
|
||||
|
||||
if options is not None:
|
||||
for key, value in options.items():
|
||||
setattr(Meta, key, value)
|
||||
|
||||
def save(instance, *args, **kwargs):
|
||||
# Custom .save() method to use random primary key values.
|
||||
if instance.pk:
|
||||
return models.Model.self(instance, *args, **kwargs)
|
||||
else:
|
||||
instance.pk = RandomPrimaryKeyModelMonkeyPatchMixin.get_unique_primary_key(
|
||||
model=instance._meta.model
|
||||
)
|
||||
instance.id = instance.pk
|
||||
|
||||
return instance.save_base(force_insert=True)
|
||||
|
||||
attrs = {
|
||||
'__module__': self.__class__.__module__, 'save': save, 'Meta': Meta
|
||||
}
|
||||
|
||||
if fields:
|
||||
attrs.update(fields)
|
||||
|
||||
# Clear previous model registration before re-registering it again to
|
||||
# avoid conflict with test models with the same name, in the same app
|
||||
# but from another test module.
|
||||
apps.all_models[app_label].pop(model_name.lower(), None)
|
||||
|
||||
TestModel = type(
|
||||
force_bytes(model_name), (models.Model,), attrs
|
||||
)
|
||||
|
||||
setattr(self, model_name, TestModel)
|
||||
|
||||
with connection.schema_editor() as schema_editor:
|
||||
schema_editor.create_model(model=TestModel)
|
||||
|
||||
ContentType.objects.clear_cache()
|
||||
|
||||
def _create_test_object(self, model_name='TestModel', **kwargs):
|
||||
TestModel = getattr(self, model_name)
|
||||
|
||||
self.test_object = TestModel.objects.create(**kwargs)
|
||||
|
||||
|
||||
class TestViewTestCaseMixin(object):
|
||||
has_test_view = False
|
||||
|
||||
def tearDown(self):
|
||||
from mayan.urls import urlpatterns
|
||||
|
||||
self.client.logout()
|
||||
if self.has_test_view:
|
||||
urlpatterns.pop(0)
|
||||
super(TestViewTestCaseMixin, self).tearDown()
|
||||
|
||||
def add_test_view(self, test_object):
|
||||
from mayan.urls import urlpatterns
|
||||
|
||||
def test_view(request):
|
||||
template = Template('{{ object }}')
|
||||
context = Context(
|
||||
{'object': test_object, 'resolved_object': test_object}
|
||||
)
|
||||
return HttpResponse(template.render(context=context))
|
||||
|
||||
urlpatterns.insert(0, url(TEST_VIEW_URL, test_view, name=TEST_VIEW_NAME))
|
||||
clear_url_caches()
|
||||
self.has_test_view = True
|
||||
|
||||
def get_test_view(self):
|
||||
response = self.get(TEST_VIEW_NAME)
|
||||
if isinstance(response.context, ContextList):
|
||||
# template widget rendering causes test client response to be
|
||||
# ContextList rather than RequestContext. Typecast to dictionary
|
||||
# before updating.
|
||||
result = dict(response.context).copy()
|
||||
result.update({'request': response.wsgi_request})
|
||||
return Context(result)
|
||||
else:
|
||||
response.context.update({'request': response.wsgi_request})
|
||||
return Context(response.context)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
|
||||
from mayan.apps.rest_api.tests import BaseAPITestCase
|
||||
|
||||
@@ -11,23 +10,40 @@ TEST_TEMPLATE_RESULT = '<div'
|
||||
|
||||
|
||||
class CommonAPITestCase(BaseAPITestCase):
|
||||
auto_login_user = False
|
||||
|
||||
def test_content_type_list_view(self):
|
||||
response = self.client.get(reverse('rest_api:content-type-list'))
|
||||
response = self.get(viewname='rest_api:content_type-list')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@override_settings(LANGUAGE_CODE='de')
|
||||
def _request_template_detail_view(self):
|
||||
return self.get(path=self.test_template.get_absolute_url())
|
||||
|
||||
def test_template_detail_view(self):
|
||||
self.login_user()
|
||||
template_main_menu = Template.get(name='menu_main')
|
||||
response = self.client.get(template_main_menu.get_absolute_url())
|
||||
|
||||
self.test_template = Template.get(name='menu_main')
|
||||
|
||||
response = self._request_template_detail_view()
|
||||
self.assertContains(
|
||||
response=response, text=TEST_TEMPLATE_RESULT, status_code=200
|
||||
)
|
||||
|
||||
@override_settings(LANGUAGE_CODE='de')
|
||||
def test_template_detail_german_view(self):
|
||||
self.login_user()
|
||||
|
||||
self.test_template = Template.get(name='menu_main')
|
||||
|
||||
response = self._request_template_detail_view()
|
||||
self.assertContains(
|
||||
response=response, text=TEST_TEMPLATE_RESULT, status_code=200
|
||||
)
|
||||
|
||||
def test_template_detail_anonymous_view(self):
|
||||
template_main_menu = Template.get(name='menu_main')
|
||||
response = self.client.get(template_main_menu.get_absolute_url())
|
||||
self.test_template = Template.get(name='menu_main')
|
||||
|
||||
response = self._request_template_detail_view()
|
||||
self.assertNotContains(
|
||||
response=response, text=TEST_TEMPLATE_RESULT, status_code=403
|
||||
)
|
||||
|
||||
@@ -6,5 +6,5 @@ from mayan.apps.user_management.tests.mixins import UserTestMixin
|
||||
|
||||
class UserLocaleProfileTestCase(UserTestMixin, BaseTestCase):
|
||||
def test_natural_keys(self):
|
||||
self._create_user()
|
||||
self._create_test_user()
|
||||
self._test_database_conversion('auth', 'common')
|
||||
|
||||
@@ -14,8 +14,6 @@ from .literals import TEST_ERROR_LOG_ENTRY_RESULT
|
||||
|
||||
class CommonViewTestCase(GenericViewTestCase):
|
||||
def test_about_view(self):
|
||||
self.login_user()
|
||||
|
||||
response = self.get('common:about_view')
|
||||
self.assertContains(response, text='About', status_code=200)
|
||||
|
||||
@@ -25,27 +23,36 @@ class CommonViewTestCase(GenericViewTestCase):
|
||||
)
|
||||
ErrorLogEntry.objects.register(model=get_user_model())
|
||||
|
||||
self.error_log_entry = self.user.error_logs.create(
|
||||
self.error_log_entry = self._test_case_user.error_logs.create(
|
||||
result=TEST_ERROR_LOG_ENTRY_RESULT
|
||||
)
|
||||
|
||||
def _request_object_error_log_list(self):
|
||||
content_type = ContentType.objects.get_for_model(model=self.user)
|
||||
content_type = ContentType.objects.get_for_model(model=self._test_case_user)
|
||||
|
||||
return self.get(
|
||||
'common:object_error_list', kwargs={
|
||||
'app_label': content_type.app_label,
|
||||
'model': content_type.model,
|
||||
'object_id': self.user.pk
|
||||
'object_id': self._test_case_user.pk
|
||||
}, follow=True
|
||||
)
|
||||
|
||||
def test_object_error_list_view_with_permissions(self):
|
||||
def test_object_error_list_view_no_permissions(self):
|
||||
self._create_error_log_entry()
|
||||
|
||||
response = self._request_object_error_log_list()
|
||||
|
||||
self.assertNotContains(
|
||||
response=response, text=TEST_ERROR_LOG_ENTRY_RESULT,
|
||||
status_code=403
|
||||
)
|
||||
|
||||
def test_object_error_list_view_with_access(self):
|
||||
self._create_error_log_entry()
|
||||
|
||||
self.login_user()
|
||||
self.grant_access(
|
||||
obj=self.user, permission=permission_error_log_view
|
||||
obj=self._test_case_user, permission=permission_error_log_view
|
||||
)
|
||||
|
||||
response = self._request_object_error_log_list()
|
||||
@@ -54,15 +61,3 @@ class CommonViewTestCase(GenericViewTestCase):
|
||||
response=response, text=TEST_ERROR_LOG_ENTRY_RESULT,
|
||||
status_code=200
|
||||
)
|
||||
|
||||
def test_object_error_list_view_no_permissions(self):
|
||||
self._create_error_log_entry()
|
||||
|
||||
self.login_user()
|
||||
|
||||
response = self._request_object_error_log_list()
|
||||
|
||||
self.assertNotContains(
|
||||
response=response, text=TEST_ERROR_LOG_ENTRY_RESULT,
|
||||
status_code=403
|
||||
)
|
||||
|
||||
@@ -3,9 +3,7 @@ from __future__ import unicode_literals
|
||||
from django.conf.urls import url
|
||||
from django.views.i18n import javascript_catalog, set_language
|
||||
|
||||
from .api_views import (
|
||||
APIContentTypeList, APITemplateListView, APITemplateView
|
||||
)
|
||||
from .api_views import ContentTypeAPIViewSet, TemplateAPIViewSet
|
||||
from .views import (
|
||||
AboutView, CheckVersionView, CurrentUserLocaleProfileDetailsView,
|
||||
CurrentUserLocaleProfileEditView, FaviconRedirectView, HomeView,
|
||||
@@ -15,67 +13,65 @@ from .views import (
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', RootView.as_view(), name='root'),
|
||||
url(r'^home/$', HomeView.as_view(), name='home'),
|
||||
url(r'^about/$', AboutView.as_view(), name='about_view'),
|
||||
url(regex=r'^$', name='root', view=RootView.as_view()),
|
||||
url(regex=r'^home/$', name='home', view=HomeView.as_view()),
|
||||
url(regex=r'^about/$', name='about_view', view=AboutView.as_view()),
|
||||
url(
|
||||
r'^check_version/$', CheckVersionView.as_view(),
|
||||
name='check_version_view'
|
||||
regex=r'^check_version/$', name='check_version_view',
|
||||
view=CheckVersionView.as_view()
|
||||
),
|
||||
url(r'^license/$', LicenseView.as_view(), name='license_view'),
|
||||
url(regex=r'^license/$', name='license_view', view=LicenseView.as_view()),
|
||||
url(
|
||||
r'^packages/licenses/$', PackagesLicensesView.as_view(),
|
||||
name='packages_licenses_view'
|
||||
regex=r'^packages/licenses/$', name='packages_licenses_view',
|
||||
view=PackagesLicensesView.as_view()
|
||||
),
|
||||
url(
|
||||
r'^object/multiple/action/$', multi_object_action_view,
|
||||
name='multi_object_action_view'
|
||||
regex=r'^objects/multiple/action/$', name='multi_object_action_view',
|
||||
view=multi_object_action_view
|
||||
),
|
||||
url(r'^setup/$', SetupListView.as_view(), name='setup_list'),
|
||||
url(r'^tools/$', ToolsListView.as_view(), name='tools_list'),
|
||||
url(regex=r'^setup/$', name='setup_list', view=SetupListView.as_view()),
|
||||
url(regex=r'^tools/$', name='tools_list', view=ToolsListView.as_view()),
|
||||
url(
|
||||
r'^user/locale/$', CurrentUserLocaleProfileDetailsView.as_view(),
|
||||
name='current_user_locale_profile_details'
|
||||
regex=r'^users/current/locale/$',
|
||||
name='current_user_locale_profile_details',
|
||||
view=CurrentUserLocaleProfileDetailsView.as_view()
|
||||
),
|
||||
url(
|
||||
r'^user/locale/edit/$', CurrentUserLocaleProfileEditView.as_view(),
|
||||
name='current_user_locale_profile_edit'
|
||||
regex=r'^users/current/locale/edit/$',
|
||||
name='current_user_locale_profile_edit',
|
||||
view=CurrentUserLocaleProfileEditView.as_view()
|
||||
),
|
||||
url(
|
||||
r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/errors/$',
|
||||
ObjectErrorLogEntryListView.as_view(), name='object_error_list'
|
||||
regex=r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/errors/$',
|
||||
name='object_error_list', view=ObjectErrorLogEntryListView.as_view()
|
||||
),
|
||||
url(
|
||||
r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/errors/clear/$',
|
||||
ObjectErrorLogEntryListClearView.as_view(),
|
||||
name='object_error_list_clear'
|
||||
regex=r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/errors/clear/$',
|
||||
name='object_error_list_clear',
|
||||
view=ObjectErrorLogEntryListClearView.as_view()
|
||||
),
|
||||
]
|
||||
|
||||
urlpatterns += [
|
||||
url(
|
||||
r'^favicon\.ico$', FaviconRedirectView.as_view()
|
||||
regex=r'^favicon\.ico$', view=FaviconRedirectView.as_view()
|
||||
),
|
||||
url(
|
||||
r'^jsi18n/(?P<packages>\S+?)/$', javascript_catalog,
|
||||
name='javascript_catalog'
|
||||
regex=r'^jsi18n/(?P<packages>\S+?)/$', name='javascript_catalog',
|
||||
view=javascript_catalog
|
||||
),
|
||||
url(
|
||||
r'^set_language/$', set_language, name='set_language'
|
||||
regex=r'^set_language/$', name='set_language', view=set_language
|
||||
),
|
||||
]
|
||||
|
||||
api_urls = [
|
||||
url(
|
||||
r'^content_types/$', APIContentTypeList.as_view(),
|
||||
name='content-type-list'
|
||||
),
|
||||
url(
|
||||
r'^templates/$', APITemplateListView.as_view(),
|
||||
name='template-list'
|
||||
),
|
||||
url(
|
||||
r'^templates/(?P<name>[-\w]+)/$', APITemplateView.as_view(),
|
||||
name='template-detail'
|
||||
),
|
||||
]
|
||||
api_router_entries = (
|
||||
{
|
||||
'prefix': r'content_types', 'viewset': ContentTypeAPIViewSet,
|
||||
'basename': 'content_type'
|
||||
},
|
||||
{
|
||||
'prefix': r'templates', 'viewset': TemplateAPIViewSet,
|
||||
'basename': 'template'
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import FieldDoesNotExist
|
||||
from django.db.models.constants import LOOKUP_SEP
|
||||
from django.urls import resolve as django_resolve
|
||||
from django.urls.base import get_script_prefix
|
||||
from django.utils.datastructures import MultiValueDict
|
||||
from django.utils.http import urlencode as django_urlencode
|
||||
from django.utils.http import urlquote as django_urlquote
|
||||
from django.utils.module_loading import import_string
|
||||
from django.utils.six.moves import reduce as reduce_function
|
||||
from django.utils.six.moves import xmlrpc_client
|
||||
|
||||
@@ -20,7 +17,6 @@ import mayan
|
||||
|
||||
from .exceptions import NotLatestVersion, UnknownLatestVersion
|
||||
from .literals import DJANGO_SQLITE_BACKEND, MAYAN_PYPI_NAME, PYPI_URL
|
||||
from .settings import setting_temporary_directory
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -39,27 +35,6 @@ def check_version():
|
||||
raise NotLatestVersion(upstream_version=versions[0])
|
||||
|
||||
|
||||
# http://stackoverflow.com/questions/123198/how-do-i-copy-a-file-in-python
|
||||
def copyfile(source, destination, buffer_size=1024 * 1024):
|
||||
"""
|
||||
Copy a file from source to dest. source and dest
|
||||
can either be strings or any object with a read or
|
||||
write method, like StringIO for example.
|
||||
"""
|
||||
source_descriptor = get_descriptor(source)
|
||||
destination_descriptor = get_descriptor(destination, read=False)
|
||||
|
||||
while True:
|
||||
copy_buffer = source_descriptor.read(buffer_size)
|
||||
if copy_buffer:
|
||||
destination_descriptor.write(copy_buffer)
|
||||
else:
|
||||
break
|
||||
|
||||
source_descriptor.close()
|
||||
destination_descriptor.close()
|
||||
|
||||
|
||||
def encapsulate(function):
|
||||
# Workaround Django ticket 15791
|
||||
# Changeset 16045
|
||||
@@ -68,74 +43,49 @@ def encapsulate(function):
|
||||
return lambda: function
|
||||
|
||||
|
||||
def fs_cleanup(filename, file_descriptor=None, suppress_exceptions=True):
|
||||
"""
|
||||
Tries to remove the given filename. Ignores non-existent files
|
||||
"""
|
||||
if file_descriptor:
|
||||
os.close(file_descriptor)
|
||||
|
||||
def get_related_field(model, related_field_name):
|
||||
try:
|
||||
os.remove(filename)
|
||||
except OSError:
|
||||
local_field_name, remaining_field_path = related_field_name.split(
|
||||
LOOKUP_SEP, 1
|
||||
)
|
||||
except ValueError:
|
||||
local_field_name = related_field_name
|
||||
remaining_field_path = None
|
||||
|
||||
related_field = model._meta.get_field(local_field_name)
|
||||
|
||||
if remaining_field_path:
|
||||
return get_related_field(
|
||||
model=related_field.related_model,
|
||||
related_field_name=remaining_field_path
|
||||
)
|
||||
|
||||
return related_field
|
||||
|
||||
|
||||
def introspect_attribute(attribute_name, obj):
|
||||
try:
|
||||
# Try as a related field
|
||||
obj._meta.get_field(field_name=attribute_name)
|
||||
except (AttributeError, FieldDoesNotExist):
|
||||
attribute_name = attribute_name.replace('__', '.')
|
||||
|
||||
try:
|
||||
shutil.rmtree(filename)
|
||||
except OSError:
|
||||
if suppress_exceptions:
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
def get_descriptor(file_input, read=True):
|
||||
try:
|
||||
# Is it a file like object?
|
||||
file_input.seek(0)
|
||||
except AttributeError:
|
||||
# If not, try open it.
|
||||
if read:
|
||||
return open(file_input, mode='rb')
|
||||
# If there are separators in the attribute name, traverse them
|
||||
# to the final attribute
|
||||
attribute_part, attribute_remaining = attribute_name.split(
|
||||
'.', 1
|
||||
)
|
||||
except ValueError:
|
||||
return attribute_name, obj
|
||||
else:
|
||||
return open(file_input, mode='wb')
|
||||
related_field = obj._meta.get_field(field_name=attribute_part)
|
||||
return introspect_attribute(
|
||||
attribute_name=attribute_part,
|
||||
obj=related_field.related_model,
|
||||
)
|
||||
else:
|
||||
return file_input
|
||||
|
||||
|
||||
def get_storage_subclass(dotted_path):
|
||||
"""
|
||||
Import a storage class and return a subclass that will always return eq
|
||||
True to avoid creating a new migration when for runtime storage class
|
||||
changes.
|
||||
"""
|
||||
imported_storage_class = import_string(dotted_path=dotted_path)
|
||||
|
||||
class StorageSubclass(imported_storage_class):
|
||||
def __init__(self, *args, **kwargs):
|
||||
return super(StorageSubclass, self).__init__(*args, **kwargs)
|
||||
|
||||
def __eq__(self, other):
|
||||
return True
|
||||
|
||||
def deconstruct(self):
|
||||
return ('mayan.apps.common.classes.FakeStorageSubclass', (), {})
|
||||
|
||||
|
||||
return StorageSubclass
|
||||
|
||||
|
||||
def TemporaryFile(*args, **kwargs):
|
||||
kwargs.update({'dir': setting_temporary_directory.value})
|
||||
return tempfile.TemporaryFile(*args, **kwargs)
|
||||
|
||||
|
||||
def mkdtemp(*args, **kwargs):
|
||||
kwargs.update({'dir': setting_temporary_directory.value})
|
||||
return tempfile.mkdtemp(*args, **kwargs)
|
||||
|
||||
|
||||
def mkstemp(*args, **kwargs):
|
||||
kwargs.update({'dir': setting_temporary_directory.value})
|
||||
return tempfile.mkstemp(*args, **kwargs)
|
||||
return attribute_name, obj
|
||||
|
||||
|
||||
def resolve(path, urlconf=None):
|
||||
@@ -230,24 +180,3 @@ def urlquote(link=None, get=None):
|
||||
return '%s%s' % (link, django_urlencode(get, doseq=True))
|
||||
else:
|
||||
return django_urlquote(link)
|
||||
|
||||
|
||||
def validate_path(path):
|
||||
if not os.path.exists(path):
|
||||
# If doesn't exist try to create it
|
||||
try:
|
||||
os.mkdir(path)
|
||||
except Exception as exception:
|
||||
logger.debug('unhandled exception: %s', exception)
|
||||
return False
|
||||
|
||||
# Check if it is writable
|
||||
try:
|
||||
fd, test_filepath = tempfile.mkstemp(dir=path)
|
||||
os.close(fd)
|
||||
os.unlink(test_filepath)
|
||||
except Exception as exception:
|
||||
logger.debug('unhandled exception: %s', exception)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -6,27 +6,25 @@ from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404, resolve_url
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.template import RequestContext
|
||||
from django.urls import reverse_lazy
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils import timezone, translation
|
||||
from django.utils.http import urlencode
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic import RedirectView, TemplateView
|
||||
|
||||
from mayan.apps.acls.models import AccessControlList
|
||||
from mayan.apps.common.mixins import (
|
||||
ContentTypeViewMixin, ExternalObjectMixin
|
||||
)
|
||||
|
||||
from .exceptions import NotLatestVersion, UnknownLatestVersion
|
||||
from .forms import (
|
||||
LicenseForm, LocaleProfileForm, LocaleProfileForm_view,
|
||||
PackagesLicensesForm
|
||||
)
|
||||
from .generics import ( # NOQA
|
||||
AssignRemoveView, ConfirmView, FormView, MultiFormView,
|
||||
MultipleObjectConfirmActionView, MultipleObjectFormActionView, SimpleView,
|
||||
SingleObjectCreateView, SingleObjectDeleteView, SingleObjectDetailView,
|
||||
SingleObjectDownloadView, SingleObjectDynamicFormCreateView,
|
||||
SingleObjectDynamicFormEditView, SingleObjectEditView, SingleObjectListView
|
||||
from .generics import (
|
||||
ConfirmView, SimpleView, SingleObjectEditView, SingleObjectListView
|
||||
)
|
||||
from .icons import icon_object_error_list, icon_setup
|
||||
from .menus import menu_setup, menu_tools
|
||||
@@ -171,7 +169,9 @@ class ObjectErrorLogEntryListClearView(ConfirmView):
|
||||
)
|
||||
|
||||
|
||||
class ObjectErrorLogEntryListView(SingleObjectListView):
|
||||
class ObjectErrorLogEntryListView(ContentTypeViewMixin, ExternalObjectMixin, SingleObjectListView):
|
||||
#TODO: Update for MERC 6. Return 404.
|
||||
"""
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
AccessControlList.objects.check_access(
|
||||
obj=self.get_object(), permissions=permission_error_log_view,
|
||||
@@ -181,6 +181,7 @@ class ObjectErrorLogEntryListView(SingleObjectListView):
|
||||
return super(ObjectErrorLogEntryListView, self).dispatch(
|
||||
request, *args, **kwargs
|
||||
)
|
||||
"""
|
||||
|
||||
def get_extra_context(self):
|
||||
return {
|
||||
@@ -202,6 +203,7 @@ class ObjectErrorLogEntryListView(SingleObjectListView):
|
||||
'title': _('Error log entries for: %s' % self.get_object()),
|
||||
}
|
||||
|
||||
"""
|
||||
def get_object(self):
|
||||
content_type = get_object_or_404(
|
||||
klass=ContentType, app_label=self.kwargs['app_label'],
|
||||
@@ -211,9 +213,9 @@ class ObjectErrorLogEntryListView(SingleObjectListView):
|
||||
return get_object_or_404(
|
||||
klass=content_type.model_class(), pk=self.kwargs['object_id']
|
||||
)
|
||||
|
||||
"""
|
||||
def get_object_list(self):
|
||||
return self.get_object().error_logs.all()
|
||||
return self.get_external_object().error_logs.all()
|
||||
|
||||
|
||||
class PackagesLicensesView(SimpleView):
|
||||
@@ -234,41 +236,41 @@ class RootView(SimpleView):
|
||||
template_name = 'appearance/root.html'
|
||||
|
||||
|
||||
class SetupListView(TemplateView):
|
||||
class SetupListView(SimpleView):
|
||||
template_name = 'appearance/generic_list_horizontal.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
data = super(SetupListView, self).get_context_data(**kwargs)
|
||||
def get_extra_context(self):
|
||||
context = RequestContext(self.request)
|
||||
context['request'] = self.request
|
||||
data.update(
|
||||
{
|
||||
'no_results_icon': icon_setup,
|
||||
'no_results_label': _('No setup options available.'),
|
||||
'no_results_text': _(
|
||||
'No results here means that don\'t have the required '
|
||||
'permissions to perform administrative task.'
|
||||
),
|
||||
'resolved_links': menu_setup.resolve(context=context),
|
||||
'title': _('Setup items'),
|
||||
}
|
||||
)
|
||||
return data
|
||||
return {
|
||||
'no_results_icon': icon_setup,
|
||||
'no_results_label': _('No setup options available.'),
|
||||
'no_results_text': _(
|
||||
'No results here means that don\'t have the required '
|
||||
'permissions to perform administrative task.'
|
||||
),
|
||||
'resolved_links': menu_setup.resolve(context=context),
|
||||
'title': _('Setup'),
|
||||
'subtitle': _(
|
||||
'Here you can configure all aspects of the system.'
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class ToolsListView(SimpleView):
|
||||
template_name = 'appearance/generic_list_horizontal.html'
|
||||
|
||||
def get_menu_links(self):
|
||||
def get_extra_context(self):
|
||||
context = RequestContext(self.request)
|
||||
context['request'] = self.request
|
||||
|
||||
return menu_tools.resolve(context=context)
|
||||
|
||||
def get_extra_context(self):
|
||||
return {
|
||||
'resolved_links': self.get_menu_links(),
|
||||
'resolved_links': menu_tools.resolve(context=context),
|
||||
'title': _('Tools'),
|
||||
'subtitle': _(
|
||||
'These are programs are modules used to do maintenance in '
|
||||
'the system.'
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -280,7 +282,7 @@ def multi_object_action_view(request):
|
||||
next = request.POST.get(
|
||||
'next', request.GET.get(
|
||||
'next', request.META.get(
|
||||
'HTTP_REFERER', resolve_url(settings.LOGIN_REDIRECT_URL)
|
||||
'HTTP_REFERER', reverse(setting_home_view.value)
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -297,7 +299,7 @@ def multi_object_action_view(request):
|
||||
messages.error(request, _('No action selected.'))
|
||||
return HttpResponseRedirect(
|
||||
request.META.get(
|
||||
'HTTP_REFERER', resolve_url(settings.LOGIN_REDIRECT_URL)
|
||||
'HTTP_REFERER', reverse(setting_home_view.value)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -305,7 +307,7 @@ def multi_object_action_view(request):
|
||||
messages.error(request, _('Must select at least one item.'))
|
||||
return HttpResponseRedirect(
|
||||
request.META.get(
|
||||
'HTTP_REFERER', resolve_url(settings.LOGIN_REDIRECT_URL)
|
||||
'HTTP_REFERER', reverse(setting_home_view.value)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
13
mayan/apps/common/warnings.py
Normal file
13
mayan/apps/common/warnings.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from __future__ import absolute_import
|
||||
|
||||
|
||||
class DatabaseWarning(UserWarning):
|
||||
"""
|
||||
Warning when using unsupported database backends
|
||||
"""
|
||||
|
||||
|
||||
class InterfaceWarning(UserWarning):
|
||||
"""
|
||||
Warning when using obsolete internal interfaces
|
||||
"""
|
||||
@@ -3,7 +3,6 @@ from __future__ import unicode_literals
|
||||
from django import forms
|
||||
from django.template import Context, Template
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from .icons import icon_fail as default_icon_fail
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from .runtime import converter_class # NOQA
|
||||
from .transformations import ( # NOQA
|
||||
BaseTransformation, TransformationResize, TransformationRotate,
|
||||
TransformationZoom
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from mayan.apps.common import MayanAppConfig, menu_object, menu_sidebar
|
||||
from mayan.apps.acls.classes import ModelPermission
|
||||
from mayan.apps.common import MayanAppConfig, menu_object, menu_secondary
|
||||
from mayan.apps.navigation import SourceColumn
|
||||
|
||||
from .links import (
|
||||
@@ -23,25 +23,31 @@ class ConverterApp(MayanAppConfig):
|
||||
def ready(self):
|
||||
super(ConverterApp, self).ready()
|
||||
|
||||
Transformation = self.get_model('Transformation')
|
||||
Transformation = self.get_model(model_name='Transformation')
|
||||
|
||||
ModelPermission.register_inheritance(
|
||||
model=Transformation, related='content_object'
|
||||
)
|
||||
|
||||
SourceColumn(source=Transformation, label=_('Order'), attribute='order')
|
||||
SourceColumn(
|
||||
source=Transformation, label=_('Transformation'),
|
||||
func=lambda context: force_text(context['object'])
|
||||
attribute='order', include_label=True, source=Transformation
|
||||
)
|
||||
SourceColumn(
|
||||
source=Transformation, label=_('Arguments'), attribute='arguments'
|
||||
attribute='get_transformation_label', is_identifier=True,
|
||||
source=Transformation
|
||||
)
|
||||
SourceColumn(
|
||||
attribute='arguments', include_label=True, source=Transformation
|
||||
)
|
||||
|
||||
menu_object.bind_links(
|
||||
links=(link_transformation_edit, link_transformation_delete),
|
||||
sources=(Transformation,)
|
||||
)
|
||||
menu_sidebar.bind_links(
|
||||
menu_secondary.bind_links(
|
||||
links=(link_transformation_create,), sources=(Transformation,)
|
||||
)
|
||||
menu_sidebar.bind_links(
|
||||
menu_secondary.bind_links(
|
||||
links=(link_transformation_create,),
|
||||
sources=(
|
||||
'converter:transformation_create',
|
||||
|
||||
@@ -11,7 +11,7 @@ import sh
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from mayan.apps.common.utils import fs_cleanup, mkstemp
|
||||
from mayan.apps.storage.utils import fs_cleanup, mkstemp
|
||||
|
||||
from ..classes import ConverterBase
|
||||
from ..exceptions import PageCountError
|
||||
|
||||
@@ -9,9 +9,9 @@ import sh
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from mayan.apps.common.settings import setting_temporary_directory
|
||||
from mayan.apps.common.utils import fs_cleanup, mkdtemp, mkstemp
|
||||
from mayan.apps.mimetype.api import get_mimetype
|
||||
from mayan.apps.storage.settings import setting_temporary_directory
|
||||
from mayan.apps.storage.utils import fs_cleanup, mkdtemp, mkstemp
|
||||
|
||||
from .exceptions import InvalidOfficeFormat, OfficeConversionError
|
||||
from .literals import (
|
||||
|
||||
@@ -16,10 +16,10 @@ class TransformationForm(forms.ModelForm):
|
||||
|
||||
def clean(self):
|
||||
try:
|
||||
yaml.safe_load(self.cleaned_data['arguments'])
|
||||
yaml.safe_load(stream=self.cleaned_data['arguments'])
|
||||
except yaml.YAMLError:
|
||||
raise ValidationError(
|
||||
_(
|
||||
message=_(
|
||||
'"%s" not a valid entry.'
|
||||
) % self.cleaned_data['arguments']
|
||||
)
|
||||
|
||||
@@ -3,4 +3,9 @@ from __future__ import absolute_import, unicode_literals
|
||||
from mayan.apps.appearance.classes import Icon
|
||||
|
||||
icon_transformation = Icon(driver_name='fontawesome', symbol='crop')
|
||||
icon_transformation_create = Icon(driver_name='fontawesome', symbol='plus')
|
||||
icon_transformation_create = Icon(
|
||||
driver_name='fontawesome-dual', primary_symbol='crop',
|
||||
secondary_symbol='plus'
|
||||
)
|
||||
icon_transformation_delete = Icon(driver_name='fontawesome', symbol='times')
|
||||
icon_transformation_edit = Icon(driver_name='fontawesome', symbol='pencil-alt')
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user