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:
Roberto Rosario
2019-03-21 19:48:13 -04:00
516 changed files with 19888 additions and 14690 deletions

View File

@@ -217,6 +217,42 @@
- The tags app permission workflow is now reciprocal. In order - The tags app permission workflow is now reciprocal. In order
to attach a tag, the user's role will need the tag attach to attach a tag, the user's role will need the tag attach
permissions for both, the document and the tag. 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) 3.1.9 (2018-11-01)
================== ==================

View File

@@ -62,15 +62,16 @@ clean-pyc:
find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyc' -exec rm -f {} +
find . -name '*.pyo' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} +
find . -name '*~' -exec rm -f {} + find . -name '*~' -exec rm -f {} +
find . -name '__pycache__' -exec rm -R -f {} +
# Testing # Testing
test: test:
./manage.py test $(MODULE) --settings=mayan.settings.testing.development --nomigrations ./manage.py test $(MODULE) --settings=mayan.settings.testing.development --nomigrations $(ARGUMENTS)
test-all: 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: test-launch-postgres:
@docker rm -f test-postgres || true @docker rm -f test-postgres || true

View File

@@ -63,5 +63,5 @@ Changes needed:
the Role model's permissions many to many field. the Role model's permissions many to many field.
4. Update the ``AccessControlList`` models roles field to point to the group 4. Update the ``AccessControlList`` models roles field to point to the group
models. 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. ``AccessControlList`` model manager methods.

View File

@@ -1,203 +1,121 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
from django.contrib.contenttypes.models import ContentType from rest_framework import status
from django.shortcuts import get_object_or_404 from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import generics from mayan.apps.common.mixins import ContentTypeViewMixin
from mayan.apps.permissions.serializers import (
from .models import AccessControlList PermissionSerializer, RolePermissionAddRemoveSerializer
from .permissions import permission_acl_edit, permission_acl_view
from .serializers import (
AccessControlListPermissionSerializer, AccessControlListSerializer,
WritableAccessControlListPermissionSerializer,
WritableAccessControlListSerializer
) )
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): class ObjectACLAPIViewSet(ContentTypeViewMixin, ExternalObjectAPIViewSetMixin, MayanAPIModelViewSet):
""" content_type_url_kw_args = {
get: Returns a list of all the object's access control lists 'app_label': 'app_label',
post: Create a new access control list for the selected object. 'model': 'model_name'
""" }
def get_content_object(self): external_object_pk_url_kwarg = 'object_id'
content_type = get_object_or_404( lookup_url_kwarg = 'acl_id'
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.
"""
serializer_class = AccessControlListSerializer serializer_class = AccessControlListSerializer
def get_content_object(self): def create(self, request, *args, **kwargs):
if self.request.method == 'GET': serializer = self.get_serializer(data=request.data)
permission_required = permission_acl_view serializer.is_valid(raise_exception=True)
else: serializer.validated_data.update(
permission_required = permission_acl_edit {
'object_id': self.external_object.pk,
content_type = get_object_or_404( 'content_type': self.get_content_type(),
klass=ContentType, app_label=self.kwargs['app_label'], }
model=self.kwargs['model']
) )
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( def get_external_object_permission(self):
klass=content_type.model_class(), pk=self.kwargs['object_id'] action = getattr(self, 'action', None)
) if action is None:
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:
return None return None
elif action in ['list', 'retrieve', 'permission_list', 'permission_inherited_list']:
return super(APIObjectACLPermissionListView, self).get_serializer(*args, **kwargs) return permission_acl_view
def get_serializer_class(self):
if self.request.method == 'GET':
return AccessControlListPermissionSerializer
else: else:
return WritableAccessControlListPermissionSerializer return permission_acl_edit
def get_serializer_context(self): def get_external_object_queryset(self):
context = super(APIObjectACLPermissionListView, self).get_serializer_context() # Here we get a queryset the object model for which the event
if self.kwargs: # will be accessed.
context.update( return self.get_content_type().get_all_objects_for_this_type()
{
'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_queryset(self): def get_queryset(self):
return self.get_acl().permissions.all() return self.get_external_object().acls.all()
def get_serializer_context(self): @action(
context = super(APIObjectACLPermissionView, self).get_serializer_context() detail=True, lookup_url_kwarg='acl_id', methods=('post',),
if self.kwargs: serializer_class=RolePermissionAddRemoveSerializer,
context.update( url_name='permission-add', url_path='permissions/add'
{ )
'acl': self.get_acl(), 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
)

View File

@@ -2,9 +2,15 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _ 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 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 from .links import link_acl_create, link_acl_delete, link_acl_permissions
@@ -18,22 +24,33 @@ class ACLsApp(MayanAppConfig):
def ready(self): def ready(self):
super(ACLsApp, self).ready() super(ACLsApp, self).ready()
from actstream import registry
AccessControlList = self.get_model(model_name='AccessControlList') 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( SourceColumn(
attribute='role', is_identifier=True, is_sortable=True, attribute='role', is_identifier=True, is_sortable=True,
source=AccessControlList source=AccessControlList
) )
SourceColumn(
attribute='get_permission_titles', include_label=True,
source=AccessControlList
)
menu_object.bind_links( 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,) sources=(AccessControlList,)
) )
menu_sidebar.bind_links( menu_secondary.bind_links(
links=(link_acl_create,), sources=('acls:acl_list',) links=(link_acl_create,), sources=('acls:acl_list',)
) )
registry.register(AccessControlList)

View File

@@ -40,25 +40,15 @@ class ModelPermission(object):
app_label='permissions', model_name='StoredPermission' app_label='permissions', model_name='StoredPermission'
) )
permissions = [] permissions = cls.get_for_class(klass=type(instance))
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))
pks = [ 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) return StoredPermission.objects.filter(pk__in=pks)
@classmethod @classmethod
def get_inheritance(cls, model): def get_inheritances(cls, model):
return cls._inheritances[model] return cls._inheritances[model]
@classmethod @classmethod
@@ -79,7 +69,8 @@ class ModelPermission(object):
@classmethod @classmethod
def register_inheritance(cls, model, related): def register_inheritance(cls, model, related):
cls._inheritances[model] = related cls._inheritances.setdefault(model, [])
cls._inheritances[model].append(related)
@classmethod @classmethod
def register_proxy(cls, source, model): def register_proxy(cls, source, model):

16
mayan/apps/acls/events.py Normal file
View 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'
)

View File

@@ -2,7 +2,7 @@ from __future__ import absolute_import, unicode_literals
from mayan.apps.appearance.classes import Icon 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_list = Icon(driver_name='fontawesome', symbol='lock')
icon_acl_new = Icon( icon_acl_new = Icon(
driver_name='fontawesome-dual', primary_symbol='lock', driver_name='fontawesome-dual', primary_symbol='lock',

View File

@@ -21,7 +21,7 @@ def get_kwargs_factory(variable_name):
) )
return { return {
'app_label': '"{}"'.format(content_type.app_label), 'app_label': '"{}"'.format(content_type.app_label),
'model': '"{}"'.format(content_type.model), 'model_name': '"{}"'.format(content_type.model),
'object_id': '{}.pk'.format(variable_name) 'object_id': '{}.pk'.format(variable_name)
} }
@@ -29,21 +29,21 @@ def get_kwargs_factory(variable_name):
link_acl_delete = Link( link_acl_delete = Link(
args='resolved_object.pk', icon_class=icon_acl_delete, icon_class=icon_acl_delete, kwargs={'acl_id': 'resolved_object.pk'},
permissions=(permission_acl_edit,), permissions_related='content_object', permission=permission_acl_edit, tags='dangerous', text=_('Delete'),
tags='dangerous', text=_('Delete'), view='acls:acl_delete', view='acls:acl_delete',
) )
link_acl_list = Link( link_acl_list = Link(
icon_class=icon_acl_list, kwargs=get_kwargs_factory('resolved_object'), icon_class=icon_acl_list, kwargs=get_kwargs_factory(
permissions=(permission_acl_view,), text=_('ACLs'), view='acls:acl_list' variable_name='resolved_object'
), permission=permission_acl_view, text=_('ACLs'), view='acls:acl_list'
) )
link_acl_create = Link( link_acl_create = Link(
icon_class=icon_acl_new, kwargs=get_kwargs_factory('resolved_object'), icon_class=icon_acl_new, kwargs=get_kwargs_factory('resolved_object'),
permissions=(permission_acl_edit,), text=_('New ACL'), permission=permission_acl_edit, text=_('New ACL'), view='acls:acl_create'
view='acls:acl_create'
) )
link_acl_permissions = Link( link_acl_permissions = Link(
args='resolved_object.pk', icon_class=icon_permission, args='resolved_object.pk', icon_class=icon_permission,
permissions=(permission_acl_edit,), permissions_related='content_object', permission=permission_acl_edit, text=_('Permissions'),
text=_('Permissions'), view='acls:acl_permissions', view='acls:acl_permissions',
) )

View File

@@ -1,15 +1,21 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
import logging import logging
import operator
import warnings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import CharField, Value as V, Q
from django.utils.translation import ugettext from django.db.models.functions import Concat
from django.utils.translation import ugettext_lazy as _ 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 import Permission
from mayan.apps.permissions.models import StoredPermission 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 Implement a 3 tier permission system, involving a permissions, an actor
and an object and an object
""" """
def check_access(self, permissions, user, obj, related=None): def _get_acl_filters(self, queryset, stored_permission, user, related_field_name=None):
if user.is_superuser or user.is_staff: """
logger.debug( This method does the bulk of the work. It generates filters for the
'Permissions "%s" on "%s" granted to user "%s" as superuser ' AccessControlList model to determine if there are ACL entries for the
'or staff', permissions, obj, user 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: if isinstance(related_field, GenericForeignKey):
return Permission.check_permissions( # Case 3: Generic Foreign Key, multiple ContentTypes + object
requester=user, permissions=permissions # id combinations
) content_type_object_id_queryset = queryset.annotate(
except PermissionDenied: ct_fk_combination=Concat(
try: related_field.ct_field, V('-'), related_field.fk_field,
stored_permissions = [ output_field=CharField()
permission.stored_permission for permission in permissions )
] ).values('ct_fk_combination')
except TypeError:
# Not a list of permissions, just one
stored_permissions = (permissions.stored_permission,)
if related: acl_filter = self.annotate(
obj = resolve_attribute(obj=obj, attribute=related) 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: field_lookup = 'object_id__in'
parent_accessor = ModelPermission.get_inheritance(
model=obj._meta.model 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: field_lookup = '{}_id__in'.format(related_field_name)
# AttributeError means non model objects: ie Statistics acl_filter = self.filter(
# These can't have ACLs so we raise PermissionDenied content_type=content_type, permissions=stored_permission,
raise PermissionDenied( role__groups__user=user
_('Insufficient access for: %(object)s') % {'object': obj} ).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: except KeyError:
pass pass
else: else:
try: relation_result = []
return self.check_access(
obj=getattr(obj, parent_accessor), for related_field_name in related_fields:
permissions=permissions, user=user inherited_acl_queries = self._get_acl_filters(
queryset=queryset, stored_permission=stored_permission,
related_field_name=related_field_name, user=user
) )
except AttributeError: relation_result.append(reduce(operator.and_, inherited_acl_queries))
# 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
user_roles = [] result.append(reduce(operator.or_, relation_result))
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
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(): def check_access(self, obj, permission, user, raise_404=False):
logger.debug( warnings.warn(
'Permissions "%s" on "%s" denied for user "%s"', 'check_access() is deprecated, use restrict_queryset() to '
permissions, obj, user 'produce a queryset from which to .get() the corresponding '
) 'object in the local code.', InterfaceWarning
raise PermissionDenied(ugettext('Insufficient access for: %s') % obj) )
queryset = self.restrict_queryset(
permission=permission, queryset=obj._meta.default_manager.all(),
user=user
)
logger.debug( if queryset.filter(pk=obj.pk).exists():
'Permissions "%s" on "%s" granted to user "%s" through roles "%s" by direct ACL', return True
permissions, obj, user, user_roles else:
) if raise_404:
raise Http404
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()
else: else:
instance = queryset.first() raise PermissionDenied
if instance:
parent_object = return_related(
instance=instance, related_field=parent_accessor
)
try: def get_inherited_permissions(self, obj, role):
# Try to see if parent_object is a function queryset = self._get_inherited_object_permissions(obj=obj, role=role)
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)
return queryset.filter(pk__in=result) queryset = queryset | role.permissions.all()
else:
parent_acl_query = Q()
# Directly granted access # Filter the permissions to the ones that apply to the model
content_type = ContentType.objects.get_for_model( queryset = ModelPermission.get_for_instance(
model=queryset.model instance=obj
) ).filter(
acl_query = Q(pk__in=self.filter( pk__in=queryset
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
)
return queryset.filter(parent_acl_query | acl_query) return queryset
else:
def _get_inherited_object_permissions(self, obj, role):
queryset = StoredPermission.objects.none()
if not obj:
return queryset return queryset
def get_inherited_permissions(self, role, obj):
try: try:
instance = obj.first() related_fields = ModelPermission.get_inheritances(
except AttributeError: model=type(obj)
instance = obj
else:
if not instance:
return StoredPermission.objects.none()
try:
parent_accessor = ModelPermission.get_inheritance(
model=type(instance)
) )
except KeyError: except KeyError:
return StoredPermission.objects.none() pass
else: else:
try: for related_field_name in related_fields:
parent_object = resolve_attribute( try:
obj=instance, attribute=parent_accessor parent_object = resolve_attribute(
) obj=obj, attribute=related_field_name
except AttributeError: )
# Parent accessor is not an attribute, try it as a related except AttributeError:
# field. # Parent accessor is not an attribute, try it as a related
parent_object = return_related( # field.
instance=instance, related_field=parent_accessor parent_object = return_related(
) instance=obj, related_field=related_field_name
content_type = ContentType.objects.get_for_model(parent_object) )
try: content_type = ContentType.objects.get_for_model(model=parent_object)
return self.get( try:
role=role, content_type=content_type, queryset = queryset | self.get(
object_id=parent_object.pk content_type=content_type, object_id=parent_object.pk,
).permissions.all() role=role
except self.model.DoesNotExist: ).permissions.all()
return StoredPermission.objects.none() 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__) class_permissions = ModelPermission.get_for_class(klass=obj.__class__)
if permission not in class_permissions: if permission not in class_permissions:
raise PermissionNotValidForClass raise PermissionNotValidForClass
@@ -242,7 +227,42 @@ class AccessControlListManager(models.Manager):
return acl 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) content_type = ContentType.objects.get_for_model(model=obj)
acl, created = self.get_or_create( acl, created = self.get_or_create(
content_type=content_type, object_id=obj.pk, content_type=content_type, object_id=obj.pk,

View File

@@ -1,16 +1,18 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
import logging import logging
import operator
from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType 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.urls import reverse
from django.utils.encoding import force_text, python_2_unicode_compatible from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from mayan.apps.permissions.models import Role, StoredPermission from mayan.apps.permissions.models import Role, StoredPermission
from .events import event_acl_created, event_acl_edited
from .managers import AccessControlListManager from .managers import AccessControlListManager
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -30,6 +32,11 @@ class AccessControlList(models.Model):
* Role - Custom role that is being granted a permission. Roles are created * Role - Custom role that is being granted a permission. Roles are created
in the Setup menu. in the Setup menu.
""" """
# Multiple inheritance operator types
OPERATOR_AND = operator.and_
OPERATOR_OR = operator.or_
operator_default = OPERATOR_AND
content_type = models.ForeignKey( content_type = models.ForeignKey(
on_delete=models.CASCADE, related_name='object_content_type', on_delete=models.CASCADE, related_name='object_content_type',
to=ContentType to=ContentType
@@ -58,16 +65,15 @@ class AccessControlList(models.Model):
def __str__(self): def __str__(self):
return _( 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, 'object': self.content_object,
'role': self.role 'role': self.role,
} }
def get_absolute_url(self): def get_absolute_url(self):
return reverse( 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): def get_inherited_permissions(self):
@@ -85,3 +91,32 @@ class AccessControlList(models.Model):
return result or _('None') return result or _('None')
get_permission_titles.short_description = _('Permissions') 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
)

View File

@@ -1,216 +1,143 @@
from __future__ import absolute_import, unicode_literals 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 django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework.reverse import reverse
from mayan.apps.common.serializers import ContentTypeSerializer from mayan.apps.common.serializers import ContentTypeSerializer
from mayan.apps.permissions import Permission from mayan.apps.permissions.models import Role
from mayan.apps.permissions.models import Role, StoredPermission from mayan.apps.permissions.permissions import permission_role_edit
from mayan.apps.permissions.serializers import ( from mayan.apps.permissions.serializers import RoleSerializer
PermissionSerializer, RoleSerializer from mayan.apps.rest_api.mixins import ExternalObjectSerializerMixin
) from mayan.apps.rest_api.relations import MultiKwargHyperlinkedIdentityField
from .models import AccessControlList from .models import AccessControlList
class AccessControlListSerializer(serializers.ModelSerializer): class AccessControlListSerializer(ExternalObjectSerializerMixin, serializers.ModelSerializer):
content_type = ContentTypeSerializer(read_only=True) 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) 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: class Meta:
external_object_model = Role
external_object_pk_field = 'role_id'
external_object_permission = permission_role_edit
fields = ( 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 model = AccessControlList
read_only_fields = ('object_id',)
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',)
def create(self, validated_data): def create(self, validated_data):
for permission in validated_data['permissions']: role = self.get_external_object()
self.context['acl'].permissions.add(permission)
return validated_data['permissions'][0] if role:
validated_data['role'] = role
def validate(self, attrs): return super(AccessControlListSerializer, self).create(
permissions_pk_list = attrs.pop('permission_pk', None) validated_data=validated_data
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']
) )
def get_url(self, instance): def update(self, instance, validated_data):
return reverse( role = self.get_external_object()
'rest_api:accesscontrollist-detail', kwargs={
'app_label': instance.content_type.app_label, if role:
'model': instance.content_type.model, validated_data['role'] = role
'object_id': instance.object_id,
'acl_pk': instance.pk return super(AccessControlListSerializer, self).update(
}, request=self.context['request'], format=self.context['format'] 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

View File

@@ -1,47 +1,73 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.models import Group from django.core.exceptions import ImproperlyConfigured
from mayan.apps.permissions.models import Role from mayan.apps.common.tests.mixins import TestModelTestMixin
from mayan.apps.permissions.tests.literals import TEST_ROLE_LABEL from mayan.apps.permissions.tests.mixins import (
from mayan.apps.user_management.tests import ( PermissionTestMixin, RoleTestCaseMixin, RoleTestMixin
TEST_ADMIN_EMAIL, TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME,
TEST_GROUP_NAME, TEST_USER_EMAIL, TEST_USER_PASSWORD, TEST_USER_USERNAME
) )
from mayan.apps.user_management.tests.mixins import UserTestCaseMixin
from ..classes import ModelPermission
from ..models import AccessControlList from ..models import AccessControlList
from ..permissions import permission_acl_edit, permission_acl_view
class ACLBaseTestMixin(object): class ACLTestCaseMixin(RoleTestCaseMixin, UserTestCaseMixin):
auto_create_group = True
auto_create_users = True
def setUp(self): def setUp(self):
super(ACLBaseTestMixin, self).setUp() super(ACLTestCaseMixin, self).setUp()
if self.auto_create_users: if hasattr(self, '_test_case_user'):
self.admin_user = get_user_model().objects.create_superuser( self._test_case_role.groups.add(self._test_case_group)
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)
def grant_access(self, obj, permission): def grant_access(self, obj, permission):
return AccessControlList.objects.grant( if not hasattr(self, '_test_case_role'):
obj=obj, permission=permission, role=self.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( class ACLTestMixin(PermissionTestMixin, RoleTestMixin, TestModelTestMixin):
permission.stored_permission 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()

View File

@@ -9,16 +9,13 @@ from ..workflow_actions import GrantAccessAction, RevokeAccessAction
class ACLActionTestCase(ActionTestCase): class ACLActionTestCase(ActionTestCase):
def setUp(self):
super(ACLActionTestCase, self).setUp()
def test_grant_access_action(self): def test_grant_access_action(self):
action = GrantAccessAction( action = GrantAccessAction(
form_data={ form_data={
'content_type': ContentType.objects.get_for_model(model=self.document).pk, 'content_type': ContentType.objects.get_for_model(model=self.document).pk,
'object_id': self.document.pk, 'object_id': self.document.pk,
'roles': [self.role.pk], 'roles': [self._test_case_role.pk],
'permissions': [permission_document_view.uuid], 'permissions': [permission_document_view.pk],
} }
) )
action.execute(context={'entry_log': self.entry_log}) action.execute(context={'entry_log': self.entry_log})
@@ -28,7 +25,7 @@ class ACLActionTestCase(ActionTestCase):
list(self.document.acls.first().permissions.all()), list(self.document.acls.first().permissions.all()),
[permission_document_view.stored_permission] [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): def test_revoke_access_action(self):
self.grant_access( self.grant_access(
@@ -39,8 +36,8 @@ class ACLActionTestCase(ActionTestCase):
form_data={ form_data={
'content_type': ContentType.objects.get_for_model(model=self.document).pk, 'content_type': ContentType.objects.get_for_model(model=self.document).pk,
'object_id': self.document.pk, 'object_id': self.document.pk,
'roles': [self.role.pk], 'roles': [self._test_case_role.pk],
'permissions': [permission_document_view.uuid], 'permissions': [permission_document_view.pk],
} }
) )
action.execute(context={'entry_log': self.entry_log}) action.execute(context={'entry_log': self.entry_log})

View File

@@ -1,205 +1,189 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
from django.contrib.contenttypes.models import ContentType
from rest_framework import status 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 mayan.apps.rest_api.tests import BaseAPITestCase
from ..models import AccessControlList 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): def setUp(self):
super(ACLAPITestCase, self).setUp() 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( def _request_object_acl_list_api_view(self):
self.document return self.get(
viewname='rest_api:object-acl-list',
kwargs=self.test_content_object_view_kwargs
) )
def _create_acl(self): def test_object_acl_list_api_view_no_permission(self):
self.acl = AccessControlList.objects.create( response = self._request_object_acl_list_api_view()
content_object=self.document, self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
role=self.role
)
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): response = self._request_object_acl_list_api_view()
self._create_acl() self.assertEqual(response.status_code, status.HTTP_200_OK)
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
}
)
self.assertEqual( self.assertEqual(
response.data['results'][0]['content_type']['app_label'], response.data['results'][0]['content_type']['app_label'],
self.document_content_type.app_label self.test_object_content_type.app_label
) )
self.assertEqual( 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): def _request_acl_delete_api_view(self):
self._create_acl() kwargs = self.test_content_object_view_kwargs.copy()
kwargs['acl_id'] = self.test_acl.pk
response = self.delete( return self.delete(
viewname='rest_api:accesscontrollist-detail', viewname='rest_api:object-acl-detail',
kwargs={ kwargs=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 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(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): def test_object_acl_delete_api_view_no_permission(self):
self._create_acl() response = self._request_acl_delete_api_view()
response = self.get( self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
viewname='rest_api:accesscontrollist-detail', self.assertTrue(self.test_acl in AccessControlList.objects.all())
kwargs={
'app_label': self.document_content_type.app_label, def _request_object_acl_detail_api_view(self):
'model': self.document_content_type.model, kwargs = self.test_content_object_view_kwargs.copy()
'object_id': self.document.pk, kwargs['acl_id'] = self.test_acl.pk
'acl_pk': self.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( self.assertEqual(
response.data['content_type']['app_label'], response.data['content_type']['app_label'],
self.document_content_type.app_label self.test_object_content_type.app_label
) )
self.assertEqual( 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): def test_object_acl_detail_api_view_no_permission(self):
self._create_acl() response = self._request_object_acl_detail_api_view()
permission = self.acl.permissions.first() self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
response = self.delete( def _request_object_acl_permission_list_api_view(self):
viewname='rest_api:accesscontrollist-permission-detail', kwargs = self.test_content_object_view_kwargs.copy()
kwargs={ kwargs['acl_id'] = self.test_acl.pk
'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 test_object_acl_permission_detail_view(self): return self.get(
self._create_acl() viewname='rest_api:object-acl-permission-list',
permission = self.acl.permissions.first() kwargs=kwargs
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
}
) )
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( 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): def test_object_acl_permission_list_api_view_no_permission(self):
self._create_acl() response = self._request_object_acl_permission_list_api_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
response = self.get( def _request_object_acl_permission_remove_api_view(self):
viewname='rest_api:accesscontrollist-permission-list', kwargs = self.test_content_object_view_kwargs.copy()
kwargs={ kwargs['acl_id'] = self.test_acl.pk
'app_label': self.document_content_type.app_label,
'model': self.document_content_type.model, return self.post(
'object_id': self.document.pk, viewname='rest_api:object-acl-permission-remove',
'acl_pk': self.acl.pk 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( self.assertEqual(
response.data['results'][0]['permission_pk'], response.data['results'][0]['pk'],
permission_document_view.pk self.test_permission.pk
) )
def test_object_acl_permission_list_post_view(self): def test_object_acl_inherited_permission_list_api_view_no_permission(self):
self._create_acl() self.test_acl.permissions.clear()
self.test_role.grant(permission=self.test_permission)
response = self.post( response = self._request_object_acl_inherited_permission_list_api_view()
viewname='rest_api:accesscontrollist-permission-list', self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
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
)

View File

@@ -1,100 +1,84 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse from django.urls import reverse
from mayan.apps.documents.tests import GenericDocumentViewTestCase from mayan.apps.common.tests import GenericViewTestCase
from ..links import ( from ..links import (
link_acl_create, link_acl_delete, link_acl_list, link_acl_permissions 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 ..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) class AccessControlListLinksTestCase(ACLTestMixin, GenericViewTestCase):
self.login_user() 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() context = self.get_test_view()
resolved_link = link_acl_create.resolve(context=context) resolved_link = link_acl_create.resolve(context=context)
self.assertNotEqual(resolved_link, None) 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( 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): def test_object_acl_delete_link(self):
acl = AccessControlList.objects.create( self.grant_access(obj=self.test_object, permission=permission_acl_edit)
content_object=self.document, role=self.role
)
acl.permissions.add(permission_acl_edit.stored_permission) self.add_test_view(test_object=self._test_case_acl)
self.login_user()
self.add_test_view(test_object=acl)
context = self.get_test_view() context = self.get_test_view()
resolved_link = link_acl_delete.resolve(context=context) resolved_link = link_acl_delete.resolve(context=context)
self.assertNotEqual(resolved_link, None) self.assertNotEqual(resolved_link, None)
self.assertEqual( 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): def test_object_acl_edit_link(self):
acl = AccessControlList.objects.create( self.grant_access(obj=self.test_object, permission=permission_acl_edit)
content_object=self.document, role=self.role
)
acl.permissions.add(permission_acl_edit.stored_permission) self.add_test_view(test_object=self._test_case_acl)
self.login_user()
self.add_test_view(test_object=acl)
context = self.get_test_view() context = self.get_test_view()
resolved_link = link_acl_permissions.resolve(context=context) resolved_link = link_acl_permissions.resolve(context=context)
self.assertNotEqual(resolved_link, None) self.assertNotEqual(resolved_link, None)
self.assertEqual( 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): def test_object_acl_list_link(self):
acl = AccessControlList.objects.create( self.grant_access(obj=self.test_object, permission=permission_acl_view)
content_object=self.document, role=self.role
)
acl.permissions.add(permission_acl_view.stored_permission) self.add_test_view(test_object=self.test_object)
self.login_user()
self.add_test_view(test_object=self.document)
context = self.get_test_view() context = self.get_test_view()
resolved_link = link_acl_list.resolve(context=context) resolved_link = link_acl_list.resolve(context=context)
self.assertNotEqual(resolved_link, None) 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( 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
)
) )

View File

@@ -1,159 +1,401 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import models
from mayan.apps.common.tests import BaseTestCase from mayan.apps.common.tests import BaseTestCase
from mayan.apps.documents.models import Document, DocumentType from mayan.apps.documents.models import Document, DocumentType
from mayan.apps.documents.permissions import permission_document_view from mayan.apps.documents.permissions import permission_document_view
from mayan.apps.documents.tests import ( from mayan.apps.documents.tests import (
TEST_DOCUMENT_TYPE_2_LABEL, TEST_DOCUMENT_TYPE_LABEL, DocumentTestMixin, TEST_DOCUMENT_TYPE_2_LABEL, TEST_DOCUMENT_TYPE_LABEL
TEST_SMALL_DOCUMENT_PATH
) )
from ..classes import ModelPermission
from ..models import AccessControlList from ..models import AccessControlList
from .mixins import ACLTestMixin
class PermissionTestCase(DocumentTestMixin, BaseTestCase):
auto_create_document_type = False
class PermissionTestCase(BaseTestCase):
def setUp(self): def setUp(self):
super(PermissionTestCase, self).setUp() super(PermissionTestCase, self).setUp()
self.document_type_1 = DocumentType.objects.create( self.test_document_type_1 = DocumentType.objects.create(
label=TEST_DOCUMENT_TYPE_LABEL 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 label=TEST_DOCUMENT_TYPE_2_LABEL
) )
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object: self.test_document_1 = self.upload_document(
self.document_1 = self.document_type_1.new_document( document_type=self.test_document_type_1
file_object=file_object )
) self.test_document_2 = self.upload_document(
document_type=self.test_document_type_1
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object: )
self.document_2 = self.document_type_1.new_document( self.test_document_3 = self.upload_document(
file_object=file_object document_type=self.test_document_type_2
) )
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()
def test_check_access_without_permissions(self): def test_check_access_without_permissions(self):
with self.assertRaises(PermissionDenied): with self.assertRaises(PermissionDenied):
AccessControlList.objects.check_access( AccessControlList.objects.check_access(
permissions=(permission_document_view,), obj=self.test_document_1, permission=permission_document_view,
user=self.user, obj=self.document_1 user=self._test_case_user
) )
def test_filtering_without_permissions(self): def test_filtering_without_permissions(self):
self.assertQuerysetEqual( self.assertEqual(
AccessControlList.objects.filter_by_access( AccessControlList.objects.restrict_queryset(
permission=permission_document_view, user=self.user, permission=permission_document_view,
queryset=Document.objects.all() queryset=Document.objects.all(), user=self._test_case_user,
), [] ).count(), 0
) )
def test_check_access_with_acl(self): def test_check_access_with_acl(self):
acl = AccessControlList.objects.create( 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) acl.permissions.add(permission_document_view.stored_permission)
try: try:
AccessControlList.objects.check_access( AccessControlList.objects.check_access(
permissions=(permission_document_view,), user=self.user, obj=self.test_document_1, permission=permission_document_view,
obj=self.document_1 user=self._test_case_user
) )
except PermissionDenied: except PermissionDenied:
self.fail('PermissionDenied exception was not expected.') self.fail('PermissionDenied exception was not expected.')
def test_filtering_with_permissions(self): def test_filtering_with_permissions(self):
acl = AccessControlList.objects.create( 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) acl.permissions.add(permission_document_view.stored_permission)
self.assertQuerysetEqual( self.assertQuerysetEqual(
AccessControlList.objects.filter_by_access( AccessControlList.objects.restrict_queryset(
permission=permission_document_view, user=self.user, permission=permission_document_view,
queryset=Document.objects.all() queryset=Document.objects.all(), user=self._test_case_user
), (repr(self.document_1),) ), (repr(self.test_document_1),)
) )
def test_check_access_with_inherited_acl(self): def test_check_access_with_inherited_acl(self):
acl = AccessControlList.objects.create( 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.permissions.add(permission_document_view.stored_permission)
try: try:
AccessControlList.objects.check_access( AccessControlList.objects.check_access(
permissions=(permission_document_view,), user=self.user, obj=self.test_document_1, permission=permission_document_view,
obj=self.document_1 user=self._test_case_user
) )
except PermissionDenied: except PermissionDenied:
self.fail('PermissionDenied exception was not expected.') self.fail('PermissionDenied exception was not expected.')
def test_check_access_with_inherited_acl_and_local_acl(self): def test_check_access_with_inherited_acl_and_direct_acl(self):
acl = AccessControlList.objects.create( test_acl_1 = 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) test_acl_1.permissions.add(permission_document_view.stored_permission)
acl = AccessControlList.objects.create( test_acl_2 = 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) test_acl_2.permissions.add(permission_document_view.stored_permission)
try: try:
AccessControlList.objects.check_access( AccessControlList.objects.check_access(
permissions=(permission_document_view,), user=self.user, obj=self.test_document_3, permission=permission_document_view,
obj=self.document_3 user=self._test_case_user
) )
except PermissionDenied: except PermissionDenied:
self.fail('PermissionDenied exception was not expected.') self.fail('PermissionDenied exception was not expected.')
def test_filtering_with_inherited_permissions(self): def test_filtering_with_inherited_permissions(self):
acl = AccessControlList.objects.create( 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.permissions.add(permission_document_view.stored_permission)
result = AccessControlList.objects.filter_by_access( result = AccessControlList.objects.restrict_queryset(
permission=permission_document_view, user=self.user, permission=permission_document_view, queryset=Document.objects.all(),
queryset=Document.objects.all() user=self._test_case_user
) )
# Since document_1 and document_2 are of document_type_1 # Since document_1 and document_2 are of document_type_1
# they are the only ones that should be returned # they are the only ones that should be returned
self.assertTrue(self.test_document_1 in result)
self.assertTrue(self.document_1 in result) self.assertTrue(self.test_document_2 in result)
self.assertTrue(self.document_2 in result) self.assertTrue(self.test_document_3 not in result)
self.assertTrue(self.document_3 not in result)
def test_filtering_with_inherited_permissions_and_local_acl(self): 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( 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.permissions.add(permission_document_view.stored_permission)
acl = AccessControlList.objects.create( 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) acl.permissions.add(permission_document_view.stored_permission)
result = AccessControlList.objects.filter_by_access( result = AccessControlList.objects.restrict_queryset(
permission=permission_document_view, user=self.user, permission=permission_document_view, queryset=Document.objects.all(),
queryset=Document.objects.all() user=self._test_case_user,
) )
self.assertTrue(self.document_1 in result) self.assertTrue(self.test_document_1 in result)
self.assertTrue(self.document_2 in result) self.assertTrue(self.test_document_2 in result)
self.assertTrue(self.document_3 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)

View File

@@ -1,136 +1,159 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
from django.contrib.contenttypes.models import ContentType
from django.utils.encoding import force_text from django.utils.encoding import force_text
from mayan.apps.documents.tests import GenericDocumentViewTestCase from mayan.apps.common.tests import GenericViewTestCase
from mayan.apps.permissions.tests.mixins import RoleTestMixin
from ..classes import ModelPermission
from ..models import AccessControlList from ..models import AccessControlList
from ..permissions import permission_acl_edit, permission_acl_view from ..permissions import permission_acl_edit, permission_acl_view
from .mixins import ACLTestMixin
class AccessControlListViewTestCase(RoleTestMixin, GenericDocumentViewTestCase):
class AccessControlListViewTestCase(ACLTestMixin, GenericViewTestCase):
def setUp(self): def setUp(self):
super(AccessControlListViewTestCase, self).setUp() 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 = { self._inject_test_object_content_type()
'app_label': content_type.app_label,
'model': content_type.model,
'object_id': self.test_object.pk
}
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( return self.get(
viewname='acls:acl_create', viewname='acls:acl_create',
kwargs=self.view_content_object_arguments, data={ kwargs=self.test_content_object_view_kwargs, data={
'role': self.test_role.pk 'role': self.test_role.pk
} }
) )
def test_acl_create_view_get_no_permission(self): def test_acl_create_get_view_no_permission(self):
response = self._request_get_acl_create_view() self.test_acl.delete()
response = self._request_acl_create_get_view()
self.assertEqual(response.status_code, 404) 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) 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( self.assertContains(
response=response, text=force_text(self.test_object), response=response, text=force_text(self.test_object),
status_code=200 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( return self.post(
viewname='acls:acl_create', viewname='acls:acl_create',
kwargs=self.view_content_object_arguments, data={ kwargs=self.test_content_object_view_kwargs, data={
'role': self.test_role.pk 'role': self.test_role.pk
} }
) )
def test_acl_create_view_post_no_permission(self): 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(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) 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) 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 Test creating a duplicate ACL entry: same object & role
Result: Should redirect to existing ACL for object + role combination 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) 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( self.assertNotContains(
response=response, text=force_text(self.test_acl.role), response=response, text=force_text(self.test_acl.role),
status_code=200 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.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.assertQuerysetEqual(
self.test_acl = AccessControlList.objects.create( qs=AccessControlList.objects.order_by('role__id').values(
content_object=self.test_object, role=self.test_role 'object_id', 'role',
), transform=dict, values=expected_results
) )
def _request_acl_delete_view(self): def _request_acl_delete_view(self):
return self.post( 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): def test_acl_delete_view_no_permission(self):
self._create_test_acl()
response = self._request_acl_delete_view() response = self._request_acl_delete_view()
self.assertNotContains( self.assertNotContains(
response=response, text=force_text(self.test_object), response=response, text=force_text(self.test_object),
status_code=404 status_code=404
) )
# 1 ACL: the test one
self.assertQuerysetEqual( self.assertTrue(self.test_object.acls.filter(role=self.test_role).exists())
qs=AccessControlList.objects.all(), values=(repr(self.test_acl),)
)
def test_acl_delete_view_with_access(self): def test_acl_delete_view_with_access(self):
self._create_test_acl() self.grant_access(
acl = self.grant_access(
obj=self.test_object, permission=permission_acl_edit obj=self.test_object, permission=permission_acl_edit
) )
response = self._request_acl_delete_view() response = self._request_acl_delete_view()
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
# 1 ACL: the one created by the self.grant_access
self.assertQuerysetEqual( self.assertFalse(self.test_object.acls.filter(role=self.test_role).exists())
qs=AccessControlList.objects.all(), values=(repr(acl),)
)
def _request_acl_list_view(self): def _request_acl_list_view(self):
return self.get( 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): def test_acl_list_view_no_permission(self):
@@ -151,28 +174,66 @@ class AccessControlListViewTestCase(RoleTestMixin, GenericDocumentViewTestCase):
status_code=200 status_code=200
) )
def _request_get_acl_permissions_view(self): def _request_get_acl_permissions_get_view(self):
return self.get( return self.get(
viewname='acls:acl_permissions', 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): def test_acl_permissions_get_view_no_permission(self):
self._create_test_acl() self.test_acl.permissions.clear()
response = self._request_get_acl_permissions_view() response = self._request_get_acl_permissions_get_view()
self.assertNotContains( self.assertNotContains(
response=response, text=force_text(self.test_object), response=response, text=force_text(self.test_object),
status_code=404 status_code=404
) )
def test_acl_permissions_view_get_with_access(self): self.assertFalse(
self._create_test_acl() 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) 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( self.assertContains(
response=response, text=force_text(self.test_object), response=response, text=force_text(self.test_object),
status_code=200 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()
)

View File

@@ -2,50 +2,33 @@ from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
from .api_views import ( from .api_views import ObjectACLAPIViewSet
APIObjectACLListView, APIObjectACLPermissionListView,
APIObjectACLPermissionView, APIObjectACLView
)
from .views import ( from .views import (
ACLCreateView, ACLDeleteView, ACLListView, ACLPermissionsView ACLCreateView, ACLDeleteView, ACLListView, ACLPermissionsView
) )
urlpatterns = [ urlpatterns = [
url( 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() name='acl_create', view=ACLCreateView.as_view()
), ),
url( 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() name='acl_list', view=ACLListView.as_view()
), ),
url( 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() view=ACLDeleteView.as_view()
), ),
url( 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() view=ACLPermissionsView.as_view()
), ),
] ]
api_urls = [ api_router_entries = (
url( {
regex=r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/acls/$', 'prefix': r'apps/(?P<app_label>[^/.]+)/models/(?P<model_name>[^/.]+)/objects/(?P<object_id>[^/.]+)/acls',
name='accesscontrollist-list', view=APIObjectACLListView.as_view() 'viewset': ObjectACLAPIViewSet, 'basename': 'object-acl'
), },
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()
),
]

View File

@@ -1,24 +1,20 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
import itertools
import logging import logging
from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404
from django.template import RequestContext from django.template import RequestContext
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.mixins import ( from mayan.apps.common.mixins import (
ContentTypeViewMixin, ExternalObjectViewMixin ContentTypeViewMixin, ExternalObjectMixin
) )
from mayan.apps.common.views import ( from mayan.apps.common.generics import (
AssignRemoveView, SingleObjectCreateView, SingleObjectDeleteView, AddRemoveView, SingleObjectCreateView, SingleObjectDeleteView,
SingleObjectListView SingleObjectListView
) )
from mayan.apps.permissions import Permission, PermissionNamespace from mayan.apps.permissions.models import Role
from mayan.apps.permissions.models import Role, StoredPermission
from .classes import ModelPermission from .classes import ModelPermission
from .forms import ACLCreateForm from .forms import ACLCreateForm
@@ -30,7 +26,11 @@ from .permissions import permission_acl_edit, permission_acl_view
logger = logging.getLogger(__name__) 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_permission = permission_acl_edit
external_object_pk_url_kwarg = 'object_id' external_object_pk_url_kwarg = 'object_id'
form_class = ACLCreateForm form_class = ACLCreateForm
@@ -44,7 +44,7 @@ class ACLCreateView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectC
def get_external_object_queryset(self): def get_external_object_queryset(self):
# Here we get a queryset the object model for which an ACL will be # Here we get a queryset the object model for which an ACL will be
# created. # 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): def get_extra_context(self):
return { return {
@@ -61,7 +61,8 @@ class ACLCreateView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectC
'queryset': Role.objects.exclude( 'queryset': Role.objects.exclude(
pk__in=self.get_external_object().acls.values('role') 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): def get_instance_extra_data(self):
@@ -77,15 +78,17 @@ class ACLCreateView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectC
class ACLDeleteView(SingleObjectDeleteView): class ACLDeleteView(SingleObjectDeleteView):
object_permission = permission_acl_edit
object_permission_related = 'content_object'
object_permission_raise_404 = True
model = AccessControlList model = AccessControlList
pk_url_kwarg = 'acl_pk' object_permission = permission_acl_edit
pk_url_kwarg = 'acl_id'
def get_extra_context(self): def get_extra_context(self):
acl = self.get_object()
return { 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(), 'title': _('Delete ACL: %s') % self.get_object(),
} }
@@ -94,20 +97,24 @@ class ACLDeleteView(SingleObjectDeleteView):
return reverse( return reverse(
'acls:acl_list', kwargs={ 'acls:acl_list', kwargs={
'app_label': instance.content_type.app_label, 'app_label': instance.content_type.app_label,
'model': instance.content_type.model, 'model_name': instance.content_type.model,
'object_id': instance.object_id '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_permission = permission_acl_view
external_object_pk_url_kwarg = 'object_id' external_object_pk_url_kwarg = 'object_id'
def get_external_object_queryset(self): def get_external_object_queryset(self):
# Here we get a queryset the object model for which an ACL will be # Here we get a queryset the object model for which an ACL will be
# created. # 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): def get_extra_context(self):
return { 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() return self.get_external_object().acls.all()
class ACLPermissionsView(AssignRemoveView): class ACLPermissionsView(AddRemoveView):
grouped = True action_add_method = 'permissions_add'
left_list_title = _('Available permissions') action_remove_method = 'permissions_remove'
right_list_title = _('Granted permissions') 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(self, queryset):
def generate_choices(entries): namespaces_dictionary = {}
results = []
entries = sorted( # Sort permissions by their translatable label
entries, key=lambda x: ( object_list = sorted(
x.volatile_permission.namespace.label, queryset, key=lambda permission: permission.volatile_permission.label
x.volatile_permission.label
)
) )
for namespace, permissions in itertools.groupby(entries, lambda entry: entry.namespace): # Group permissions by namespace
permission_options = [ for permission in object_list:
(force_text(permission.pk), permission) for permission in permissions namespaces_dictionary.setdefault(
] permission.volatile_permission.namespace.label,
results.append( []
(PermissionNamespace.get(name=namespace), permission_options) )
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): def get_actions_extra_kwargs(self):
permission = get_object_or_404(klass=StoredPermission, pk=item) return {'_user': self.request.user}
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_disabled_choices(self): def get_disabled_choices(self):
""" """
Get permissions from a parent's acls but remove the permissions we Get permissions from a parent's ACLs or directly granted to the role.
already hold for this object We return a list since that is what the form widget's can process.
""" """
return map( return self.main_object.get_inherited_permissions().values_list('pk', flat=True)
str, set(
self.get_object().get_inherited_permissions().values_list(
'pk', flat=True
)
).difference(
self.get_object().permissions.values_list('pk', flat=True)
)
)
def get_extra_context(self): def get_extra_context(self):
return { return {
'object': self.get_object().content_object, 'acl': self.main_object,
'title': _('Role "%(role)s" permission\'s for "%(object)s"') % { 'object': self.main_object.content_object,
'role': self.get_object().role, 'navigation_object_list': ('object', 'acl'),
'object': self.get_object().content_object, '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 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( queryset_acl = super(ACLPermissionsView, self).get_list_added_queryset()
'pk', flat=True
) | self.get_object().get_inherited_permissions().values_list( return (
'pk', flat=True 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())

View File

@@ -89,7 +89,8 @@ class GrantAccessAction(WorkflowAction):
try: try:
AccessControlList.objects.check_access( 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: except Exception as exception:
raise ValidationError(exception) raise ValidationError(exception)

View File

@@ -1 +1 @@
DEFAULT_MAXIMUM_TITLE_LENGTH = 80 DEFAULT_MAXIMUM_TITLE_LENGTH = 120

View File

@@ -8,10 +8,10 @@ class MayanApp {
ajaxMenusOptions: [] ajaxMenusOptions: []
} }
this.ajaxSpinnerSeletor = '#ajax-spinner';
this.ajaxExecuting = false; this.ajaxExecuting = false;
this.ajaxMenusOptions = options.ajaxMenusOptions; this.ajaxMenusOptions = options.ajaxMenusOptions;
this.ajaxMenuHashes = {}; this.ajaxMenuHashes = {};
this.ajaxSpinnerSeletor = '#ajax-spinner';
this.window = $(window); 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 () { static setupMultiItemActions () {
$('body').on('change', '.check-all-slave', function () { $('body').on('change', '.check-all-slave', function () {
MayanApp.countChecked(); 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 () { static updateNavbarState () {
var uri = new URI(window.location.hash); var uri = new URI(window.location.hash);
var uriFragment = uri.fragment(); var uriFragment = uri.fragment();
@@ -110,35 +71,6 @@ class MayanApp {
// Instance methods // 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 () { callbackAJAXSpinnerUpdate () {
if (this.ajaxExecuting) { if (this.ajaxExecuting) {
$(this.ajaxSpinnerSeletor).fadeIn(50); $(this.ajaxSpinnerSeletor).fadeIn(50);
@@ -239,7 +171,6 @@ class MayanApp {
initialize () { initialize () {
var self = this; var self = this;
this.setupAJAXPeriodicWorkers();
this.setupAJAXSpinner(); this.setupAJAXSpinner();
this.setupFormHotkeys(); this.setupFormHotkeys();
this.setupFullHeightResizing(); this.setupFullHeightResizing();
@@ -256,22 +187,6 @@ class MayanApp {
partialNavigation.initialize(); 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 () { setupAJAXSpinner () {
var self = this; var self = this;
@@ -445,12 +360,6 @@ class MayanApp {
dropdownAutoWidth: true, dropdownAutoWidth: true,
width: '100%' width: '100%'
}); });
$('.select2-tags').select2({
templateSelection: MayanApp.tagSelectionTemplate,
templateResult: MayanApp.tagResultTemplate,
width: '100%'
});
} }
resizeFullHeight () { resizeFullHeight () {

View File

@@ -37,7 +37,7 @@
</div> </div>
</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> <style>
@@ -45,12 +45,12 @@
<div class="row"> <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' %} {% include 'appearance/calculate_form_title.html' %}
{# action menu #} {# action menu #}
{% get_menus_links names='object,sidebar,secondary' sort_results=True as links_actions %} {% navigation_resolve_menus names='object,secondary' sort_results=True as action_menus_link_results %}
{% if links_actions %} {% if action_menus_link_results %}
<div class="pull-right btn-group" id="menu-actions"> <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"> <button aria-expanded="true" class="btn btn-danger btn-sm dropdown-toggle" data-toggle="dropdown" type="button">
{% trans 'Actions' %} {% trans 'Actions' %}
@@ -58,19 +58,39 @@
<span class="sr-only">{% trans 'Toggle Dropdown' %}</span> <span class="sr-only">{% trans 'Toggle Dropdown' %}</span>
</button> </button>
<ul class="dropdown-menu" role="menu"> <ul class="dropdown-menu" role="menu">
{% for object_navigation_links in links_actions %} {% for menus_link_result in action_menus_link_results %}
{% with 'true' as as_li %} {% if action_menus_link_results|length > 1 %}
{% with 'true' as hide_active_anchor %} <li class="dropdown-header">{{ menus_link_result.menu.label }}</li>
{% with 'btn-sm' as link_classes %} {% endif %}
{% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% endwith %}
{% endwith %}
{% 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> <li class="divider"></li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
@@ -80,17 +100,21 @@
{% block footer %}{% endblock %} {% block footer %}{% endblock %}
</div> </div>
{% if links_facet %} {% if facet_menus_link_results %}
<div id="sidebar"> <div id="sidebar">
<div class="pull-right list-group"> <div class="pull-right list-group">
{% for object_navigation_links in links_facet %} {% for menu_link_result in facet_menus_link_results %}
{% with 'true' as hide_active_anchor %} {% for link_group in menu_link_result.link_groups %}
{% with 'active' as link_class_active %} {% with link_group.links as object_navigation_links %}
{% with 'list-group-item btn-sm' as link_classes %} {% with 'true' as hide_active_anchor %}
{% include 'navigation/generic_navigation.html' %} {% with 'active' as link_class_active %}
{% endwith %} {% with 'list-group-item btn-sm' as link_classes %}
{% endwith %} {% include 'navigation/generic_navigation.html' %}
{% endwith %} {% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endfor %}
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@@ -90,6 +90,17 @@
<i class="fa fa-times"></i> {% if cancel_label %}{{ cancel_label }}{% else %}{% trans 'Cancel' %}{% endif %} <i class="fa fa-times"></i> {% if cancel_label %}{{ cancel_label }}{% else %}{% trans 'Cancel' %}{% endif %}
</a> </a>
{% endif %} {% 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> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}

View File

@@ -11,8 +11,10 @@
<div class="well center-block"> <div class="well center-block">
<div class="row"> <div class="row">
{% with 'navigation/large_button_link.html' as link_template %} {% with 'navigation/large_button_link.html' as link_template %}
{% for object_navigation_links in resolved_links %} {% for menu_results in resolved_links %}
{% include 'navigation/generic_navigation.html' %} {% with menu_results.links as object_navigation_links %}
{% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% empty %} {% empty %}
<p class="text-center"> <p class="text-center">
{% include 'appearance/no_results.html' %} {% include 'appearance/no_results.html' %}

View File

@@ -27,7 +27,7 @@
<div class="well center-block"> <div class="well center-block">
{% if object_list %} {% if object_list %}
{% if not hide_multi_item_actions %} {% 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 %}
{% endif %} {% endif %}
@@ -45,7 +45,7 @@
<div class="form-group"> <div class="form-group">
<div class="checkbox"> <div class="checkbox">
<label for="id_indexes_0"> <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" /> <input class="form-multi-object-action-checkbox check-all-slave checkbox" name="pk_{{ object.pk }}" type="checkbox" />
{% endif %} {% endif %}
@@ -84,9 +84,9 @@
{% endfor %} {% endfor %}
{% if not hide_links %} {% 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"> <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"> <button aria-expanded="false" aria-haspopup="true" class="btn btn-default btn-danger btn-sm dropdown-toggle" data-toggle="dropdown">
{% trans 'Actions' %} {% trans 'Actions' %}
@@ -94,18 +94,25 @@
<span class="sr-only">{% trans 'Toggle Dropdown' %}</span> <span class="sr-only">{% trans 'Toggle Dropdown' %}</span>
</button> </button>
<ul class="dropdown-menu" role="menu"> <ul class="dropdown-menu" role="menu">
{% for object_navigation_links in links %} {% for facet_menu_link_result in facet_menus_link_results %}
{% with 'true' as as_li %} {% for link_group in facet_menu_link_result.link_groups %}
{% with 'true' as hide_active_anchor %}
{% with 'btn-sm' as link_classes %}
{% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% endwith %}
{% endwith %}
{% 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> <li class="divider"></li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>

View File

@@ -27,7 +27,7 @@
<div class="well center-block"> <div class="well center-block">
{% if object_list %} {% if object_list %}
{% if not hide_multi_item_actions %} {% 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 %}
{% endif %} {% endif %}
@@ -42,21 +42,36 @@
<thead> <thead>
{% if not hide_header %} {% if not hide_header %}
<tr> <tr>
{% if links_multi_item %} {% if links_multi_menus_results %}
<th class="first"></th> <th class="first"></th>
{% endif %} {% endif %}
{% if not hide_object %} {% if not hide_object %}
<th>{% trans 'Identifier' %}</th> <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 %} {% endif %}
{% if not hide_columns %} {% 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 %} {% for column in source_columns %}
<th> <th>
{% if column.is_sortable %} {% if column.is_sortable %}
<a href="{% get_sort_field_querystring column=column %}">{{ column.label }} <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 %} {% if icon_sort %}{{ icon_sort.render }}{% endif %}
{% endif %} {% endif %}
</a> </a>
@@ -80,7 +95,7 @@
<tbody> <tbody>
{% for object in object_list %} {% for object in object_list %}
<tr> <tr>
{% if links_multi_item %} {% if links_multi_menus_results %}
<td> <td>
<input class="form-multi-object-action-checkbox check-all-slave checkbox" name="pk_{{ object.pk }}" type="checkbox" value="" /> <input class="form-multi-object-action-checkbox check-all-slave checkbox" name="pk_{{ object.pk }}" type="checkbox" value="" />
</td> </td>
@@ -100,7 +115,7 @@
</td> </td>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if not hide_columns %} {% if not hide_columns %}
{% get_source_columns source=object exclude_identifier=True as source_columns %} {% get_source_columns source=object exclude_identifier=True as source_columns %}
{% for column in source_columns %} {% for column in source_columns %}
<td> <td>
@@ -116,21 +131,29 @@
{% endfor %} {% endfor %}
{% if not hide_links %} {% if not hide_links %}
<td class="last"> <td class="last">
{% get_menu_links name='list facet' sort_results=True source=object as resolved_links %} {% navigation_resolve_menu name='list facet' sort_results=True source=object as facet_menus_results %}
{% for object_navigation_links in resolved_links %} {% for facet_menu_results in facet_menus_results %}
{% with 'true' as horizontal %} {% for link_group in facet_menu_results.link_groups %}
{% with 'true' as hide_icon %} {% with link_group.links as object_navigation_links %}
{% include 'navigation/generic_navigation.html' %} {% with 'true' as horizontal %}
{% endwith %} {% with 'true' as hide_icon %}
{% endwith %} {% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endfor %}
{% endfor %} {% endfor %}
{% get_menu_links name='object' source=object as resolved_links %} {% navigation_resolve_menu name='object' source=object as object_menus_results %}
{% for object_navigation_links in resolved_links %} {% for object_menu_results in object_menus_results %}
{% with 'true' as horizontal %} {% for link_group in object_menu_results.link_groups %}
{% with 'true' as hide_icon %} {% with link_group.links as object_navigation_links %}
{% include 'navigation/generic_navigation.html' %} {% with 'true' as horizontal %}
{% endwith %} {% with 'true' as hide_icon %}
{% endwith %} {% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endfor %}
{% endfor %} {% endfor %}
</td> </td>
{% endif %} {% endif %}

View File

@@ -6,7 +6,7 @@
<div class="pull-left"> <div class="pull-left">
<div class="btn-toolbar" role="toolbar"> <div class="btn-toolbar" role="toolbar">
<div class="btn-group"> <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' %}"> <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> <i class="far fa-square"></i>
</a> </a>
@@ -19,7 +19,7 @@
</div> </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> <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;"> <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> <span class="sr-only">{% trans 'Toggle Dropdown' %}</span>
</button> </button>
<ul class="dropdown-menu" role="menu"> <ul class="dropdown-menu" role="menu">
{% for object_navigation_links in links_multi_item %} {% for multi_item_menu_results in links_multi_menus_results %}
{% with 'true' as as_li %} {% for link_group in multi_item_menu_results.link_groups %}
{% with 'true' as hide_active_anchor %} {% with link_group.links as object_navigation_links %}
{% with 'btn-sm btn-multi-item-action' as link_classes %} {% with 'true' as as_li %}
{% include 'navigation/generic_navigation.html' %} {% with 'true' as hide_active_anchor %}
{% endwith %} {% with 'btn-sm btn-multi-item-action' as link_classes %}
{% endwith %} {% include 'navigation/generic_navigation.html' %}
{% endwith %} {% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}
{% if not forloop.last and object_navigation_links %} {% endfor %}
{% if not forloop.last and link_group %}
<li class="divider"></li> <li class="divider"></li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}

View File

@@ -8,58 +8,62 @@
{% spaceless %} {% spaceless %}
<div class="panel-group" id="accordion-sidebar" role="tablist" aria-multiselectable="true"> <div class="panel-group" id="accordion-sidebar" role="tablist" aria-multiselectable="true">
{% get_menu_links name='main' as menu_links %} {% navigation_resolve_menu name='main' as main_menus_results %}
{% for link_set in menu_links %} {% for main_menu_results in main_menus_results %}
{% for link in link_set %} {% for link_group in main_menu_results.link_groups %}
{% with 'active' as li_class_active %} {% for link in link_group.links %}
{% with ' ' as link_classes %} {% with 'active' as li_class_active %}
{% if link|get_type == "<class 'mayan.apps.navigation.classes.Menu'>" %} {% with ' ' as link_classes %}
<div class="panel panel-default"> {% if link|get_type == "<class 'mayan.apps.navigation.classes.Menu'>" %}
<div class="panel-heading" role="tab" id="headingOne"> <div class="panel panel-default">
<h4 class="panel-title"> <div class="panel-heading" role="tab" id="headingOne">
<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"> <h4 class="panel-title">
<div class="pull-left"> <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">
{% if link.icon %} <div class="pull-left">
<i class="hidden-xs hidden-sm hidden-md {{ link.icon }}"></i> {% if link.icon %}
{% endif %} <i class="hidden-xs hidden-sm hidden-md {{ link.icon }}"></i>
{% if link.icon_class %}{{ link.icon_class.render }}{% endif %} {% endif %}
{{ link.label }} {% if link.icon_class %}{{ link.icon_class.render }}{% endif %}
</div> {{ link.label }}
<div class="accordion-indicator pull-right"><span class="caret"></span></div> </div>
<div class="clearfix"></div> <div class="accordion-indicator pull-right"><span class="caret"></span></div>
</a> <div class="clearfix"></div>
</h4> </a>
</div> </h4>
<div id="accordion-body-{{ forloop.counter }}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingOne"> </div>
<div class="panel-body"> <div id="accordion-body-{{ forloop.counter }}" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingOne">
<ul class="list-unstyled"> <div class="panel-body">
{% get_menu_links name=link.name as menu_links %} <ul class="list-unstyled">
{% for linkset in menu_links %} {% navigation_resolve_menu name=link.name as sub_menus_results %}
{% with '' as link_class_active %} {% for sub_menu_results in sub_menus_results %}
{% with 'a-main-menu-accordion-link' as link_classes %} {% for link_group in sub_menu_results.link_groups %}
{% with 'true' as as_li %} {% with '' as link_class_active %}
{% with linkset as object_navigation_links %} {% with 'a-main-menu-accordion-link' as link_classes %}
{% include 'navigation/generic_navigation.html' %} {% with 'true' as as_li %}
{% endwith %} {% with link_group.links as object_navigation_links %}
{% endwith %} {% include 'navigation/generic_navigation.html' %}
{% endwith %} {% endwith %}
{% endwith %} {% endwith %}
{% endfor %} {% endwith %}
</ul> {% endwith %}
{% endfor %}
{% endfor %}
</ul>
</div>
</div> </div>
</div> </div>
</div> {% else %}
{% else %} <div class="panel panel-default">
<div class="panel panel-default"> <div class="panel-heading" role="tab" id="headingOne">
<div class="panel-heading" role="tab" id="headingOne"> <h4 class="panel-title">
<h4 class="panel-title"> {% include 'navigation/generic_link_instance.html' %}
{% include 'navigation/generic_link_instance.html' %} </h4>
</h4> </div>
</div> </div>
</div> {% endif %}
{% endif %} {% endwith %}
{% endwith %} {% endwith %}
{% endwith %} {% endfor %}
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
</div> </div>

View File

@@ -18,20 +18,22 @@
<div id="navbar" class="navbar-collapse collapse"> <div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
{% get_menu_links name='topbar' as menu_links %} {% navigation_resolve_menu name='topbar' as topbar_menus_results %}
{% for link_set in menu_links %} {% for tobpar_menu_result in topbar_menus_results %}
{% for link in link_set %} {% for link_group in tobpar_menu_result.link_groups %}
{% with 'true' as as_li %} {% for link in link_group.links %}
{% with 'true' as hide_active_anchor %} {% with 'true' as as_li %}
{% with 'active' as li_class_active %} {% with 'true' as hide_active_anchor %}
{% with 'first' as li_class_first %} {% with 'active' as li_class_active %}
{% with ' ' as link_classes %} {% with 'first' as li_class_first %}
{% include 'navigation/generic_subnavigation.html' %} {% with ' ' as link_classes %}
{% endwith %} {% include 'navigation/generic_subnavigation.html' %}
{% endwith %} {% endwith %}
{% endwith %} {% endwith %}
{% endwith %} {% endwith %}
{% endwith %} {% endwith %}
{% endwith %}
{% endfor %}
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
</ul> </ul>

View File

@@ -28,4 +28,4 @@ def get_form_media_js(form):
@register.simple_tag @register.simple_tag
def get_icon(icon_path): def get_icon(icon_path):
return import_string(icon_path).render() return import_string(dotted_path=icon_path).render()

View File

@@ -23,8 +23,10 @@ class EmailAuthenticationForm(forms.Form):
remember_me = forms.BooleanField(label=_('Remember me'), required=False) remember_me = forms.BooleanField(label=_('Remember me'), required=False)
error_messages = { error_messages = {
'invalid_login': _('Please enter a correct email and password. ' 'invalid_login': _(
'Note that the password field is case-sensitive.'), 'Please enter a correct email and password. Note that the '
'password field is case-sensitive.'
),
'inactive': _('This account is inactive.'), 'inactive': _('This account is inactive.'),
} }
@@ -56,8 +58,10 @@ class EmailAuthenticationForm(forms.Form):
return self.cleaned_data return self.cleaned_data
def check_for_test_cookie(self): def check_for_test_cookie(self):
warnings.warn('check_for_test_cookie is deprecated; ensure your login ' warnings.warn(
'view is CSRF-protected.', DeprecationWarning) 'check_for_test_cookie is deprecated; ensure your login view '
'is CSRF-protected.', DeprecationWarning
)
def get_user_id(self): def get_user_id(self):
if self.user_cache: if self.user_cache:

View File

@@ -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="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="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>
</div> </div>
{% endblock content_plain %} {% endblock content_plain %}

View File

@@ -8,8 +8,7 @@ from django.urls import reverse
from mayan.apps.common.tests import GenericViewTestCase from mayan.apps.common.tests import GenericViewTestCase
from mayan.apps.smart_settings.classes import Namespace from mayan.apps.smart_settings.classes import Namespace
from mayan.apps.user_management.tests.literals import ( from mayan.apps.user_management.tests.literals import (
TEST_ADMIN_EMAIL, TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, TEST_CASE_USER_EMAIL, TEST_CASE_USER_PASSWORD, TEST_CASE_USER_USERNAME,
TEST_USER_PASSWORD_EDITED
) )
from ..settings import setting_maximum_session_length from ..settings import setting_maximum_session_length
@@ -19,11 +18,12 @@ from .literals import TEST_EMAIL_AUTHENTICATION_BACKEND
class UserLoginTestCase(GenericViewTestCase): 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( authenticated_url = '{}?next={}'.format(
reverse(settings.LOGIN_URL), reverse(viewname='documents:document_list') reverse(settings.LOGIN_URL), reverse(viewname='documents:document_list')
) )
auto_login_user = False
def setUp(self): def setUp(self):
super(UserLoginTestCase, self).setUp() super(UserLoginTestCase, self).setUp()
@@ -42,7 +42,7 @@ class UserLoginTestCase(GenericViewTestCase):
@override_settings(AUTHENTICATION_LOGIN_METHOD='username') @override_settings(AUTHENTICATION_LOGIN_METHOD='username')
def test_username_login(self): def test_username_login(self):
logged_in = self.login( 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) self.assertTrue(logged_in)
response = self._request_authenticated_view() response = self._request_authenticated_view()
@@ -53,12 +53,12 @@ class UserLoginTestCase(GenericViewTestCase):
def test_email_login(self): def test_email_login(self):
with self.settings(AUTHENTICATION_BACKENDS=(TEST_EMAIL_AUTHENTICATION_BACKEND,)): with self.settings(AUTHENTICATION_BACKENDS=(TEST_EMAIL_AUTHENTICATION_BACKEND,)):
logged_in = self.login( 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) self.assertFalse(logged_in)
logged_in = self.login( 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) self.assertTrue(logged_in)
@@ -75,8 +75,8 @@ class UserLoginTestCase(GenericViewTestCase):
response = self.post( response = self.post(
viewname=settings.LOGIN_URL, data={ viewname=settings.LOGIN_URL, data={
'username': TEST_ADMIN_USERNAME, 'username': TEST_CASE_USER_USERNAME,
'password': TEST_ADMIN_PASSWORD 'password': TEST_CASE_USER_PASSWORD
} }
) )
response = self._request_authenticated_view() response = self._request_authenticated_view()
@@ -93,7 +93,7 @@ class UserLoginTestCase(GenericViewTestCase):
response = self.post( response = self.post(
viewname=settings.LOGIN_URL, data={ 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 }, follow=True
) )
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
@@ -106,8 +106,8 @@ class UserLoginTestCase(GenericViewTestCase):
def test_username_remember_me(self): def test_username_remember_me(self):
response = self.post( response = self.post(
viewname=settings.LOGIN_URL, data={ viewname=settings.LOGIN_URL, data={
'username': TEST_ADMIN_USERNAME, 'username': TEST_CASE_USER_USERNAME,
'password': TEST_ADMIN_PASSWORD, 'password': TEST_CASE_USER_PASSWORD,
'remember_me': True 'remember_me': True
}, follow=True }, follow=True
) )
@@ -125,8 +125,8 @@ class UserLoginTestCase(GenericViewTestCase):
def test_username_dont_remember_me(self): def test_username_dont_remember_me(self):
response = self.post( response = self.post(
viewname=settings.LOGIN_URL, data={ viewname=settings.LOGIN_URL, data={
'username': TEST_ADMIN_USERNAME, 'username': TEST_CASE_USER_USERNAME,
'password': TEST_ADMIN_PASSWORD, 'password': TEST_CASE_USER_PASSWORD,
'remember_me': False 'remember_me': False
}, follow=True }, follow=True
) )
@@ -141,8 +141,8 @@ class UserLoginTestCase(GenericViewTestCase):
with self.settings(AUTHENTICATION_BACKENDS=(TEST_EMAIL_AUTHENTICATION_BACKEND,)): with self.settings(AUTHENTICATION_BACKENDS=(TEST_EMAIL_AUTHENTICATION_BACKEND,)):
response = self.post( response = self.post(
viewname=settings.LOGIN_URL, data={ viewname=settings.LOGIN_URL, data={
'email': TEST_ADMIN_EMAIL, 'email': TEST_CASE_USER_EMAIL,
'password': TEST_ADMIN_PASSWORD, 'password': TEST_CASE_USER_PASSWORD,
'remember_me': True 'remember_me': True
}, follow=True }, follow=True
) )
@@ -161,8 +161,8 @@ class UserLoginTestCase(GenericViewTestCase):
with self.settings(AUTHENTICATION_BACKENDS=(TEST_EMAIL_AUTHENTICATION_BACKEND,)): with self.settings(AUTHENTICATION_BACKENDS=(TEST_EMAIL_AUTHENTICATION_BACKEND,)):
response = self.post( response = self.post(
viewname=settings.LOGIN_URL, data={ viewname=settings.LOGIN_URL, data={
'email': TEST_ADMIN_EMAIL, 'email': TEST_CASE_USER_EMAIL,
'password': TEST_ADMIN_PASSWORD, 'password': TEST_CASE_USER_PASSWORD,
'remember_me': False 'remember_me': False
} }
) )
@@ -176,7 +176,7 @@ class UserLoginTestCase(GenericViewTestCase):
def test_password_reset(self): def test_password_reset(self):
response = self.post( response = self.post(
viewname='authentication:password_reset_view', data={ 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( response = self.post(
viewname='authentication:password_reset_confirm_view', viewname='authentication:password_reset_confirm_view',
args=uid_token[-3:-1], data={ args=uid_token[-3:-1], data={
'new_password1': TEST_USER_PASSWORD_EDITED, 'new_password1': TEST_CASE_USER_PASSWORD,
'new_password2': TEST_USER_PASSWORD_EDITED, 'new_password2': TEST_CASE_USER_PASSWORD,
} }
) )
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.login( 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() response = self._request_authenticated_view()
@@ -209,8 +209,8 @@ class UserLoginTestCase(GenericViewTestCase):
path='{}?next={}'.format( path='{}?next={}'.format(
reverse(settings.LOGIN_URL), TEST_REDIRECT_URL reverse(settings.LOGIN_URL), TEST_REDIRECT_URL
), data={ ), data={
'username': TEST_ADMIN_USERNAME, 'username': TEST_CASE_USER_USERNAME,
'password': TEST_ADMIN_PASSWORD, 'password': TEST_CASE_USER_PASSWORD,
'remember_me': False 'remember_me': False
}, follow=True }, follow=True
) )

View File

@@ -1,43 +1,44 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf import settings
from django.conf.urls import url from django.conf.urls import url
from django.contrib.auth.views import logout
from .views import ( from .views import (
login_view, password_change_done, password_change_view, MayanLoginView, MayanLogoutView, MayanPasswordChangeDoneView,
password_reset_complete_view, password_reset_confirm_view, MayanPasswordChangeView, MayanPasswordResetCompleteView,
password_reset_done_view, password_reset_view MayanPasswordResetConfirmView, MayanPasswordResetDoneView,
MayanPasswordResetView
) )
urlpatterns = [ urlpatterns = [
url(regex=r'^login/$', name='login_view', view=login_view), url(regex=r'^login/$', name='login_view', view=MayanLoginView.as_view()),
url( url(
regex=r'^logout/$', kwargs={'next_page': settings.LOGIN_REDIRECT_URL}, regex=r'^logout/$', name='logout_view', view=MayanLogoutView.as_view()
name='logout_view', view=logout
), ),
url( url(
regex=r'^password/change/$', name='password_change_view', regex=r'^password/change/$', name='password_change_view',
view=password_change_view view=MayanPasswordChangeView.as_view()
), ),
url( url(
regex=r'^password/change/done/$', name='password_change_done', regex=r'^password/change/done/$', name='password_change_done',
view=password_change_done view=MayanPasswordChangeDoneView.as_view()
), ),
url( url(
regex=r'^password/reset/$', name='password_reset_view', regex=r'^password/reset/$', name='password_reset_view',
view=password_reset_view view=MayanPasswordResetView.as_view()
), ),
url( 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})/$', 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( url(
regex=r'^password/reset/complete/$', 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( url(
regex=r'^password/reset/done/$', name='password_reset_done_view', regex=r'^password/reset/done/$', name='password_reset_done_view',
view=password_reset_done_view view=MayanPasswordResetDoneView.as_view()
), ),
] ]

View File

@@ -1,19 +1,17 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.views import ( from django.contrib.auth.views import (
login, password_change, password_reset, password_reset_complete, LoginView, LogoutView, PasswordChangeDoneView, PasswordChangeView,
password_reset_confirm, password_reset_done PasswordResetCompleteView, PasswordResetConfirmView, PasswordResetDoneView,
PasswordResetView
) )
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.shortcuts import redirect, resolve_url from django.shortcuts import redirect
from django.urls import reverse from django.urls import reverse, reverse_lazy
from django.utils.http import is_safe_url
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from stronghold.decorators import public from stronghold.views import StrongholdPublicMixin
import mayan import mayan
from mayan.apps.common.settings import ( 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 from .settings import setting_login_method, setting_maximum_session_length
@public class MayanLoginView(StrongholdPublicMixin, LoginView):
def login_view(request): extra_context = {
""" 'appearance_type': 'plain'
Control how the use is to be authenticated, options are 'email' and }
'username' template_name = 'authentication/login.html'
""" redirect_authenticated_user = True
success_url_allowed_hosts = set()
kwargs = {'template_name': 'authentication/login.html'}
if setting_login_method.value == 'email': def form_valid(self, form):
kwargs['authentication_form'] = EmailAuthenticationForm result = super(MayanLoginView, self).form_valid(form=form)
else: remember_me = form.cleaned_data.get('remember_me')
kwargs['authentication_form'] = UsernameAuthenticationForm
allowed_hosts = {request.get_host()} # remember_me values:
allowed_hosts.update(success_url_allowed_hosts) # 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( if remember_me is True:
REDIRECT_FIELD_NAME, request.GET.get(REDIRECT_FIELD_NAME, '') self.request.session.set_expiry(
) setting_maximum_session_length.value
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.'
) )
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 redirect(to='common:current_user_details')
return password_change(
request, extra_context=extra_context,
template_name='appearance/generic_form.html',
post_change_redirect=reverse(viewname='authentication:password_change_done'),
)
def password_change_done(request): class MayanPasswordChangeView(PasswordChangeView):
""" extra_context = {'title': _('Current user password change')}
View called when the new user password has been accepted success_url = reverse_lazy(viewname='authentication:password_change_done')
""" template_name = 'appearance/generic_form.html'
messages.success(
request, _('Your password has been successfully changed.') def dispatch(self, *args, **kwargs):
) if self.request.user.user_options.block_password_change:
return redirect('common:current_user_details') 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 class MayanPasswordResetCompleteView(StrongholdPublicMixin, PasswordResetCompleteView):
def password_reset_complete_view(request):
extra_context = { extra_context = {
'appearance_type': 'plain' 'appearance_type': 'plain'
} }
template_name = 'authentication/password_reset_complete.html'
return password_reset_complete(
request=request, extra_context=extra_context,
template_name='authentication/password_reset_complete.html'
)
@public class MayanPasswordResetConfirmView(StrongholdPublicMixin, PasswordResetConfirmView):
def password_reset_confirm_view(request, uidb64=None, token=None):
extra_context = { extra_context = {
'appearance_type': 'plain' 'appearance_type': 'plain'
} }
success_url = reverse_lazy(
return password_reset_confirm( viewname='authentication:password_reset_complete_view'
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
) )
template_name = 'authentication/password_reset_confirm.html'
@public class MayanPasswordResetDoneView(StrongholdPublicMixin, PasswordResetDoneView):
def password_reset_done_view(request):
extra_context = { extra_context = {
'appearance_type': 'plain' 'appearance_type': 'plain'
} }
template_name = 'authentication/password_reset_done.html'
return password_reset_done(
request=request, extra_context=extra_context,
template_name='authentication/password_reset_done.html'
)
@public class MayanPasswordResetView(StrongholdPublicMixin, PasswordResetView):
def password_reset_view(request): email_template_name = 'authentication/password_reset_email.html'
extra_context = { extra_context = {
'appearance_type': 'plain' 'appearance_type': 'plain'
} }
extra_email_context = {
return password_reset( 'project_copyright': mayan.__copyright__,
request=request, extra_context=extra_context, 'project_license': mayan.__license__,
email_template_name='authentication/password_reset_email.html', 'project_title': setting_project_title.value,
extra_email_context={ 'project_website': setting_project_url.value
'project_title': setting_project_title.value, }
'project_website': setting_project_url.value, subject_template_name = 'authentication/password_reset_subject.txt'
'project_copyright': mayan.__copyright__, success_url = reverse_lazy(
'project_license': mayan.__license__, viewname='authentication:password_reset_done_view'
}, subject_template_name='authentication/password_reset_subject.txt',
template_name='authentication/password_reset_form.html',
post_reset_redirect=reverse(
'authentication:password_reset_done_view'
)
) )
template_name = 'authentication/password_reset_form.html'

View File

@@ -12,8 +12,10 @@ from .literals import TEST_FIRST_TIME_LOGIN_TEXT, TEST_MOCK_VIEW_TEXT
class AutoAdminViewCase(GenericViewTestCase): class AutoAdminViewCase(GenericViewTestCase):
auto_create_group = False auto_create_group = False
auto_create_users = False auto_create_users = False
auto_login_user = False
def setUp(self): def setUp(self):
super(AutoAdminViewCase, self).setUp()
with mute_stdout(): with mute_stdout():
AutoAdminSingleton.objects.create_autoadmin() AutoAdminSingleton.objects.create_autoadmin()

View File

@@ -140,7 +140,7 @@ class APICabinetDocumentListView(generics.ListCreateAPIView):
def get_queryset(self): def get_queryset(self):
cabinet = self.get_cabinet() cabinet = self.get_cabinet()
return AccessControlList.objects.filter_by_access( return AccessControlList.objects.restrict_queryset(
permission_document_view, self.request.user, permission_document_view, self.request.user,
queryset=cabinet.documents.all() queryset=cabinet.documents.all()
) )

View File

@@ -9,7 +9,7 @@ from mayan.apps.acls.permissions import (
) )
from mayan.apps.common import ( from mayan.apps.common import (
MayanAppConfig, menu_facet, menu_main, menu_multi_item, menu_object, MayanAppConfig, menu_facet, menu_main, menu_multi_item, menu_object,
menu_sidebar menu_secondary
) )
from mayan.apps.common.classes import ModelAttribute from mayan.apps.common.classes import ModelAttribute
from mayan.apps.documents.search import document_page_search, document_search from mayan.apps.documents.search import document_page_search, document_search
@@ -75,9 +75,9 @@ class CabinetsApp(MayanAppConfig):
permission_cabinet_remove_document permission_cabinet_remove_document
) )
) )
ModelPermission.register_inheritance( #ModelPermission.register_inheritance(
model=Cabinet, related='get_root', # model=Cabinet, related='get_root',
) #)
SourceColumn( SourceColumn(
func=lambda context: widget_document_cabinets( func=lambda context: widget_document_cabinets(
@@ -123,7 +123,7 @@ class CabinetsApp(MayanAppConfig):
link_cabinet_delete link_cabinet_delete
), sources=(Cabinet,) ), sources=(Cabinet,)
) )
menu_sidebar.bind_links( menu_secondary.bind_links(
links=(link_document_cabinet_add, link_document_cabinet_remove), links=(link_document_cabinet_add, link_document_cabinet_remove),
sources=( sources=(
'cabinets:document_cabinet_list', 'cabinets:document_cabinet_list',

View File

@@ -6,7 +6,9 @@ from mayan.apps.common.forms import FilteredSelectionForm
class CabinetListForm(FilteredSelectionForm): class CabinetListForm(FilteredSelectionForm):
_field_name = 'cabinets' class Meta:
_label = _('Cabinets') allow_multiple = True
_widget_attributes = {'class': 'select2'} field_name = 'cabinets'
_allow_multiple = True label = _('Cabinets')
required = False
widget_attributes = {'class': 'select2'}

View File

@@ -24,17 +24,17 @@ from .permissions import (
link_document_cabinet_list = Link( link_document_cabinet_list = Link(
args='resolved_object.pk', icon_class=icon_cabinet_list, args='resolved_object.pk', icon_class=icon_cabinet_list,
permissions=(permission_document_view,), permission=permission_document_view, text=_('Cabinets'),
text=_('Cabinets'), view='cabinets:document_cabinet_list', view='cabinets:document_cabinet_list',
) )
link_document_cabinet_remove = Link( link_document_cabinet_remove = Link(
args='resolved_object.pk', icon_class=icon_document_cabinet_remove, 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' text=_('Remove from cabinets'), view='cabinets:document_cabinet_remove'
) )
link_document_cabinet_add = Link( link_document_cabinet_add = Link(
args='object.pk', icon_class=icon_document_cabinet_add, 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', view='cabinets:document_cabinet_add',
) )
link_document_multiple_cabinet_add = Link( link_document_multiple_cabinet_add = Link(
@@ -61,21 +61,21 @@ link_custom_acl_list.condition = cabinet_is_root
link_cabinet_child_add = Link( link_cabinet_child_add = Link(
args='object.pk', icon_class=icon_cabinet_child_add, 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' view='cabinets:cabinet_child_add'
) )
link_cabinet_create = Link( 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' text=_('Create cabinet'), view='cabinets:cabinet_create'
) )
link_cabinet_delete = Link( link_cabinet_delete = Link(
args='object.pk', icon_class=icon_cabinet_delete, 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' text=_('Delete'), view='cabinets:cabinet_delete'
) )
link_cabinet_edit = Link( link_cabinet_edit = Link(
args='object.pk', icon_class=icon_cabinet_edit, args='object.pk', icon_class=icon_cabinet_edit,
permissions=(permission_cabinet_edit,), text=_('Edit'), permission=permission_cabinet_edit, text=_('Edit'),
view='cabinets:cabinet_edit' view='cabinets:cabinet_edit'
) )
link_cabinet_list = Link( link_cabinet_list = Link(
@@ -87,6 +87,6 @@ link_cabinet_list = Link(
) )
link_cabinet_view = Link( link_cabinet_view = Link(
args='object.pk', icon_class=icon_cabinet_view, args='object.pk', icon_class=icon_cabinet_view,
permissions=(permission_cabinet_view,), text=_('Details'), permission=permission_cabinet_view, text=_('Details'),
view='cabinets:cabinet_view' view='cabinets:cabinet_view'
) )

View File

@@ -73,7 +73,7 @@ class Cabinet(MPTTModel):
Provide a queryset of the documents in a cabinet. The queryset is Provide a queryset of the documents in a cabinet. The queryset is
filtered by access. filtered by access.
""" """
return AccessControlList.objects.filter_by_access( return AccessControlList.objects.restrict_queryset(
permission=permission_document_view, queryset=self.documents, permission=permission_document_view, queryset=self.documents,
user=user user=user
) )

View File

@@ -9,7 +9,7 @@ from .permissions import permission_cabinet_view
cabinet_search = SearchModel( cabinet_search = SearchModel(
app_label='cabinets', model_name='Cabinet', app_label='cabinets', model_name='Cabinet',
permission=permission_cabinet_view, permission=permission_cabinet_view,
serializer_string='mayan.apps.cabinets.serializers.CabinetSerializer' serializer_path='mayan.apps.cabinets.serializers.CabinetSerializer'
) )
cabinet_search.add_model_field( cabinet_search.add_model_field(

View File

@@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext from django.utils.translation import ungettext
from mayan.apps.acls.models import AccessControlList from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.views import ( from mayan.apps.common.generics import (
MultipleObjectFormActionView, SingleObjectCreateView, MultipleObjectFormActionView, SingleObjectCreateView,
SingleObjectDeleteView, SingleObjectEditView, SingleObjectListView SingleObjectDeleteView, SingleObjectEditView, SingleObjectListView
) )
@@ -64,7 +64,7 @@ class CabinetChildAddView(SingleObjectCreateView):
cabinet = super(CabinetChildAddView, self).get_object(*args, **kwargs) cabinet = super(CabinetChildAddView, self).get_object(*args, **kwargs)
AccessControlList.objects.check_access( 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 user=self.request.user, raise_404=True
) )
@@ -96,7 +96,7 @@ class CabinetDetailView(DocumentListView):
template_name = 'cabinets/cabinet_details.html' template_name = 'cabinets/cabinet_details.html'
def get_document_queryset(self): def get_document_queryset(self):
queryset = AccessControlList.objects.filter_by_access( queryset = AccessControlList.objects.restrict_queryset(
permission=permission_document_view, permission=permission_document_view,
queryset=self.get_object().documents.all(), queryset=self.get_object().documents.all(),
user=self.request.user user=self.request.user
@@ -146,7 +146,7 @@ class CabinetDetailView(DocumentListView):
permission_object = cabinet.get_root() permission_object = cabinet.get_root()
AccessControlList.objects.check_access( 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 user=self.request.user, raise_404=True
) )
@@ -187,7 +187,7 @@ class CabinetListView(SingleObjectListView):
'no_results_title': _('No cabinets available'), '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 # Add explicit ordering of root nodes since the queryset returned
# is not affected by the model's order Meta option. # is not affected by the model's order Meta option.
return Cabinet.objects.root_nodes().order_by('label') return Cabinet.objects.root_nodes().order_by('label')
@@ -200,8 +200,8 @@ class DocumentCabinetListView(CabinetListView):
) )
AccessControlList.objects.check_access( AccessControlList.objects.check_access(
permissions=permission_document_view, user=request.user, obj=self.document, permission=permission_document_view,
obj=self.document, raise_404=True user=request.user, raise_404=True
) )
return super(DocumentCabinetListView, self).dispatch( return super(DocumentCabinetListView, self).dispatch(
@@ -227,7 +227,7 @@ class DocumentCabinetListView(CabinetListView):
'title': _('Cabinets containing document: %s') % self.document, 'title': _('Cabinets containing document: %s') % self.document,
} }
def get_object_list(self): def get_source_queryset(self):
return self.document.get_cabinets().all() return self.document.get_cabinets().all()
@@ -236,7 +236,7 @@ class DocumentAddToCabinetView(MultipleObjectFormActionView):
model = Document model = Document
object_permission = permission_cabinet_add_document object_permission = permission_cabinet_add_document
pk_url_kwarg = 'document_pk' pk_url_kwarg = 'document_pk'
success_message = _( success_message_singular = _(
'Add to cabinet request performed on %(count)d document' 'Add to cabinet request performed on %(count)d document'
) )
success_message_plural = _( success_message_plural = _(
@@ -295,7 +295,7 @@ class DocumentAddToCabinetView(MultipleObjectFormActionView):
for cabinet in form.cleaned_data['cabinets']: for cabinet in form.cleaned_data['cabinets']:
AccessControlList.objects.check_access( 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 user=self.request.user, raise_404=True
) )
if cabinet in cabinet_membership: if cabinet in cabinet_membership:
@@ -326,7 +326,7 @@ class DocumentRemoveFromCabinetView(MultipleObjectFormActionView):
model = Document model = Document
object_permission = permission_cabinet_remove_document object_permission = permission_cabinet_remove_document
pk_url_kwarg = 'document_pk' pk_url_kwarg = 'document_pk'
success_message = _( success_message_singular = _(
'Remove from cabinet request performed on %(count)d document' 'Remove from cabinet request performed on %(count)d document'
) )
success_message_plural = _( success_message_plural = _(
@@ -383,7 +383,7 @@ class DocumentRemoveFromCabinetView(MultipleObjectFormActionView):
for cabinet in form.cleaned_data['cabinets']: for cabinet in form.cleaned_data['cabinets']:
AccessControlList.objects.check_access( 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 user=self.request.user, raise_404=True
) )

View File

@@ -42,8 +42,9 @@ def widget_document_cabinets(document, user):
app_label='acls', model_name='AccessControlList' app_label='acls', model_name='AccessControlList'
) )
cabinets = AccessControlList.objects.filter_by_access( cabinets = AccessControlList.objects.restrict_queryset(
permission_cabinet_view, user, queryset=document.get_cabinets().all() queryset=document.get_cabinets().all(),
permission=permission_cabinet_view, user=user
) )
return format_html_join( return format_html_join(

View File

@@ -9,6 +9,7 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.sources.wizards import WizardStep from mayan.apps.sources.wizards import WizardStep
from .forms import CabinetListForm from .forms import CabinetListForm
from .models import Cabinet
from .permissions import permission_cabinet_add_document from .permissions import permission_cabinet_add_document
@@ -41,6 +42,7 @@ class WizardStepCabinets(WizardStep):
return { return {
'help_text': _('Cabinets to which the document will be added.'), 'help_text': _('Cabinets to which the document will be added.'),
'permission': permission_cabinet_add_document, 'permission': permission_cabinet_add_document,
'queryset': Cabinet.objects.all(),
'user': wizard.request.user 'user': wizard.request.user
} }

View File

@@ -7,7 +7,7 @@ from mayan.apps.documents.permissions import permission_document_view
from .models import DocumentCheckout from .models import DocumentCheckout
from .permissions import ( from .permissions import (
permission_document_checkin, permission_document_checkin_override, permission_document_check_in, permission_document_check_in_override,
permission_document_checkout_detail_view permission_document_checkout_detail_view
) )
from .serializers import ( from .serializers import (
@@ -33,11 +33,11 @@ class APICheckedoutDocumentListView(generics.ListCreateAPIView):
return DocumentCheckoutSerializer return DocumentCheckoutSerializer
def get_queryset(self): 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, permission=permission_document_view, user=self.request.user,
queryset=DocumentCheckout.objects.checked_out_documents() 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, permission=permission_document_checkout_detail_view, user=self.request.user,
queryset=filtered_documents queryset=filtered_documents
) )
@@ -56,11 +56,11 @@ class APICheckedoutDocumentView(generics.RetrieveDestroyAPIView):
def get_queryset(self): def get_queryset(self):
if self.request.method == 'GET': 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, permission=permission_document_view, user=self.request.user,
queryset=DocumentCheckout.objects.checked_out_documents() 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, permission=permission_document_checkout_detail_view, user=self.request.user,
queryset=filtered_documents queryset=filtered_documents
) )
@@ -78,12 +78,12 @@ class APICheckedoutDocumentView(generics.RetrieveDestroyAPIView):
if document.checkout_info().user == request.user: if document.checkout_info().user == request.user:
AccessControlList.objects.check_access( AccessControlList.objects.check_access(
permissions=permission_document_checkin, user=request.user, permissions=permission_document_check_in, user=request.user,
obj=document obj=document
) )
else: else:
AccessControlList.objects.check_access( AccessControlList.objects.check_access(
permissions=permission_document_checkin_override, permissions=permission_document_check_in_override,
user=request.user, obj=document user=request.user, obj=document
) )

View File

@@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls import ModelPermission from mayan.apps.acls import ModelPermission
from mayan.apps.common import ( 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.dashboards.dashboards import dashboard_main
from mayan.apps.events import ModelEventType from mayan.apps.events import ModelEventType
@@ -22,9 +22,11 @@ from .events import (
event_document_check_out, event_document_forceful_check_in event_document_check_out, event_document_forceful_check_in
) )
from .handlers import handler_check_new_version_creation from .handlers import handler_check_new_version_creation
from .hooks import hook_is_new_version_allowed
from .links import ( from .links import (
link_checkin_document, link_checkout_document, link_checkout_info, link_document_check_in, link_document_checkout, link_document_checkout_info,
link_checkout_list link_document_checkout_list, link_document_multiple_check_in,
link_document_multiple_checkout
) )
from .literals import CHECK_EXPIRED_CHECK_OUTS_INTERVAL from .literals import CHECK_EXPIRED_CHECK_OUTS_INTERVAL
from .methods import ( from .methods import (
@@ -32,7 +34,7 @@ from .methods import (
method_is_checked_out method_is_checked_out
) )
from .permissions import ( 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 permission_document_checkout, permission_document_checkout_detail_view
) )
from .queues import * # NOQA from .queues import * # NOQA
@@ -69,6 +71,10 @@ class CheckoutsApp(MayanAppConfig):
name='is_checked_out', value=method_is_checked_out name='is_checked_out', value=method_is_checked_out
) )
DocumentVersion.register_pre_save_hook(
func=hook_is_new_version_allowed
)
ModelEventType.register( ModelEventType.register(
model=Document, event_types=( model=Document, event_types=(
event_document_auto_check_in, event_document_check_in, event_document_auto_check_in, event_document_check_in,
@@ -79,8 +85,8 @@ class CheckoutsApp(MayanAppConfig):
ModelPermission.register( ModelPermission.register(
model=Document, permissions=( model=Document, permissions=(
permission_document_checkout, permission_document_checkout,
permission_document_checkin, permission_document_check_in,
permission_document_checkin_override, permission_document_check_in_override,
permission_document_checkout_detail_view permission_document_checkout_detail_view
) )
) )
@@ -115,13 +121,18 @@ class CheckoutsApp(MayanAppConfig):
widget=DashboardWidgetTotalCheckouts, order=-1 widget=DashboardWidgetTotalCheckouts, order=-1
) )
menu_facet.bind_links(links=(link_checkout_info,), sources=(Document,)) menu_facet.bind_links(links=(link_document_checkout_info,), sources=(Document,))
menu_main.bind_links(links=(link_checkout_list,), position=98) menu_main.bind_links(links=(link_document_checkout_list,), position=98)
menu_sidebar.bind_links( menu_multi_item.bind_links(
links=(link_checkout_document, link_checkin_document), 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=( sources=(
'checkouts:checkout_info', 'checkouts:checkout_document', 'checkouts:document_checkout_info', 'checkouts:document_checkout',
'checkouts:checkin_document' 'checkouts:document_check_in'
) )
) )

View File

@@ -14,7 +14,7 @@ from .permissions import permission_document_checkout_detail_view
class DashboardWidgetTotalCheckouts(DashboardWidgetNumeric): class DashboardWidgetTotalCheckouts(DashboardWidgetNumeric):
icon_class = icon_dashboard_checkouts icon_class = icon_dashboard_checkouts
label = _('Checkedout documents') label = _('Checkedout documents')
link = reverse_lazy('checkouts:checkout_list') link = reverse_lazy(viewname='checkouts:document_checkout_list')
def render(self, request): def render(self, request):
AccessControlList = apps.get_model( AccessControlList = apps.get_model(
@@ -23,14 +23,16 @@ class DashboardWidgetTotalCheckouts(DashboardWidgetNumeric):
DocumentCheckout = apps.get_model( DocumentCheckout = apps.get_model(
app_label='checkouts', model_name='DocumentCheckout' app_label='checkouts', model_name='DocumentCheckout'
) )
queryset = AccessControlList.objects.filter_by_access( queryset = AccessControlList.objects.restrict_queryset(
permission=permission_document_checkout_detail_view, permission=permission_document_checkout_detail_view,
queryset=DocumentCheckout.objects.checked_out_documents(),
user=request.user, user=request.user,
queryset=DocumentCheckout.objects.checked_out_documents()
) )
queryset = AccessControlList.objects.filter_by_access( queryset = AccessControlList.objects.restrict_queryset(
permission=permission_document_view, user=request.user, permission=permission_document_view, queryset=queryset,
queryset=queryset user=request.user
) )
self.count = queryset.count() self.count = queryset.count()
return super(DashboardWidgetTotalCheckouts, self).render(request) return super(DashboardWidgetTotalCheckouts, self).render(
request=request
)

View File

@@ -4,19 +4,19 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.events import EventTypeNamespace 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( 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( 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( 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( 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'
) )

View File

@@ -13,6 +13,6 @@ def handler_check_new_version_creation(sender, instance, **kwargs):
app_label='checkouts', model_name='NewVersionBlock' 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. # Block only new versions (no pk), not existing version being updated.
raise NewDocumentVersionNotAllowed raise NewDocumentVersionNotAllowed

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

View File

@@ -2,6 +2,14 @@ from __future__ import absolute_import, unicode_literals
from mayan.apps.appearance.classes import Icon 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_checkout_info = Icon(driver_name='fontawesome', symbol='shopping-cart')
icon_dashboard_checkouts = Icon( icon_dashboard_checkouts = Icon(
driver_name='fontawesome', symbol='shopping-cart' driver_name='fontawesome', symbol='shopping-cart'

View File

@@ -4,10 +4,12 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.navigation import Link 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 ( from .permissions import (
permission_document_checkin, permission_document_checkin_override, permission_document_check_in, permission_document_checkout,
permission_document_checkout permission_document_checkout_detail_view
) )
@@ -27,24 +29,32 @@ def is_not_checked_out(context):
return True return True
link_checkout_list = Link( link_document_checkout_list = Link(
icon_class=icon_checkout_info, text=_('Checkouts'), icon_class=icon_checkout_info, text=_('Checkouts'),
view='checkouts:checkout_list' view='checkouts:document_checkout_list'
) )
link_checkout_document = Link( link_document_checkout = Link(
args='object.pk', condition=is_not_checked_out, condition=is_not_checked_out, icon_class=icon_checkout_document,
permissions=(permission_document_checkout,), kwargs={'document_id': 'object.pk'},
text=_('Check out document'), view='checkouts:checkout_document', permission=permission_document_checkout, text=_('Check out document'),
view='checkouts:document_checkout',
) )
link_checkin_document = Link( link_document_multiple_checkout = Link(
args='object.pk', condition=is_checked_out, permissions=( icon_class=icon_checkout_document,
permission_document_checkin, permission_document_checkin_override permission=permission_document_checkout, text=_('Check out'),
), text=_('Check in document'), view='checkouts:checkin_document', view='checkouts:document_multiple_checkout',
) )
link_checkout_info = Link( link_document_check_in = Link(
args='resolved_object.pk', icon_class=icon_checkout_info, permissions=( condition=is_checked_out, icon_class=icon_checkin_document,
permission_document_checkin, permission_document_checkin_override, kwargs={'document_id': 'object.pk'}, permission=permission_document_check_in,
permission_document_checkout text=_('Check in document'), view='checkouts:document_check_in',
), text=_('Check in/out'), view='checkouts:checkout_info', )
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',
) )

View File

@@ -2,48 +2,54 @@ from __future__ import absolute_import, unicode_literals
import logging import logging
from django.apps import apps from django.core.exceptions import PermissionDenied
from django.db import models from django.db import models, transaction
from django.utils.timezone import now 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 mayan.apps.documents.models import Document
from .events import ( from .events import (
event_document_auto_check_in, event_document_check_in, event_document_auto_check_in, event_document_check_in,
event_document_forceful_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 .literals import STATE_CHECKED_IN, STATE_CHECKED_OUT
from .permissions import permission_document_check_in_override
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class DocumentCheckoutManager(models.Manager): 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): def check_in_document(self, document, user=None):
try: try:
document_checkout = self.model.objects.get(document=document) document_checkout = self.model.objects.get(document=document)
except self.model.DoesNotExist: except self.model.DoesNotExist:
raise DocumentNotCheckedOut raise DocumentNotCheckedOut(
_('Document not checked out.')
)
else: else:
if user: with transaction.atomic():
if self.get_document_checkout_info(document).user != user: if user:
event_document_forceful_check_in.commit( if self.get_document_checkout_info(document=document).user != user:
actor=user, target=document 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: else:
event_document_check_in.commit(actor=user, target=document) event_document_auto_check_in.commit(target=document)
else:
event_document_auto_check_in.commit(target=document)
document_checkout.delete() document_checkout.delete()
def check_in_expired_check_outs(self): def check_in_expired_check_outs(self):
for document in self.get_expired_check_outs(): 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): def checkout_document(self, document, expiration_datetime, user, block_new_version=True):
return self.create( return self.create(
document=document, expiration_datetime=expiration_datetime, block_new_version=block_new_version, document=document,
user=user, block_new_version=block_new_version expiration_datetime=expiration_datetime, user=user
) )
def checked_out_documents(self): def checked_out_documents(self):
return Document.objects.filter( return Document.objects.filter(
pk__in=self.model.objects.all().values_list( pk__in=self.model.objects.values('document__id')
'document__pk', flat=True
)
) )
def get_by_natural_key(self, document_natural_key): def get_by_natural_key(self, document_natural_key):
Document = apps.get_model(
app_label='documents', model_name='Document'
)
try: try:
document = Document.objects.get_by_natural_key(document_natural_key) document = Document.objects.get_by_natural_key(document_natural_key)
except Document.DoesNotExist: except Document.DoesNotExist:
@@ -80,22 +81,22 @@ class DocumentCheckoutManager(models.Manager):
raise DocumentNotCheckedOut raise DocumentNotCheckedOut
def get_document_checkout_state(self, document): 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 return STATE_CHECKED_OUT
else: else:
return STATE_CHECKED_IN return STATE_CHECKED_IN
def get_expired_check_outs(self): def get_expired_check_outs(self):
expired_list = Document.objects.filter( expired_list = Document.objects.filter(
pk__in=self.model.objects.filter( pk__in=self.filter(
expiration_datetime__lte=now() expiration_datetime__lte=now()
).values_list('document__pk', flat=True) ).values('document__id')
) )
logger.debug('expired_list: %s', expired_list) logger.debug('expired_list: %s', expired_list)
return expired_list return expired_list
def is_document_checked_out(self, document): def is_document_checked_out(self, document):
if self.model.objects.filter(document=document): if self.filter(document=document).exists():
return True return True
else: else:
return False return False
@@ -105,13 +106,7 @@ class NewVersionBlockManager(models.Manager):
def block(self, document): def block(self, document):
self.get_or_create(document=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): def get_by_natural_key(self, document_natural_key):
Document = apps.get_model(
app_label='documents', model_name='Document'
)
try: try:
document = Document.objects.get_by_natural_key(document_natural_key) document = Document.objects.get_by_natural_key(document_natural_key)
except Document.DoesNotExist: except Document.DoesNotExist:
@@ -119,5 +114,12 @@ class NewVersionBlockManager(models.Manager):
return self.get(document__pk=document.pk) 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): def unblock(self, document):
self.filter(document=document).delete() self.filter(document=document).delete()

View File

@@ -4,7 +4,7 @@ import logging
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models, transaction
from django.urls import reverse from django.urls import reverse
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from django.utils.timezone import now from django.utils.timezone import now
@@ -68,37 +68,42 @@ class DocumentCheckout(models.Model):
) )
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
# TODO: enclose in transaction with transaction.atomic():
NewVersionBlock.objects.unblock(self.document) NewVersionBlock.objects.unblock(document=self.document)
super(DocumentCheckout, self).delete(*args, **kwargs) super(DocumentCheckout, self).delete(*args, **kwargs)
def get_absolute_url(self): 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): def natural_key(self):
return self.document.natural_key() return self.document.natural_key()
natural_key.dependencies = ['documents.Document'] natural_key.dependencies = ['documents.Document']
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# TODO: enclose in transaction
new_checkout = not self.pk new_checkout = not self.pk
if not new_checkout or self.document.is_checked_out(): if not new_checkout or self.document.is_checked_out():
raise DocumentAlreadyCheckedOut raise DocumentAlreadyCheckedOut(
_('Document already checked out.')
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 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): class NewVersionBlock(models.Model):

View File

@@ -6,15 +6,15 @@ from mayan.apps.permissions import PermissionNamespace
namespace = PermissionNamespace(label=_('Document checkout'), name='checkouts') namespace = PermissionNamespace(label=_('Document checkout'), name='checkouts')
permission_document_checkin = namespace.add_permission( permission_document_check_in = namespace.add_permission(
name='checkin_document', label=_('Check in documents') label=_('Check in documents'), name='checkin_document'
) )
permission_document_checkin_override = namespace.add_permission( permission_document_check_in_override = namespace.add_permission(
name='checkin_document_override', label=_('Forcefully check in documents') label=_('Forcefully check in documents'), name='checkin_document_override'
) )
permission_document_checkout = namespace.add_permission( 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( 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'
) )

View File

@@ -5,9 +5,9 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.task_manager.classes import CeleryQueue from mayan.apps.task_manager.classes import CeleryQueue
queue_checkouts_periodic = 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( 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'
) )

View File

@@ -42,8 +42,8 @@ class NewDocumentCheckoutSerializer(serializers.ModelSerializer):
document = Document.objects.get(pk=validated_data.pop('document_pk')) document = Document.objects.get(pk=validated_data.pop('document_pk'))
AccessControlList.objects.check_access( AccessControlList.objects.check_access(
permissions=permission_document_checkout, obj=document, permissions=permission_document_checkout,
user=self.context['request'].user, obj=document user=self.context['request'].user
) )
validated_data['document'] = document validated_data['document'] = document

View File

@@ -25,7 +25,7 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
def _request_checkedout_document_view(self): def _request_checkedout_document_view(self):
return self.get( return self.get(
viewname='rest_api:checkedout-document-view', viewname='rest_api:checkedout-document-view',
args=(self.checkout.pk,) kwargs={'document_pk': self.checkout.pk}
) )
def _checkout_document(self): def _checkout_document(self):
@@ -44,7 +44,8 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
def test_checkedout_document_view_with_checkout_access(self): def test_checkedout_document_view_with_checkout_access(self):
self._checkout_document() self._checkout_document()
self.grant_access( 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() response = self._request_checkedout_document_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) 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): def test_checkedout_document_view_with_document_access(self):
self._checkout_document() self._checkout_document()
self.grant_access( self.grant_access(
permission=permission_document_view, obj=self.document obj=self.document, permission=permission_document_view
) )
response = self._request_checkedout_document_view() response = self._request_checkedout_document_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) 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): def test_checkedout_document_view_with_access(self):
self._checkout_document() self._checkout_document()
self.grant_access( self.grant_access(
permission=permission_document_view, obj=self.document obj=self.document, permission=permission_document_view
) )
self.grant_access( 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() response = self._request_checkedout_document_view()
self.assertEqual(response.status_code, status.HTTP_200_OK) 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): def _request_document_checkout_view(self):
return self.post( return self.post(
@@ -83,7 +87,9 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
self.assertEqual(DocumentCheckout.objects.count(), 0) self.assertEqual(DocumentCheckout.objects.count(), 0)
def test_document_checkout_with_access(self): 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() response = self._request_document_checkout_view()
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual( self.assertEqual(
@@ -102,7 +108,7 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
def test_checkout_list_view_with_document_access(self): def test_checkout_list_view_with_document_access(self):
self._checkout_document() self._checkout_document()
self.grant_access( self.grant_access(
permission=permission_document_view, obj=self.document obj=self.document, permission=permission_document_view
) )
response = self._request_checkout_list_view() response = self._request_checkout_list_view()
self.assertEqual(response.status_code, status.HTTP_200_OK) 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): def test_checkout_list_view_with_checkout_access(self):
self._checkout_document() self._checkout_document()
self.grant_access( 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() response = self._request_checkout_list_view()
self.assertEqual(response.status_code, status.HTTP_200_OK) 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): def test_checkout_list_view_with_access(self):
self._checkout_document() self._checkout_document()
self.grant_access( self.grant_access(
permission=permission_document_view, obj=self.document obj=self.document, permission=permission_document_view
) )
self.grant_access( 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() response = self._request_checkout_list_view()
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@@ -23,7 +23,7 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase):
DocumentCheckout.objects.checkout_document( DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime, 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()) 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): def test_checkin_in(self):
expiration_datetime = now() + datetime.timedelta(days=1) expiration_datetime = now() + datetime.timedelta(days=1)
DocumentCheckout.objects.checkout_document( DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime, 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() self.document.check_in()
@@ -72,13 +55,13 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase):
DocumentCheckout.objects.checkout_document( DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime, 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): with self.assertRaises(DocumentAlreadyCheckedOut):
DocumentCheckout.objects.checkout_document( DocumentCheckout.objects.checkout_document(
document=self.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 block_new_version=True
) )
@@ -91,7 +74,7 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase):
DocumentCheckout.objects.checkout_document( DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime, 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) time.sleep(.11)
@@ -100,18 +83,6 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase):
self.assertFalse(self.document.is_checked_out()) 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): class NewVersionBlockTestCase(DocumentTestMixin, BaseTestCase):
def test_blocking(self): def test_blocking(self):
@@ -141,3 +112,32 @@ class NewVersionBlockTestCase(DocumentTestMixin, BaseTestCase):
self.assertFalse( self.assertFalse(
NewVersionBlock.objects.is_blocked(document=self.document) 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)

View File

@@ -8,61 +8,53 @@ from django.utils.timezone import now
from mayan.apps.common.literals import TIME_DELTA_UNIT_DAYS from mayan.apps.common.literals import TIME_DELTA_UNIT_DAYS
from mayan.apps.documents.tests import GenericDocumentViewTestCase from mayan.apps.documents.tests import GenericDocumentViewTestCase
from mayan.apps.sources.links import link_upload_version 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 ..models import DocumentCheckout
from ..permissions import ( 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 permission_document_checkout, permission_document_checkout_detail_view
) )
class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase): 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): def _request_document_check_in_view(self):
return self.post( 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): def test_document_check_in_view_no_permission(self):
self.login_user() self._checkout_document()
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())
response = self._request_document_check_in_view() 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()) self.assertTrue(self.document.is_checked_out())
def test_checkin_document_view_with_access(self): def test_document_check_in_view_with_access(self):
self.login_user() self._checkout_document()
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())
self.grant_access( self.grant_access(
obj=self.document, permission=permission_document_checkin obj=self.document, permission=permission_document_check_in
) )
self.grant_access( self.grant_access(
obj=self.document, obj=self.document,
permission=permission_document_checkout_detail_view permission=permission_document_checkout_detail_view
) )
response = self._request_document_check_in_view() response = self._request_document_check_in_view()
self.assertEquals(response.status_code, 302) self.assertEquals(response.status_code, 302)
self.assertFalse(self.document.is_checked_out()) self.assertFalse(self.document.is_checked_out())
self.assertFalse( self.assertFalse(
DocumentCheckout.objects.is_document_checked_out( DocumentCheckout.objects.is_document_checked_out(
@@ -72,7 +64,8 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
def _request_document_checkout_view(self): def _request_document_checkout_view(self):
return self.post( return self.post(
viewname='checkouts:checkout_document', args=(self.document.pk,), viewname='checkouts:document_checkout',
kwargs={'document_id': self.document.pk},
data={ data={
'expiration_datetime_0': 2, 'expiration_datetime_0': 2,
'expiration_datetime_1': TIME_DELTA_UNIT_DAYS, 'expiration_datetime_1': TIME_DELTA_UNIT_DAYS,
@@ -81,14 +74,11 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
) )
def test_checkout_document_view_no_permission(self): def test_checkout_document_view_no_permission(self):
self.login_user()
response = self._request_document_checkout_view() 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()) self.assertFalse(self.document.is_checked_out())
def test_checkout_document_view_with_access(self): def test_checkout_document_view_with_access(self):
self.login_user()
self.grant_access( self.grant_access(
obj=self.document, permission=permission_document_checkout obj=self.document, permission=permission_document_checkout
) )
@@ -96,9 +86,9 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
obj=self.document, obj=self.document,
permission=permission_document_checkout_detail_view permission=permission_document_checkout_detail_view
) )
response = self._request_document_checkout_view() response = self._request_document_checkout_view()
self.assertEquals(response.status_code, 302) self.assertEquals(response.status_code, 302)
self.assertTrue(self.document.is_checked_out()) self.assertTrue(self.document.is_checked_out())
def test_document_new_version_after_checkout(self): def test_document_new_version_after_checkout(self):
@@ -111,31 +101,23 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
- Link to upload version view should not resolve - Link to upload version view should not resolve
- Upload version view should reject request - Upload version view should reject request
""" """
self.login( self.login_superuser()
username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD
)
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.assertTrue(self.document.is_checked_out())
response = self.post( response = self.post(
'sources:upload_version', args=(self.document.pk,), viewname='sources:upload_version',
kwargs={'document_id': self.document.pk},
follow=True follow=True
) )
self.assertContains( self.assertContains(
response, text='blocked from uploading', response, text='blocked from uploading',
status_code=200 status_code=200
) )
response = self.get( response = self.get(
'documents:document_version_list', args=(self.document.pk,), viewname='documents:document_version_list',
kwargs={'document_id': self.document.pk},
follow=True follow=True
) )
@@ -159,26 +141,22 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
DocumentCheckout.objects.checkout_document( DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime, 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.assertTrue(self.document.is_checked_out())
self.login( self.grant_access(
username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD obj=self.document, permission=permission_document_check_in
) )
self.grant_access(
self.role.permissions.add( obj=self.document, permission=permission_document_checkout
permission_document_checkin.stored_permission
) )
self.role.permissions.add(
permission_document_checkout.stored_permission
)
response = self.post( 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( self.assertContains(
response, text='Insufficient permissions', status_code=403 response, text='Insufficient permissions', status_code=403
) )
@@ -186,33 +164,21 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
self.assertTrue(self.document.is_checked_out()) self.assertTrue(self.document.is_checked_out())
def test_forcefull_check_in_document_view_with_permission(self): def test_forcefull_check_in_document_view_with_permission(self):
expiration_datetime = now() + datetime.timedelta(days=1) self._checkout_document()
DocumentCheckout.objects.checkout_document( self.grant_access(
document=self.document, expiration_datetime=expiration_datetime, obj=self.document, permission=permission_document_check_in
user=self.admin_user, block_new_version=True
) )
self.grant_access(
self.assertTrue(self.document.is_checked_out()) obj=self.document, permission=permission_document_check_in_override
self.login(
username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD
) )
self.grant_access(
self.role.permissions.add( obj=self.document, permission=permission_document_checkout_detail_view
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
) )
response = self.post( 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( self.assertContains(

View File

@@ -4,33 +4,45 @@ from django.conf.urls import url
from .api_views import APICheckedoutDocumentListView, APICheckedoutDocumentView from .api_views import APICheckedoutDocumentListView, APICheckedoutDocumentView
from .views import ( from .views import (
CheckoutDetailView, CheckoutDocumentView, CheckoutListView, DocumentCheckinView, DocumentCheckoutView, DocumentCheckoutDetailView,
DocumentCheckinView DocumentCheckoutListView
) )
urlpatterns = [ urlpatterns = [
url(r'^list/$', CheckoutListView.as_view(), name='checkout_list'),
url( url(
r'^(?P<pk>\d+)/check/out/$', CheckoutDocumentView.as_view(), regex=r'^documents/$', name='document_checkout_list',
name='checkout_document' view=DocumentCheckoutListView.as_view()
), ),
url( url(
r'^(?P<pk>\d+)/check/in/$', DocumentCheckinView.as_view(), regex=r'^documents/(?P<document_id>\d+)/check_in/$',
name='checkin_document' name='document_check_in', view=DocumentCheckinView.as_view()
), ),
url( url(
r'^(?P<pk>\d+)/check/info/$', CheckoutDetailView.as_view(), regex=r'^documents/multiple/check_in/$',
name='checkout_info' 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 = [ api_urls = [
url( url(
r'^checkouts/$', APICheckedoutDocumentListView.as_view(), regex=r'^checkouts/$', name='checkout-document-list',
name='checkout-document-list' view=APICheckedoutDocumentListView.as_view()
), ),
url( url(
r'^checkouts/(?P<pk>[0-9]+)/checkout_info/$', APICheckedoutDocumentView.as_view(), regex=r'^checkouts/(?P<document_id>\d+)/checkout_info/$',
name='checkedout-document-view' name='checkedout-document-view',
view=APICheckedoutDocumentView.as_view()
), ),
] ]

View File

@@ -1,85 +1,154 @@
from __future__ import absolute_import, unicode_literals 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.shortcuts import get_object_or_404
from django.urls import reverse 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.acls.models import AccessControlList
from mayan.apps.common.generics import ( from mayan.apps.common.generics import (
ConfirmView, SingleObjectCreateView, SingleObjectDetailView MultipleObjectConfirmActionView, MultipleObjectFormActionView,
SingleObjectDetailView
) )
from mayan.apps.common.utils import encapsulate from mayan.apps.common.utils import encapsulate
from mayan.apps.documents.models import Document from mayan.apps.documents.models import Document
from mayan.apps.documents.views import DocumentListView from mayan.apps.documents.views import DocumentListView
from .exceptions import DocumentAlreadyCheckedOut, DocumentNotCheckedOut
from .forms import DocumentCheckoutDefailForm, DocumentCheckoutForm from .forms import DocumentCheckoutDefailForm, DocumentCheckoutForm
from .icons import icon_checkout_info from .icons import icon_checkout_info
from .models import DocumentCheckout from .models import DocumentCheckout
from .permissions import ( from .permissions import (
permission_document_checkin, permission_document_checkin_override, permission_document_check_in, permission_document_checkout,
permission_document_checkout, permission_document_checkout_detail_view permission_document_checkout_detail_view
) )
class CheckoutDocumentView(SingleObjectCreateView): class DocumentCheckinView(MultipleObjectConfirmActionView):
form_class = DocumentCheckoutForm 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): def get_extra_context(self):
self.document = get_object_or_404(klass=Document, pk=self.kwargs['pk']) queryset = self.get_object_list()
AccessControlList.objects.check_access( result = {
permissions=permission_document_checkout, user=request.user, 'title': ungettext(
obj=self.document singular='Check in %(count)d document',
) plural='Check in %(count)d documents',
number=queryset.count()
) % {
'count': queryset.count(),
}
}
return super( if queryset.count() == 1:
CheckoutDocumentView, self result.update(
).dispatch(request, *args, **kwargs) {
'object': queryset.first(),
'title': _(
'Check in document: %s'
) % queryset.first()
}
)
def form_valid(self, form): return result
try:
instance = form.save(commit=False) def get_post_object_action_url(self):
instance.user = self.request.user if self.action_count == 1:
instance.document = self.document return reverse(
instance.save() viewname='checkouts:document_checkout_info',
except DocumentAlreadyCheckedOut: kwargs={'document_id': self.action_id_list[0]}
messages.error(self.request, _('Document already checked out.'))
except Exception as exception:
messages.error(
self.request,
_('Error trying to check out document; %s') % exception
) )
else: else:
messages.success( super(DocumentCheckinView, self).get_post_action_redirect()
self.request,
_('Document "%s" checked out successfully.') % self.document 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): def get_extra_context(self):
return { return {
'object': self.document, 'object': self.get_object(),
'title': _('Check out document: %s') % self.document 'title': _(
'Check out details for document: %s'
) % self.get_object()
} }
def get_post_action_redirect(self): def get_object(self):
return reverse('checkouts:checkout_info', args=(self.document.pk,)) return get_object_or_404(klass=Document, pk=self.kwargs['document_id'])
class CheckoutListView(DocumentListView): class DocumentCheckoutListView(DocumentListView):
def get_document_queryset(self): def get_document_queryset(self):
return AccessControlList.objects.filter_by_access( return AccessControlList.objects.restrict_queryset(
permission=permission_document_checkout_detail_view, 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): def get_extra_context(self):
context = super(CheckoutListView, self).get_extra_context() context = super(DocumentCheckoutListView, self).get_extra_context()
context.update( context.update(
{ {
'extra_columns': ( 'extra_columns': (
@@ -113,76 +182,3 @@ class CheckoutListView(DocumentListView):
} }
) )
return context 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
)

View File

@@ -2,42 +2,42 @@ from __future__ import unicode_literals
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from rest_framework import generics from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from .classes import Template from .classes import Template
from .serializers import ContentTypeSerializer, TemplateSerializer 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') 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. list:
get: Returns a list of partial templates. Return a list of partial templates.
retrieve:
Return the given partial template details.
""" """
serializer_class = TemplateSerializer lookup_url_kwarg = 'template_name'
permission_classes = (IsAuthenticated,) 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): def get_queryset(self):
return Template.all(rendered=True, request=self.request) 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
)

View File

@@ -4,6 +4,8 @@ import logging
import os import os
import warnings import warnings
from datetime import timedelta from datetime import timedelta
import sys
import traceback
from kombu import Exchange, Queue from kombu import Exchange, Queue
@@ -41,6 +43,7 @@ from .settings import (
from .signals import pre_initial_setup, pre_upgrade from .signals import pre_initial_setup, pre_upgrade
from .tasks import task_delete_stale_uploads # NOQA - Force task registration from .tasks import task_delete_stale_uploads # NOQA - Force task registration
from .utils import check_for_sqlite from .utils import check_for_sqlite
from .warnings import DatabaseWarning
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -74,6 +77,8 @@ class MayanAppConfig(apps.AppConfig):
'Import time error when running AppConfig.ready() of app ' 'Import time error when running AppConfig.ready() of app '
'"%s".', self.name '"%s".', self.name
) )
exc_info = sys.exc_info()
traceback.print_exception(*exc_info)
raise exception raise exception
@@ -88,7 +93,9 @@ class CommonApp(MayanAppConfig):
def ready(self): def ready(self):
super(CommonApp, self).ready() super(CommonApp, self).ready()
if check_for_sqlite(): if check_for_sqlite():
warnings.warn(force_text(MESSAGE_SQLITE_WARNING)) warnings.warn(
category=DatabaseWarning, message=force_text(MESSAGE_SQLITE_WARNING)
)
Template( Template(
name='menu_main', template_name='appearance/menu_main.html' name='menu_main', template_name='appearance/menu_main.html'

View File

@@ -72,16 +72,6 @@ class ErrorLogNamespace(object):
return ErrorLogEntry.objects.filter(namespace=self.name) 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): class MissingItem(object):
_registry = [] _registry = []
@@ -302,7 +292,7 @@ class Template(object):
def get_absolute_url(self): def get_absolute_url(self):
return reverse( 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): def render(self, request):

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,12 @@ from __future__ import absolute_import, unicode_literals
from mayan.apps.appearance.classes import Icon from mayan.apps.appearance.classes import Icon
icon_about = Icon(driver_name='fontawesome', symbol='info') 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_add = Icon(driver_name='fontawesome', symbol='plus')
icon_assign_remove_remove = Icon(driver_name='fontawesome', symbol='minus') icon_assign_remove_remove = Icon(driver_name='fontawesome', symbol='minus')
icon_check_version = Icon(driver_name='fontawesome', symbol='sync') icon_check_version = Icon(driver_name='fontawesome', symbol='sync')
@@ -43,6 +49,12 @@ icon_ok = Icon(
icon_packages_licenses = Icon( icon_packages_licenses = Icon(
driver_name='fontawesome', symbol='certificate' 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( icon_setup = Icon(
driver_name='fontawesome', symbol='cog' driver_name='fontawesome', symbol='cog'
) )

View File

@@ -57,12 +57,12 @@ link_documentation = Link(
link_object_error_list = Link( link_object_error_list = Link(
icon_class=icon_object_error_list, icon_class=icon_object_error_list,
kwargs=get_kwargs_factory('resolved_object'), 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', view='common:object_error_list',
) )
link_object_error_list_clear = Link( link_object_error_list_clear = Link(
kwargs=get_kwargs_factory('resolved_object'), 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', view='common:object_error_list_clear',
) )
link_forum = Link( link_forum = Link(

View File

@@ -11,7 +11,7 @@ MESSAGE_SQLITE_WARNING = _(
'for development and testing, not for production.' 'for development and testing, not for production.'
) )
PYPI_URL = 'https://pypi.python.org/pypi' PYPI_URL = 'https://pypi.python.org/pypi'
PK_LIST_SEPARATOR = ','
TEXT_LIST_AS_ITEMS_PARAMETER = '_list_mode' TEXT_LIST_AS_ITEMS_PARAMETER = '_list_mode'
TEXT_LIST_AS_ITEMS_VARIABLE_NAME = 'list_as_items' TEXT_LIST_AS_ITEMS_VARIABLE_NAME = 'list_as_items'
TEXT_CHOICE_ITEMS = 'items' TEXT_CHOICE_ITEMS = 'items'

View File

@@ -11,8 +11,8 @@ from django.core.management.base import CommandError
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _ 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.documents.models import DocumentType
from mayan.apps.storage.utils import fs_cleanup
CONVERTDB_FOLDER = 'convertdb' CONVERTDB_FOLDER = 'convertdb'
CONVERTDB_OUTPUT_FILENAME = 'migrate.json' CONVERTDB_OUTPUT_FILENAME = 'migrate.json'

View File

@@ -8,21 +8,20 @@ from .icons import icon_menu_about, icon_menu_user
__all__ = ( __all__ = (
'menu_about', 'menu_facet', 'menu_list_facet', 'menu_main', 'menu_object', '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_tools', 'menu_topbar', 'menu_user'
) )
menu_about = Menu( menu_about = Menu(
icon_class=icon_menu_about, label=_('System'), name='about' icon_class=icon_menu_about, label=_('System'), name='about'
) )
menu_facet = Menu(name='facet') menu_facet = Menu(label=_('Facet'), name='facet')
menu_list_facet = Menu(name='list facet') menu_list_facet = Menu(label=_('Facet'), name='list facet')
menu_main = Menu(name='main') menu_main = Menu(name='main')
menu_multi_item = Menu(name='multi item') menu_multi_item = Menu(name='multi item')
menu_object = Menu(name='object') menu_object = Menu(label=_('Actions'), name='object')
menu_secondary = Menu(name='secondary') menu_secondary = Menu(label=_('Secondary'), name='secondary')
menu_setup = Menu(name='setup') menu_setup = Menu(name='setup')
menu_sidebar = Menu(name='sidebar')
menu_tools = Menu(name='tools') menu_tools = Menu(name='tools')
menu_topbar = Menu(name='topbar') menu_topbar = Menu(name='topbar')
menu_user = Menu( menu_user = Menu(

View File

@@ -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 __future__ import unicode_literals
from django.db import migrations, models from django.db import migrations, models
import mayan.apps.common.classes import mayan.apps.common.classes
import mayan.apps.common.models import mayan.apps.common.models
import mayan.apps.storage.classes
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('common', '0010_auto_20180403_0702_squashed_0011_auto_20180429_0758'), ('common', '0010_auto_20180403_0702_squashed_0011_auto_20180429_0758'),
] ]
@@ -17,6 +16,10 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='shareduploadedfile', model_name='shareduploadedfile',
name='file', 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

View File

@@ -18,6 +18,7 @@ from .storages import storage_sharedupload
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# TODO: move outside of models.py or as a static method of SharedUploadedFile
def upload_to(instance, filename): def upload_to(instance, filename):
return 'shared-file-{}'.format(uuid.uuid4().hex) return 'shared-file-{}'.format(uuid.uuid4().hex)

View File

@@ -48,8 +48,8 @@ class PurePaginator(Paginator):
self.allow_empty_first_page = allow_empty_first_page self.allow_empty_first_page = allow_empty_first_page
self.object_list = object_list self.object_list = object_list
self.orphans = orphans self.orphans = orphans
self.per_page = per_page
self.page_kwarg = page_kwarg self.page_kwarg = page_kwarg
self.per_page = per_page
self.request = request self.request = request
def page(self, number): def page(self, number):

View File

@@ -7,5 +7,5 @@ from mayan.apps.permissions import PermissionNamespace
namespace = PermissionNamespace(label=_('Common'), name='common') namespace = PermissionNamespace(label=_('Common'), name='common')
permission_error_log_view = namespace.add_permission( permission_error_log_view = namespace.add_permission(
name='error_log_view', label=_('View error log') label=_('View error log'), name='error_log_view'
) )

View File

@@ -5,13 +5,13 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.task_manager.classes import CeleryQueue from mayan.apps.task_manager.classes import CeleryQueue
queue_default = 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( 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( 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'
) )

View File

@@ -5,9 +5,15 @@ from django.contrib.contenttypes.models import ContentType
from rest_framework import serializers from rest_framework import serializers
class ContentTypeSerializer(serializers.ModelSerializer): class ContentTypeSerializer(serializers.HyperlinkedModelSerializer):
class Meta: 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 model = ContentType
@@ -15,3 +21,7 @@ class TemplateSerializer(serializers.Serializer):
hex_hash = serializers.CharField(read_only=True) hex_hash = serializers.CharField(read_only=True)
name = serializers.CharField(read_only=True) name = serializers.CharField(read_only=True)
html = 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'
)

View File

@@ -1,7 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import os import os
import tempfile
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext_lazy as _ 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 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( setting_auto_logging = namespace.add_setting(
global_name='COMMON_AUTO_LOGGING', global_name='COMMON_AUTO_LOGGING',
@@ -53,8 +52,7 @@ setting_production_error_log_path = namespace.add_setting(
global_name='COMMON_PRODUCTION_ERROR_LOG_PATH', global_name='COMMON_PRODUCTION_ERROR_LOG_PATH',
default=os.path.join(settings.MEDIA_ROOT, 'error.log'), help_text=_( default=os.path.join(settings.MEDIA_ROOT, 'error.log'), help_text=_(
'Path to the logfile that will track errors during production.' 'Path to the logfile that will track errors during production.'
), )
is_path=True
) )
setting_project_title = namespace.add_setting( setting_project_title = namespace.add_setting(
global_name='COMMON_PROJECT_TITLE', global_name='COMMON_PROJECT_TITLE',
@@ -77,16 +75,8 @@ setting_shared_storage_arguments = namespace.add_setting(
global_name='COMMON_SHARED_STORAGE_ARGUMENTS', global_name='COMMON_SHARED_STORAGE_ARGUMENTS',
default={'location': os.path.join(settings.MEDIA_ROOT, 'shared_files')} 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( setting_django_allowed_hosts = namespace.add_setting(
global_name='ALLOWED_HOSTS', default=settings.ALLOWED_HOSTS, 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 ' 'for example. This setting also accepts named URL patterns which '
'can be used to reduce configuration duplication since you don\'t ' 'can be used to reduce configuration duplication since you don\'t '
'have to define the URL in two places (settings and URLconf).' '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( setting_django_static_url = namespace.add_setting(
global_name='STATIC_URL', 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( setting_celery_always_eager = namespace.add_setting(
global_name='CELERY_TASK_ALWAYS_EAGER', global_name='CELERY_TASK_ALWAYS_EAGER',

View File

@@ -1,7 +1,8 @@
from __future__ import unicode_literals 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 .settings import setting_shared_storage, setting_shared_storage_arguments
from .utils import get_storage_subclass
storage_sharedupload = get_storage_subclass( storage_sharedupload = get_storage_subclass(
dotted_path=setting_shared_storage.value dotted_path=setting_shared_storage.value

View File

@@ -1,5 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import logging
from django.template import Context, Library, VariableDoesNotExist, Variable from django.template import Context, Library, VariableDoesNotExist, Variable
from django.template.defaultfilters import truncatechars from django.template.defaultfilters import truncatechars
from django.template.loader import get_template 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 ..literals import MESSAGE_SQLITE_WARNING
from ..utils import check_for_sqlite, resolve_attribute from ..utils import check_for_sqlite, resolve_attribute
logger = logging.getLogger(__name__)
register = Library() register = Library()
@@ -48,6 +51,14 @@ def common_calculate_title(context):
return _('Create') 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 @register.simple_tag
def get_collections(): def get_collections():
return Collection.get_all() return Collection.get_all()

View File

@@ -1,30 +1,20 @@
from __future__ import absolute_import, unicode_literals 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 import TestCase
from django.test.utils import ContextList
from django.urls import clear_url_caches, reverse
from django_downloadview import assert_download_response 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.permissions.classes import Permission
from mayan.apps.smart_settings.classes import Namespace 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 ( from .mixins import (
ContentTypeCheckMixin, DatabaseConversionMixin, OpenFileCheckMixin, ClientMethodsTestCaseMixin, ContentTypeCheckMixin, DatabaseConversionMixin,
TempfileCheckMixin 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. 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() Permission.invalidate_cache()
class GenericViewTestCase(BaseTestCase): class GenericViewTestCase(ClientMethodsTestCaseMixin, ContentTypeCheckMixin, TestViewTestCaseMixin, BaseTestCase):
def setUp(self): """
super(GenericViewTestCase, self).setUp() A generic view test case built on top of the base test case providing
self.has_test_view = False a single, user customizable view to test object resolution and shorthand
HTTP method functions.
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
)

View File

@@ -2,20 +2,26 @@ from __future__ import unicode_literals
import glob import glob
import os import os
import random
from furl import furl
from django.apps import apps
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.conf.urls import url
from django.contrib.auth.models import Group from django.contrib.contenttypes.models import ContentType
from django.core import management 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 ( from mayan.apps.storage.settings import setting_temporary_directory
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 .literals import TEST_VIEW_NAME, TEST_VIEW_URL
from .utils import mute_stdout from .utils import mute_stdout
@@ -23,6 +29,56 @@ if getattr(settings, 'COMMON_TEST_FILE_HANDLES', False):
import psutil 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): class ContentTypeCheckMixin(object):
expected_content_type = 'text/html; charset=utf-8' expected_content_type = 'text/html; charset=utf-8'
@@ -34,13 +90,14 @@ class ContentTypeCheckMixin(object):
def request(self, *args, **kwargs): def request(self, *args, **kwargs):
response = super(CustomClient, self).request(*args, **kwargs) response = super(CustomClient, self).request(*args, **kwargs)
content_type = response._headers['content-type'][1] content_type = response._headers.get('content-type', [None, ''])[1]
test_instance.assertEqual( if test_instance.expected_content_type:
content_type, test_instance.expected_content_type, test_instance.assertEqual(
msg='Unexpected response content type: {}, expected: {}.'.format( content_type, test_instance.expected_content_type,
content_type, test_instance.expected_content_type msg='Unexpected response content type: {}, expected: {}.'.format(
content_type, test_instance.expected_content_type
)
) )
)
return response return response
@@ -55,7 +112,7 @@ class DatabaseConversionMixin(object):
) )
class OpenFileCheckMixin(object): class OpenFileCheckTestCaseMixin(object):
def _get_descriptor_count(self): def _get_descriptor_count(self):
process = psutil.Process() process = psutil.Process()
return process.num_fds() return process.num_fds()
@@ -65,7 +122,7 @@ class OpenFileCheckMixin(object):
return process.open_files() return process.open_files()
def setUp(self): def setUp(self):
super(OpenFileCheckMixin, self).setUp() super(OpenFileCheckTestCaseMixin, self).setUp()
if getattr(settings, 'COMMON_TEST_FILE_HANDLES', False): if getattr(settings, 'COMMON_TEST_FILE_HANDLES', False):
self._open_files = self._get_open_files() self._open_files = self._get_open_files()
@@ -80,10 +137,61 @@ class OpenFileCheckMixin(object):
self._skip_file_descriptor_test = False 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 the jvmstat instrumentation and GitLab's CI .config files
# Ignore LibreOffice fontconfig cache dir # Ignore LibreOffice fontconfig cache dir
ignore_globs = ('hsperfdata_*', '.config', '.cache') ignore_globs = ('hsperfdata_*', '.config', '.cache')
@@ -108,7 +216,7 @@ class TempfileCheckMixin(object):
) - set(ignored_result) ) - set(ignored_result)
def setUp(self): def setUp(self):
super(TempfileCheckMixin, self).setUp() super(TempfileCheckTestCaseMixin, self).setUp()
if getattr(settings, 'COMMON_TEST_TEMP_FILES', False): if getattr(settings, 'COMMON_TEST_TEMP_FILES', False):
self._temporary_items = self._get_temporary_entries() self._temporary_items = self._get_temporary_entries()
@@ -123,4 +231,101 @@ class TempfileCheckMixin(object):
','.join(final_temporary_items - self._temporary_items) ','.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)

View File

@@ -1,7 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.test import override_settings from django.test import override_settings
from django.urls import reverse
from mayan.apps.rest_api.tests import BaseAPITestCase from mayan.apps.rest_api.tests import BaseAPITestCase
@@ -11,23 +10,40 @@ TEST_TEMPLATE_RESULT = '<div'
class CommonAPITestCase(BaseAPITestCase): class CommonAPITestCase(BaseAPITestCase):
auto_login_user = False
def test_content_type_list_view(self): 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) 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): def test_template_detail_view(self):
self.login_user() 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( self.assertContains(
response=response, text=TEST_TEMPLATE_RESULT, status_code=200 response=response, text=TEST_TEMPLATE_RESULT, status_code=200
) )
def test_template_detail_anonymous_view(self): def test_template_detail_anonymous_view(self):
template_main_menu = Template.get(name='menu_main') self.test_template = Template.get(name='menu_main')
response = self.client.get(template_main_menu.get_absolute_url())
response = self._request_template_detail_view()
self.assertNotContains( self.assertNotContains(
response=response, text=TEST_TEMPLATE_RESULT, status_code=403 response=response, text=TEST_TEMPLATE_RESULT, status_code=403
) )

View File

@@ -6,5 +6,5 @@ from mayan.apps.user_management.tests.mixins import UserTestMixin
class UserLocaleProfileTestCase(UserTestMixin, BaseTestCase): class UserLocaleProfileTestCase(UserTestMixin, BaseTestCase):
def test_natural_keys(self): def test_natural_keys(self):
self._create_user() self._create_test_user()
self._test_database_conversion('auth', 'common') self._test_database_conversion('auth', 'common')

View File

@@ -14,8 +14,6 @@ from .literals import TEST_ERROR_LOG_ENTRY_RESULT
class CommonViewTestCase(GenericViewTestCase): class CommonViewTestCase(GenericViewTestCase):
def test_about_view(self): def test_about_view(self):
self.login_user()
response = self.get('common:about_view') response = self.get('common:about_view')
self.assertContains(response, text='About', status_code=200) self.assertContains(response, text='About', status_code=200)
@@ -25,27 +23,36 @@ class CommonViewTestCase(GenericViewTestCase):
) )
ErrorLogEntry.objects.register(model=get_user_model()) 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 result=TEST_ERROR_LOG_ENTRY_RESULT
) )
def _request_object_error_log_list(self): 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( return self.get(
'common:object_error_list', kwargs={ 'common:object_error_list', kwargs={
'app_label': content_type.app_label, 'app_label': content_type.app_label,
'model': content_type.model, 'model': content_type.model,
'object_id': self.user.pk 'object_id': self._test_case_user.pk
}, follow=True }, 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._create_error_log_entry()
self.login_user()
self.grant_access( 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() response = self._request_object_error_log_list()
@@ -54,15 +61,3 @@ class CommonViewTestCase(GenericViewTestCase):
response=response, text=TEST_ERROR_LOG_ENTRY_RESULT, response=response, text=TEST_ERROR_LOG_ENTRY_RESULT,
status_code=200 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
)

View File

@@ -3,9 +3,7 @@ from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
from django.views.i18n import javascript_catalog, set_language from django.views.i18n import javascript_catalog, set_language
from .api_views import ( from .api_views import ContentTypeAPIViewSet, TemplateAPIViewSet
APIContentTypeList, APITemplateListView, APITemplateView
)
from .views import ( from .views import (
AboutView, CheckVersionView, CurrentUserLocaleProfileDetailsView, AboutView, CheckVersionView, CurrentUserLocaleProfileDetailsView,
CurrentUserLocaleProfileEditView, FaviconRedirectView, HomeView, CurrentUserLocaleProfileEditView, FaviconRedirectView, HomeView,
@@ -15,67 +13,65 @@ from .views import (
) )
urlpatterns = [ urlpatterns = [
url(r'^$', RootView.as_view(), name='root'), url(regex=r'^$', name='root', view=RootView.as_view()),
url(r'^home/$', HomeView.as_view(), name='home'), url(regex=r'^home/$', name='home', view=HomeView.as_view()),
url(r'^about/$', AboutView.as_view(), name='about_view'), url(regex=r'^about/$', name='about_view', view=AboutView.as_view()),
url( url(
r'^check_version/$', CheckVersionView.as_view(), regex=r'^check_version/$', name='check_version_view',
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( url(
r'^packages/licenses/$', PackagesLicensesView.as_view(), regex=r'^packages/licenses/$', name='packages_licenses_view',
name='packages_licenses_view' view=PackagesLicensesView.as_view()
), ),
url( url(
r'^object/multiple/action/$', multi_object_action_view, regex=r'^objects/multiple/action/$', name='multi_object_action_view',
name='multi_object_action_view' view=multi_object_action_view
), ),
url(r'^setup/$', SetupListView.as_view(), name='setup_list'), url(regex=r'^setup/$', name='setup_list', view=SetupListView.as_view()),
url(r'^tools/$', ToolsListView.as_view(), name='tools_list'), url(regex=r'^tools/$', name='tools_list', view=ToolsListView.as_view()),
url( url(
r'^user/locale/$', CurrentUserLocaleProfileDetailsView.as_view(), regex=r'^users/current/locale/$',
name='current_user_locale_profile_details' name='current_user_locale_profile_details',
view=CurrentUserLocaleProfileDetailsView.as_view()
), ),
url( url(
r'^user/locale/edit/$', CurrentUserLocaleProfileEditView.as_view(), regex=r'^users/current/locale/edit/$',
name='current_user_locale_profile_edit' name='current_user_locale_profile_edit',
view=CurrentUserLocaleProfileEditView.as_view()
), ),
url( url(
r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/errors/$', regex=r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/errors/$',
ObjectErrorLogEntryListView.as_view(), name='object_error_list' name='object_error_list', view=ObjectErrorLogEntryListView.as_view()
), ),
url( url(
r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/errors/clear/$', regex=r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/errors/clear/$',
ObjectErrorLogEntryListClearView.as_view(), name='object_error_list_clear',
name='object_error_list_clear' view=ObjectErrorLogEntryListClearView.as_view()
), ),
] ]
urlpatterns += [ urlpatterns += [
url( url(
r'^favicon\.ico$', FaviconRedirectView.as_view() regex=r'^favicon\.ico$', view=FaviconRedirectView.as_view()
), ),
url( url(
r'^jsi18n/(?P<packages>\S+?)/$', javascript_catalog, regex=r'^jsi18n/(?P<packages>\S+?)/$', name='javascript_catalog',
name='javascript_catalog' view=javascript_catalog
), ),
url( url(
r'^set_language/$', set_language, name='set_language' regex=r'^set_language/$', name='set_language', view=set_language
), ),
] ]
api_urls = [ api_router_entries = (
url( {
r'^content_types/$', APIContentTypeList.as_view(), 'prefix': r'content_types', 'viewset': ContentTypeAPIViewSet,
name='content-type-list' 'basename': 'content_type'
), },
url( {
r'^templates/$', APITemplateListView.as_view(), 'prefix': r'templates', 'viewset': TemplateAPIViewSet,
name='template-list' 'basename': 'template'
), },
url( )
r'^templates/(?P<name>[-\w]+)/$', APITemplateView.as_view(),
name='template-detail'
),
]

View File

@@ -1,18 +1,15 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import logging import logging
import os
import shutil
import tempfile
from django.conf import settings from django.conf import settings
from django.core.exceptions import FieldDoesNotExist
from django.db.models.constants import LOOKUP_SEP from django.db.models.constants import LOOKUP_SEP
from django.urls import resolve as django_resolve from django.urls import resolve as django_resolve
from django.urls.base import get_script_prefix from django.urls.base import get_script_prefix
from django.utils.datastructures import MultiValueDict from django.utils.datastructures import MultiValueDict
from django.utils.http import urlencode as django_urlencode from django.utils.http import urlencode as django_urlencode
from django.utils.http import urlquote as django_urlquote 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 reduce as reduce_function
from django.utils.six.moves import xmlrpc_client from django.utils.six.moves import xmlrpc_client
@@ -20,7 +17,6 @@ import mayan
from .exceptions import NotLatestVersion, UnknownLatestVersion from .exceptions import NotLatestVersion, UnknownLatestVersion
from .literals import DJANGO_SQLITE_BACKEND, MAYAN_PYPI_NAME, PYPI_URL from .literals import DJANGO_SQLITE_BACKEND, MAYAN_PYPI_NAME, PYPI_URL
from .settings import setting_temporary_directory
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -39,27 +35,6 @@ def check_version():
raise NotLatestVersion(upstream_version=versions[0]) 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): def encapsulate(function):
# Workaround Django ticket 15791 # Workaround Django ticket 15791
# Changeset 16045 # Changeset 16045
@@ -68,74 +43,49 @@ def encapsulate(function):
return lambda: function return lambda: function
def fs_cleanup(filename, file_descriptor=None, suppress_exceptions=True): def get_related_field(model, related_field_name):
"""
Tries to remove the given filename. Ignores non-existent files
"""
if file_descriptor:
os.close(file_descriptor)
try: try:
os.remove(filename) local_field_name, remaining_field_path = related_field_name.split(
except OSError: 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: try:
shutil.rmtree(filename) # If there are separators in the attribute name, traverse them
except OSError: # to the final attribute
if suppress_exceptions: attribute_part, attribute_remaining = attribute_name.split(
pass '.', 1
else: )
raise except ValueError:
return attribute_name, obj
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')
else: 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: else:
return file_input return attribute_name, obj
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)
def resolve(path, urlconf=None): def resolve(path, urlconf=None):
@@ -230,24 +180,3 @@ def urlquote(link=None, get=None):
return '%s%s' % (link, django_urlencode(get, doseq=True)) return '%s%s' % (link, django_urlencode(get, doseq=True))
else: else:
return django_urlquote(link) 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

View File

@@ -6,27 +6,25 @@ from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.http import HttpResponseRedirect 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.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 import timezone, translation
from django.utils.http import urlencode from django.utils.http import urlencode
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import RedirectView, TemplateView 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 .exceptions import NotLatestVersion, UnknownLatestVersion
from .forms import ( from .forms import (
LicenseForm, LocaleProfileForm, LocaleProfileForm_view, LicenseForm, LocaleProfileForm, LocaleProfileForm_view,
PackagesLicensesForm PackagesLicensesForm
) )
from .generics import ( # NOQA from .generics import (
AssignRemoveView, ConfirmView, FormView, MultiFormView, ConfirmView, SimpleView, SingleObjectEditView, SingleObjectListView
MultipleObjectConfirmActionView, MultipleObjectFormActionView, SimpleView,
SingleObjectCreateView, SingleObjectDeleteView, SingleObjectDetailView,
SingleObjectDownloadView, SingleObjectDynamicFormCreateView,
SingleObjectDynamicFormEditView, SingleObjectEditView, SingleObjectListView
) )
from .icons import icon_object_error_list, icon_setup from .icons import icon_object_error_list, icon_setup
from .menus import menu_setup, menu_tools 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): def dispatch(self, request, *args, **kwargs):
AccessControlList.objects.check_access( AccessControlList.objects.check_access(
obj=self.get_object(), permissions=permission_error_log_view, obj=self.get_object(), permissions=permission_error_log_view,
@@ -181,6 +181,7 @@ class ObjectErrorLogEntryListView(SingleObjectListView):
return super(ObjectErrorLogEntryListView, self).dispatch( return super(ObjectErrorLogEntryListView, self).dispatch(
request, *args, **kwargs request, *args, **kwargs
) )
"""
def get_extra_context(self): def get_extra_context(self):
return { return {
@@ -202,6 +203,7 @@ class ObjectErrorLogEntryListView(SingleObjectListView):
'title': _('Error log entries for: %s' % self.get_object()), 'title': _('Error log entries for: %s' % self.get_object()),
} }
"""
def get_object(self): def get_object(self):
content_type = get_object_or_404( content_type = get_object_or_404(
klass=ContentType, app_label=self.kwargs['app_label'], klass=ContentType, app_label=self.kwargs['app_label'],
@@ -211,9 +213,9 @@ class ObjectErrorLogEntryListView(SingleObjectListView):
return get_object_or_404( return get_object_or_404(
klass=content_type.model_class(), pk=self.kwargs['object_id'] klass=content_type.model_class(), pk=self.kwargs['object_id']
) )
"""
def get_object_list(self): def get_object_list(self):
return self.get_object().error_logs.all() return self.get_external_object().error_logs.all()
class PackagesLicensesView(SimpleView): class PackagesLicensesView(SimpleView):
@@ -234,41 +236,41 @@ class RootView(SimpleView):
template_name = 'appearance/root.html' template_name = 'appearance/root.html'
class SetupListView(TemplateView): class SetupListView(SimpleView):
template_name = 'appearance/generic_list_horizontal.html' template_name = 'appearance/generic_list_horizontal.html'
def get_context_data(self, **kwargs): def get_extra_context(self):
data = super(SetupListView, self).get_context_data(**kwargs)
context = RequestContext(self.request) context = RequestContext(self.request)
context['request'] = self.request context['request'] = self.request
data.update( return {
{ 'no_results_icon': icon_setup,
'no_results_icon': icon_setup, 'no_results_label': _('No setup options available.'),
'no_results_label': _('No setup options available.'), 'no_results_text': _(
'no_results_text': _( 'No results here means that don\'t have the required '
'No results here means that don\'t have the required ' 'permissions to perform administrative task.'
'permissions to perform administrative task.' ),
), 'resolved_links': menu_setup.resolve(context=context),
'resolved_links': menu_setup.resolve(context=context), 'title': _('Setup'),
'title': _('Setup items'), 'subtitle': _(
} 'Here you can configure all aspects of the system.'
) ),
return data }
class ToolsListView(SimpleView): class ToolsListView(SimpleView):
template_name = 'appearance/generic_list_horizontal.html' template_name = 'appearance/generic_list_horizontal.html'
def get_menu_links(self): def get_extra_context(self):
context = RequestContext(self.request) context = RequestContext(self.request)
context['request'] = self.request context['request'] = self.request
return menu_tools.resolve(context=context)
def get_extra_context(self):
return { return {
'resolved_links': self.get_menu_links(), 'resolved_links': menu_tools.resolve(context=context),
'title': _('Tools'), '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.POST.get(
'next', request.GET.get( 'next', request.GET.get(
'next', request.META.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.')) messages.error(request, _('No action selected.'))
return HttpResponseRedirect( return HttpResponseRedirect(
request.META.get( 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.')) messages.error(request, _('Must select at least one item.'))
return HttpResponseRedirect( return HttpResponseRedirect(
request.META.get( request.META.get(
'HTTP_REFERER', resolve_url(settings.LOGIN_REDIRECT_URL) 'HTTP_REFERER', reverse(setting_home_view.value)
) )
) )

View 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
"""

View File

@@ -3,7 +3,6 @@ from __future__ import unicode_literals
from django import forms from django import forms
from django.template import Context, Template from django.template import Context, Template
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.html import format_html
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from .icons import icon_fail as default_icon_fail from .icons import icon_fail as default_icon_fail

View File

@@ -1,6 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from .runtime import converter_class # NOQA
from .transformations import ( # NOQA from .transformations import ( # NOQA
BaseTransformation, TransformationResize, TransformationRotate, BaseTransformation, TransformationResize, TransformationRotate,
TransformationZoom TransformationZoom

View File

@@ -1,9 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _ 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 mayan.apps.navigation import SourceColumn
from .links import ( from .links import (
@@ -23,25 +23,31 @@ class ConverterApp(MayanAppConfig):
def ready(self): def ready(self):
super(ConverterApp, self).ready() 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( SourceColumn(
source=Transformation, label=_('Transformation'), attribute='order', include_label=True, source=Transformation
func=lambda context: force_text(context['object'])
) )
SourceColumn( 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( menu_object.bind_links(
links=(link_transformation_edit, link_transformation_delete), links=(link_transformation_edit, link_transformation_delete),
sources=(Transformation,) sources=(Transformation,)
) )
menu_sidebar.bind_links( menu_secondary.bind_links(
links=(link_transformation_create,), sources=(Transformation,) links=(link_transformation_create,), sources=(Transformation,)
) )
menu_sidebar.bind_links( menu_secondary.bind_links(
links=(link_transformation_create,), links=(link_transformation_create,),
sources=( sources=(
'converter:transformation_create', 'converter:transformation_create',

View File

@@ -11,7 +11,7 @@ import sh
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _ 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 ..classes import ConverterBase
from ..exceptions import PageCountError from ..exceptions import PageCountError

View File

@@ -9,9 +9,9 @@ import sh
from django.utils.translation import ugettext_lazy as _ 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.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 .exceptions import InvalidOfficeFormat, OfficeConversionError
from .literals import ( from .literals import (

View File

@@ -16,10 +16,10 @@ class TransformationForm(forms.ModelForm):
def clean(self): def clean(self):
try: try:
yaml.safe_load(self.cleaned_data['arguments']) yaml.safe_load(stream=self.cleaned_data['arguments'])
except yaml.YAMLError: except yaml.YAMLError:
raise ValidationError( raise ValidationError(
_( message=_(
'"%s" not a valid entry.' '"%s" not a valid entry.'
) % self.cleaned_data['arguments'] ) % self.cleaned_data['arguments']
) )

View File

@@ -3,4 +3,9 @@ from __future__ import absolute_import, unicode_literals
from mayan.apps.appearance.classes import Icon from mayan.apps.appearance.classes import Icon
icon_transformation = Icon(driver_name='fontawesome', symbol='crop') 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