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
to attach a tag, the user's role will need the tag attach
permissions for both, the document and the tag.
- Refactor and optimize the access control computation. Most of
the computation has been moved to the database instead of doing
filtering in Python. The refactor added cascading access checking
in preparation for nested cabinet access control and the removal
of the permission proxy support which is now redundant.
- Remove the permissions to grant or revoke a permission to a role.
The instead the role edit permission is used.
- Add a test mixin to generate random model primary keys.
- Add support for checkout and check in multiple documents at
the same time.
- Move file and storage code to the storage app. The setting
COMMON_TEMPORARY_DIRECTORY is now STORAGE_TEMPORARY_DIRECTORY.
- To lower memory usage and reduce memory leaks, the entire
entire converter class is no longer cached and instead loaded
on demand. This allows the garbage collector to clear the memory
used.
- Update the permission requirements for the index template
document type selection screen. The document type view
permission is now required in addition to the index
template edit permission.
- Update the links display templates to show which object the
links belong to when there is more than one object.
- Update the links display templates to show which menu
the links belong to when there is more than one menu.
- Remove the sidebar menu and unify its links with the
secondary menu.
- Increate the default maximum title lenght to 120 characters.
- In the search API, the search function is now a service
of the search model resource.
- The simple and advance search are now the same service. The
difference is determined by the URL query. A ?q= means a
simple search. For advanced search pass the search model
fields in the URL query, example: ?q=document_type__label=
- Remove django-mathfilters from requirements. These tags
are provided by default by Jinja2 template engine
(http://jinja.pocoo.org/docs/2.10/templates/#math).
3.1.9 (2018-11-01)
==================

View File

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

View File

@@ -63,5 +63,5 @@ Changes needed:
the Role model's permissions many to many field.
4. Update the ``AccessControlList`` models roles field to point to the group
models.
5. Update the role checks in the ``check_access`` and ``filter_by_access``
5. Update the role checks in the ``check_access`` and ``restrict_queryset``
``AccessControlList`` model manager methods.

View File

@@ -1,203 +1,121 @@
from __future__ import absolute_import, unicode_literals
from django.contrib.contenttypes.models import ContentType
from django.shortcuts import get_object_or_404
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import generics
from .models import AccessControlList
from .permissions import permission_acl_edit, permission_acl_view
from .serializers import (
AccessControlListPermissionSerializer, AccessControlListSerializer,
WritableAccessControlListPermissionSerializer,
WritableAccessControlListSerializer
from mayan.apps.common.mixins import ContentTypeViewMixin
from mayan.apps.permissions.serializers import (
PermissionSerializer, RolePermissionAddRemoveSerializer
)
from mayan.apps.rest_api.mixins import ExternalObjectAPIViewSetMixin
from mayan.apps.rest_api.viewsets import MayanAPIModelViewSet
from .permissions import permission_acl_edit, permission_acl_view
from .serializers import AccessControlListSerializer
class APIObjectACLListView(generics.ListCreateAPIView):
"""
get: Returns a list of all the object's access control lists
post: Create a new access control list for the selected object.
"""
def get_content_object(self):
content_type = get_object_or_404(
klass=ContentType, app_label=self.kwargs['app_label'],
model=self.kwargs['model']
)
content_object = get_object_or_404(
klass=content_type.model_class(), pk=self.kwargs['object_id']
)
if self.request.method == 'GET':
permission_required = permission_acl_view
else:
permission_required = permission_acl_edit
AccessControlList.objects.check_access(
permissions=permission_required, user=self.request.user,
obj=content_object
)
return content_object
def get_queryset(self):
return self.get_content_object().acls.all()
def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
context = super(APIObjectACLListView, self).get_serializer_context()
if self.kwargs:
context.update(
{
'content_object': self.get_content_object(),
}
)
return context
def get_serializer(self, *args, **kwargs):
if not self.request:
return None
return super(APIObjectACLListView, self).get_serializer(*args, **kwargs)
def get_serializer_class(self):
if self.request.method == 'GET':
return AccessControlListSerializer
else:
return WritableAccessControlListSerializer
class APIObjectACLView(generics.RetrieveDestroyAPIView):
"""
delete: Delete the selected access control list.
get: Returns the details of the selected access control list.
"""
class ObjectACLAPIViewSet(ContentTypeViewMixin, ExternalObjectAPIViewSetMixin, MayanAPIModelViewSet):
content_type_url_kw_args = {
'app_label': 'app_label',
'model': 'model_name'
}
external_object_pk_url_kwarg = 'object_id'
lookup_url_kwarg = 'acl_id'
serializer_class = AccessControlListSerializer
def get_content_object(self):
if self.request.method == 'GET':
permission_required = permission_acl_view
else:
permission_required = permission_acl_edit
content_type = get_object_or_404(
klass=ContentType, app_label=self.kwargs['app_label'],
model=self.kwargs['model']
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.validated_data.update(
{
'object_id': self.external_object.pk,
'content_type': self.get_content_type(),
}
)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
content_object = get_object_or_404(
klass=content_type.model_class(), pk=self.kwargs['object_id']
)
AccessControlList.objects.check_access(
permissions=permission_required, user=self.request.user,
obj=content_object
)
return content_object
def get_queryset(self):
return self.get_content_object().acls.all()
class APIObjectACLPermissionListView(generics.ListCreateAPIView):
"""
get: Returns the access control list permission list.
post: Add a new permission to the selected access control list.
"""
def get_acl(self):
return get_object_or_404(
klass=self.get_content_object().acls, pk=self.kwargs['acl_pk']
)
def get_content_object(self):
content_type = get_object_or_404(
klass=ContentType, app_label=self.kwargs['app_label'],
model=self.kwargs['model']
)
content_object = get_object_or_404(
klass=content_type.model_class(), pk=self.kwargs['object_id']
)
AccessControlList.objects.check_access(
permissions=permission_acl_view, user=self.request.user,
obj=content_object
)
return content_object
def get_queryset(self):
return self.get_acl().permissions.all()
def get_serializer(self, *args, **kwargs):
if not self.request:
def get_external_object_permission(self):
action = getattr(self, 'action', None)
if action is None:
return None
return super(APIObjectACLPermissionListView, self).get_serializer(*args, **kwargs)
def get_serializer_class(self):
if self.request.method == 'GET':
return AccessControlListPermissionSerializer
elif action in ['list', 'retrieve', 'permission_list', 'permission_inherited_list']:
return permission_acl_view
else:
return WritableAccessControlListPermissionSerializer
return permission_acl_edit
def get_serializer_context(self):
context = super(APIObjectACLPermissionListView, self).get_serializer_context()
if self.kwargs:
context.update(
{
'acl': self.get_acl(),
}
)
return context
class APIObjectACLPermissionView(generics.RetrieveDestroyAPIView):
"""
delete: Remove the permission from the selected access control list.
get: Returns the details of the selected access control list permission.
"""
lookup_url_kwarg = 'permission_pk'
serializer_class = AccessControlListPermissionSerializer
def get_acl(self):
return get_object_or_404(
klass=self.get_content_object().acls, pk=self.kwargs['acl_pk']
)
def get_content_object(self):
content_type = get_object_or_404(
klass=ContentType, app_label=self.kwargs['app_label'],
model=self.kwargs['model']
)
content_object = get_object_or_404(
klass=content_type.model_class(), pk=self.kwargs['object_id']
)
AccessControlList.objects.check_access(
permissions=permission_acl_view, user=self.request.user,
obj=content_object
)
return content_object
def get_external_object_queryset(self):
# Here we get a queryset the object model for which the event
# will be accessed.
return self.get_content_type().get_all_objects_for_this_type()
def get_queryset(self):
return self.get_acl().permissions.all()
return self.get_external_object().acls.all()
def get_serializer_context(self):
context = super(APIObjectACLPermissionView, self).get_serializer_context()
if self.kwargs:
context.update(
{
'acl': self.get_acl(),
}
)
@action(
detail=True, lookup_url_kwarg='acl_id', methods=('post',),
serializer_class=RolePermissionAddRemoveSerializer,
url_name='permission-add', url_path='permissions/add'
)
def permission_add(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.permissions_add(instance=instance)
headers = self.get_success_headers(data=serializer.data)
return Response(
serializer.data, headers=headers, status=status.HTTP_200_OK
)
return context
@action(
detail=True, lookup_url_kwarg='acl_id',
serializer_class=PermissionSerializer, url_name='permission-list',
url_path='permissions'
)
def permission_list(self, request, *args, **kwargs):
queryset = self.get_object().permissions.all()
page = self.paginate_queryset(queryset)
serializer = self.get_serializer(
queryset, many=True, context={'request': request}
)
if page is not None:
return self.get_paginated_response(serializer.data)
return Response(serializer.data)
@action(
detail=True, lookup_url_kwarg='acl_id',
serializer_class=PermissionSerializer,
url_name='permission-inherited-list', url_path='permissions/inherited'
)
def permission_inherited_list(self, request, *args, **kwargs):
queryset = self.get_object().get_inherited_permissions()
page = self.paginate_queryset(queryset)
serializer = self.get_serializer(
queryset, many=True, context={'request': request}
)
if page is not None:
return self.get_paginated_response(serializer.data)
return Response(serializer.data)
@action(
detail=True, lookup_url_kwarg='acl_id',
methods=('post',), serializer_class=RolePermissionAddRemoveSerializer,
url_name='permission-remove', url_path='permissions/remove'
)
def permission_remove(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.permissions_remove(instance=instance)
headers = self.get_success_headers(data=serializer.data)
return Response(
serializer.data, headers=headers, status=status.HTTP_200_OK
)

View File

@@ -2,9 +2,15 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common import MayanAppConfig, menu_object, menu_sidebar
from mayan.apps.common import MayanAppConfig, menu_object, menu_secondary
from mayan.apps.events import ModelEventType
from mayan.apps.events.links import (
link_events_for_object, link_object_event_types_user_subcriptions_list
)
from mayan.apps.navigation import SourceColumn
from .classes import ModelPermission
from .events import event_acl_created, event_acl_edited
from .links import link_acl_create, link_acl_delete, link_acl_permissions
@@ -18,22 +24,33 @@ class ACLsApp(MayanAppConfig):
def ready(self):
super(ACLsApp, self).ready()
from actstream import registry
AccessControlList = self.get_model(model_name='AccessControlList')
ModelEventType.register(
event_types=(event_acl_created, event_acl_edited),
model=AccessControlList
)
ModelPermission.register_inheritance(
model=AccessControlList, related='content_object',
)
SourceColumn(
attribute='role', is_identifier=True, is_sortable=True,
source=AccessControlList
)
SourceColumn(
attribute='get_permission_titles', include_label=True,
source=AccessControlList
)
menu_object.bind_links(
links=(link_acl_permissions, link_acl_delete),
links=(
link_acl_permissions, link_acl_delete,
link_events_for_object,
link_object_event_types_user_subcriptions_list
),
sources=(AccessControlList,)
)
menu_sidebar.bind_links(
menu_secondary.bind_links(
links=(link_acl_create,), sources=('acls:acl_list',)
)
registry.register(AccessControlList)

View File

@@ -40,25 +40,15 @@ class ModelPermission(object):
app_label='permissions', model_name='StoredPermission'
)
permissions = []
class_permissions = cls.get_for_class(klass=type(instance))
if class_permissions:
permissions.extend(class_permissions)
proxy = cls._proxies.get(type(instance))
if proxy:
permissions.extend(cls._registry.get(proxy))
permissions = cls.get_for_class(klass=type(instance))
pks = [
permission.stored_permission.pk for permission in set(permissions)
permission.stored_permission.pk for permission in permissions
]
return StoredPermission.objects.filter(pk__in=pks)
@classmethod
def get_inheritance(cls, model):
def get_inheritances(cls, model):
return cls._inheritances[model]
@classmethod
@@ -79,7 +69,8 @@ class ModelPermission(object):
@classmethod
def register_inheritance(cls, model, related):
cls._inheritances[model] = related
cls._inheritances.setdefault(model, [])
cls._inheritances[model].append(related)
@classmethod
def register_proxy(cls, source, model):

16
mayan/apps/acls/events.py Normal file
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
icon_acl_delete = Icon(driver_name='fontawesome', symbol='minus')
icon_acl_delete = Icon(driver_name='fontawesome', symbol='times')
icon_acl_list = Icon(driver_name='fontawesome', symbol='lock')
icon_acl_new = Icon(
driver_name='fontawesome-dual', primary_symbol='lock',

View File

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

View File

@@ -1,15 +1,21 @@
from __future__ import absolute_import, unicode_literals
import logging
import operator
import warnings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext
from django.utils.translation import ugettext_lazy as _
from django.db.models import CharField, Value as V, Q
from django.db.models.functions import Concat
from django.http import Http404
from mayan.apps.common.utils import resolve_attribute, return_related
from mayan.apps.common.utils import (
get_related_field, resolve_attribute, return_related
)
from mayan.apps.common.warnings import InterfaceWarning
from mayan.apps.permissions import Permission
from mayan.apps.permissions.models import StoredPermission
@@ -24,210 +30,189 @@ class AccessControlListManager(models.Manager):
Implement a 3 tier permission system, involving a permissions, an actor
and an object
"""
def check_access(self, permissions, user, obj, related=None):
if user.is_superuser or user.is_staff:
logger.debug(
'Permissions "%s" on "%s" granted to user "%s" as superuser '
'or staff', permissions, obj, user
def _get_acl_filters(self, queryset, stored_permission, user, related_field_name=None):
"""
This method does the bulk of the work. It generates filters for the
AccessControlList model to determine if there are ACL entries for the
members of the queryset's model provided.
"""
# Determine which of the cases we need to address
# 1: No related field
# 2: Related field
# 3: Related field that is Generic Foreign Key
# 4: No related field, but has an inherited related field, solved by
# recursion, branches to #2 or #3.
# 5: Inherited field of a related field
# -- Not addressed yet --
# 6: Inherited field of a related field that is Generic Foreign Key
result = []
if related_field_name:
related_field = get_related_field(
model=queryset.model, related_field_name=related_field_name
)
return True
try:
return Permission.check_permissions(
requester=user, permissions=permissions
)
except PermissionDenied:
try:
stored_permissions = [
permission.stored_permission for permission in permissions
]
except TypeError:
# Not a list of permissions, just one
stored_permissions = (permissions.stored_permission,)
if isinstance(related_field, GenericForeignKey):
# Case 3: Generic Foreign Key, multiple ContentTypes + object
# id combinations
content_type_object_id_queryset = queryset.annotate(
ct_fk_combination=Concat(
related_field.ct_field, V('-'), related_field.fk_field,
output_field=CharField()
)
).values('ct_fk_combination')
if related:
obj = resolve_attribute(obj=obj, attribute=related)
acl_filter = self.annotate(
ct_fk_combination=Concat(
'content_type', V('-'), 'object_id', output_field=CharField()
)
).filter(
permissions=stored_permission, role__groups__user=user,
ct_fk_combination__in=content_type_object_id_queryset
).values('object_id')
try:
parent_accessor = ModelPermission.get_inheritance(
model=obj._meta.model
field_lookup = 'object_id__in'
result.append(Q(**{field_lookup: acl_filter}))
else:
# Case 2: Related field of a single type, single ContentType,
# multiple object id
content_type = ContentType.objects.get_for_model(
model=related_field.related_model
)
except AttributeError:
# AttributeError means non model objects: ie Statistics
# These can't have ACLs so we raise PermissionDenied
raise PermissionDenied(
_('Insufficient access for: %(object)s') % {'object': obj}
field_lookup = '{}_id__in'.format(related_field_name)
acl_filter = self.filter(
content_type=content_type, permissions=stored_permission,
role__groups__user=user
).values('object_id')
result.append(Q(**{field_lookup: acl_filter}))
# Case 5: Related field, has an inherited related field itself
# Bubble up permssion check
# TODO: Add relationship support: OR or AND
# TODO: OR for document pages, version, doc, and types
# TODO: AND for new cabinet levels ACLs
try:
related_field_model_related_fields = ModelPermission.get_inheritances(
model=related_field.related_model
)
except KeyError:
pass
else:
relation_result = []
for related_field_model_related_field_name in related_field_model_related_fields:
related_field_name = '{}__{}'.format(related_field_name, related_field_model_related_field_name)
related_field_inherited_acl_queries = self._get_acl_filters(
queryset=queryset, stored_permission=stored_permission,
user=user, related_field_name=related_field_name
)
relation_result.append(reduce(operator.and_, related_field_inherited_acl_queries))
result.append(reduce(operator.or_, relation_result))
else:
# Case 1: Original model, single ContentType, multiple object id
content_type = ContentType.objects.get_for_model(model=queryset.model)
field_lookup = 'id__in'
acl_filter = self.filter(
content_type=content_type, permissions=stored_permission,
role__groups__user=user
).values('object_id')
result.append(Q(**{field_lookup: acl_filter}))
# Case 4: Original model, has an inherited related field
try:
related_fields = ModelPermission.get_inheritances(
model=queryset.model
)
except KeyError:
pass
else:
try:
return self.check_access(
obj=getattr(obj, parent_accessor),
permissions=permissions, user=user
relation_result = []
for related_field_name in related_fields:
inherited_acl_queries = self._get_acl_filters(
queryset=queryset, stored_permission=stored_permission,
related_field_name=related_field_name, user=user
)
except AttributeError:
# Has no such attribute, try it as a related field
try:
return self.check_access(
obj=return_related(
instance=obj, related_field=parent_accessor
), permissions=permissions, user=user
)
except PermissionDenied:
pass
except PermissionDenied:
pass
relation_result.append(reduce(operator.and_, inherited_acl_queries))
user_roles = []
for group in user.groups.all():
for role in group.roles.all():
if set(stored_permissions).intersection(set(self.get_inherited_permissions(role=role, obj=obj))):
logger.debug(
'Permissions "%s" on "%s" granted to user "%s" through role "%s" via inherited ACL',
permissions, obj, user, role
)
return True
result.append(reduce(operator.or_, relation_result))
user_roles.append(role)
return result
if not self.filter(content_type=ContentType.objects.get_for_model(model=obj), object_id=obj.pk, permissions__in=stored_permissions, role__in=user_roles).exists():
logger.debug(
'Permissions "%s" on "%s" denied for user "%s"',
permissions, obj, user
)
raise PermissionDenied(ugettext('Insufficient access for: %s') % obj)
def check_access(self, obj, permission, user, raise_404=False):
warnings.warn(
'check_access() is deprecated, use restrict_queryset() to '
'produce a queryset from which to .get() the corresponding '
'object in the local code.', InterfaceWarning
)
queryset = self.restrict_queryset(
permission=permission, queryset=obj._meta.default_manager.all(),
user=user
)
logger.debug(
'Permissions "%s" on "%s" granted to user "%s" through roles "%s" by direct ACL',
permissions, obj, user, user_roles
)
def filter_by_access(self, permission, user, queryset):
if user.is_superuser or user.is_staff:
logger.debug(
'Unfiltered queryset returned to user "%s" as superuser '
'or staff', user
)
return queryset
try:
Permission.check_permissions(
requester=user, permissions=(permission,)
)
except PermissionDenied:
user_roles = []
for group in user.groups.all():
for role in group.roles.all():
user_roles.append(role)
try:
parent_accessor = ModelPermission.get_inheritance(
model=queryset.model
)
except KeyError:
parent_acl_query = Q()
if queryset.filter(pk=obj.pk).exists():
return True
else:
if raise_404:
raise Http404
else:
instance = queryset.first()
if instance:
parent_object = return_related(
instance=instance, related_field=parent_accessor
)
raise PermissionDenied
try:
# Try to see if parent_object is a function
parent_object()
except TypeError:
# Is not a function, try it as a field
parent_content_type = ContentType.objects.get_for_model(
model=parent_object
)
parent_queryset = self.filter(
content_type=parent_content_type,
role__in=user_roles,
permissions=permission.stored_permission
)
parent_acl_query = Q(
**{
'{}__pk__in'.format(
parent_accessor
): parent_queryset.values_list(
'object_id', flat=True
)
}
)
else:
# Is a function. Can't perform Q object filtering.
# Perform iterative filtering.
result = []
for entry in queryset:
try:
self.check_access(
obj=entry, permissions=permission,
user=user
)
except PermissionDenied:
pass
else:
result.append(entry.pk)
def get_inherited_permissions(self, obj, role):
queryset = self._get_inherited_object_permissions(obj=obj, role=role)
return queryset.filter(pk__in=result)
else:
parent_acl_query = Q()
queryset = queryset | role.permissions.all()
# Directly granted access
content_type = ContentType.objects.get_for_model(
model=queryset.model
)
acl_query = Q(pk__in=self.filter(
content_type=content_type, role__in=user_roles,
permissions=permission.stored_permission
).values_list('object_id', flat=True))
logger.debug(
'Filtered queryset returned to user "%s" based on roles "%s"',
user, user_roles
)
# Filter the permissions to the ones that apply to the model
queryset = ModelPermission.get_for_instance(
instance=obj
).filter(
pk__in=queryset
)
return queryset.filter(parent_acl_query | acl_query)
else:
return queryset
def _get_inherited_object_permissions(self, obj, role):
queryset = StoredPermission.objects.none()
if not obj:
return queryset
def get_inherited_permissions(self, role, obj):
try:
instance = obj.first()
except AttributeError:
instance = obj
else:
if not instance:
return StoredPermission.objects.none()
try:
parent_accessor = ModelPermission.get_inheritance(
model=type(instance)
related_fields = ModelPermission.get_inheritances(
model=type(obj)
)
except KeyError:
return StoredPermission.objects.none()
pass
else:
try:
parent_object = resolve_attribute(
obj=instance, attribute=parent_accessor
)
except AttributeError:
# Parent accessor is not an attribute, try it as a related
# field.
parent_object = return_related(
instance=instance, related_field=parent_accessor
)
content_type = ContentType.objects.get_for_model(parent_object)
try:
return self.get(
role=role, content_type=content_type,
object_id=parent_object.pk
).permissions.all()
except self.model.DoesNotExist:
return StoredPermission.objects.none()
for related_field_name in related_fields:
try:
parent_object = resolve_attribute(
obj=obj, attribute=related_field_name
)
except AttributeError:
# Parent accessor is not an attribute, try it as a related
# field.
parent_object = return_related(
instance=obj, related_field=related_field_name
)
content_type = ContentType.objects.get_for_model(model=parent_object)
try:
queryset = queryset | self.get(
content_type=content_type, object_id=parent_object.pk,
role=role
).permissions.all()
except self.model.DoesNotExist:
pass
def grant(self, permission, role, obj):
queryset = queryset | self._get_inherited_object_permissions(
obj=parent_object, role=role
)
return queryset
def grant(self, obj, permission, role):
class_permissions = ModelPermission.get_for_class(klass=obj.__class__)
if permission not in class_permissions:
raise PermissionNotValidForClass
@@ -242,7 +227,42 @@ class AccessControlListManager(models.Manager):
return acl
def revoke(self, permission, role, obj):
def restrict_queryset_by_accesses(self, operator, permissions, queryset, user):
result = []
for permission in permissions:
result.append(
self.restrict_queryset(
permission=permission, queryset=queryset, user=user
)
)
return reduce(operator, result)
def restrict_queryset(self, permission, queryset, user):
# Check directly granted permission via a role
try:
Permission.check_user_permission(permission=permission, user=user)
except PermissionDenied:
acl_filters = self._get_acl_filters(
queryset=queryset,
stored_permission=permission.stored_permission, user=user
)
final_query = None
for acl_filter in acl_filters:
if final_query is None:
final_query = acl_filter
else:
final_query = final_query | acl_filter
return queryset.filter(final_query)
else:
# User has direct permission assignment via a role, is superuser or
# is staff. Return the entire queryset.
return queryset
def revoke(self, obj, permission, role):
content_type = ContentType.objects.get_for_model(model=obj)
acl, created = self.get_or_create(
content_type=content_type, object_id=obj.pk,

View File

@@ -1,16 +1,18 @@
from __future__ import absolute_import, unicode_literals
import logging
import operator
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db import models, transaction
from django.urls import reverse
from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _
from mayan.apps.permissions.models import Role, StoredPermission
from .events import event_acl_created, event_acl_edited
from .managers import AccessControlListManager
logger = logging.getLogger(__name__)
@@ -30,6 +32,11 @@ class AccessControlList(models.Model):
* Role - Custom role that is being granted a permission. Roles are created
in the Setup menu.
"""
# Multiple inheritance operator types
OPERATOR_AND = operator.and_
OPERATOR_OR = operator.or_
operator_default = OPERATOR_AND
content_type = models.ForeignKey(
on_delete=models.CASCADE, related_name='object_content_type',
to=ContentType
@@ -58,16 +65,15 @@ class AccessControlList(models.Model):
def __str__(self):
return _(
'Permissions "%(permissions)s" to role "%(role)s" for "%(object)s"'
'Role "%(role)s" permission\'s for "%(object)s"'
) % {
'permissions': self.get_permission_titles(),
'object': self.content_object,
'role': self.role
'role': self.role,
}
def get_absolute_url(self):
return reverse(
viewname='acls:acl_permissions', kwargs={'acl_pk': self.pk}
viewname='acls:acl_permissions', kwargs={'acl_id': self.pk}
)
def get_inherited_permissions(self):
@@ -85,3 +91,32 @@ class AccessControlList(models.Model):
return result or _('None')
get_permission_titles.short_description = _('Permissions')
def permissions_add(self, queryset, _user=None):
with transaction.atomic():
event_acl_edited.commit(
actor=_user, target=self
)
self.permissions.add(*queryset)
def permissions_remove(self, queryset, _user=None):
with transaction.atomic():
event_acl_edited.commit(
actor=_user, target=self
)
self.permissions.remove(*queryset)
def save(self, *args, **kwargs):
_user = kwargs.pop('_user', None)
with transaction.atomic():
is_new = not self.pk
super(AccessControlList, self).save(*args, **kwargs)
if is_new:
event_acl_created.commit(
actor=_user, target=self
)
else:
event_acl_edited.commit(
actor=_user, target=self
)

View File

@@ -1,216 +1,143 @@
from __future__ import absolute_import, unicode_literals
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework.reverse import reverse
from mayan.apps.common.serializers import ContentTypeSerializer
from mayan.apps.permissions import Permission
from mayan.apps.permissions.models import Role, StoredPermission
from mayan.apps.permissions.serializers import (
PermissionSerializer, RoleSerializer
)
from mayan.apps.permissions.models import Role
from mayan.apps.permissions.permissions import permission_role_edit
from mayan.apps.permissions.serializers import RoleSerializer
from mayan.apps.rest_api.mixins import ExternalObjectSerializerMixin
from mayan.apps.rest_api.relations import MultiKwargHyperlinkedIdentityField
from .models import AccessControlList
class AccessControlListSerializer(serializers.ModelSerializer):
class AccessControlListSerializer(ExternalObjectSerializerMixin, serializers.ModelSerializer):
content_type = ContentTypeSerializer(read_only=True)
permissions_url = serializers.SerializerMethodField(
help_text=_(
'API URL pointing to the list of permissions for this access '
'control list.'
)
)
role = RoleSerializer(read_only=True)
url = serializers.SerializerMethodField()
permission_add_url = MultiKwargHyperlinkedIdentityField(
view_kwargs=(
{
'lookup_field': 'content_type__app_label', 'lookup_url_kwarg': 'app_label',
},
{
'lookup_field': 'content_type__model', 'lookup_url_kwarg': 'model_name',
},
{
'lookup_field': 'object_id', 'lookup_url_kwarg': 'object_id',
},
{
'lookup_field': 'pk', 'lookup_url_kwarg': 'acl_id',
}
),
view_name='rest_api:object-acl-permission-add'
)
permission_list_url = MultiKwargHyperlinkedIdentityField(
view_kwargs=(
{
'lookup_field': 'content_type__app_label', 'lookup_url_kwarg': 'app_label',
},
{
'lookup_field': 'content_type__model', 'lookup_url_kwarg': 'model_name',
},
{
'lookup_field': 'object_id', 'lookup_url_kwarg': 'object_id',
},
{
'lookup_field': 'pk', 'lookup_url_kwarg': 'acl_id',
}
),
view_name='rest_api:object-acl-permission-list'
)
permission_list_inherited_url = MultiKwargHyperlinkedIdentityField(
view_kwargs=(
{
'lookup_field': 'content_type__app_label', 'lookup_url_kwarg': 'app_label',
},
{
'lookup_field': 'content_type__model', 'lookup_url_kwarg': 'model_name',
},
{
'lookup_field': 'object_id', 'lookup_url_kwarg': 'object_id',
},
{
'lookup_field': 'pk', 'lookup_url_kwarg': 'acl_id',
}
),
view_name='rest_api:object-acl-permission-inherited-list'
)
permission_remove_url = MultiKwargHyperlinkedIdentityField(
view_kwargs=(
{
'lookup_field': 'content_type__app_label', 'lookup_url_kwarg': 'app_label',
},
{
'lookup_field': 'content_type__model', 'lookup_url_kwarg': 'model_name',
},
{
'lookup_field': 'object_id', 'lookup_url_kwarg': 'object_id',
},
{
'lookup_field': 'pk', 'lookup_url_kwarg': 'acl_id',
}
),
view_name='rest_api:object-acl-permission-remove'
)
role_id = serializers.CharField(
label=_('Role ID'),
help_text=_(
'Primary key of the role of the ACL that will be created or edited.'
), required=False, write_only=True
)
url = MultiKwargHyperlinkedIdentityField(
view_kwargs=(
{
'lookup_field': 'content_type__app_label', 'lookup_url_kwarg': 'app_label',
},
{
'lookup_field': 'content_type__model', 'lookup_url_kwarg': 'model_name',
},
{
'lookup_field': 'object_id', 'lookup_url_kwarg': 'object_id',
},
{
'lookup_field': 'pk', 'lookup_url_kwarg': 'acl_id',
}
),
view_name='rest_api:object-acl-detail'
)
class Meta:
external_object_model = Role
external_object_pk_field = 'role_id'
external_object_permission = permission_role_edit
fields = (
'content_type', 'id', 'object_id', 'permissions_url', 'role', 'url'
'content_type', 'id', 'object_id', 'permission_add_url',
'permission_list_url', 'permission_list_inherited_url',
'permission_remove_url', 'role', 'role_id',
'url'
)
model = AccessControlList
def get_permissions_url(self, instance):
return reverse(
viewname='rest_api:accesscontrollist-permission-list', kwargs={
'app_label': instance.content_type.app_label,
'model': instance.content_type.model,
'object_id': instance.object_id,
'acl_pk': instance.pk
}, request=self.context['request'], format=self.context['format']
)
def get_url(self, instance):
return reverse(
'rest_api:accesscontrollist-detail', kwargs={
'app_label': instance.content_type.app_label,
'model': instance.content_type.model,
'object_id': instance.object_id,
'acl_pk': instance.pk
}, request=self.context['request'], format=self.context['format']
)
class AccessControlListPermissionSerializer(PermissionSerializer):
acl_permission_url = serializers.SerializerMethodField(
help_text=_(
'API URL pointing to a permission in relation to the '
'access control list to which it is attached. This URL is '
'different than the canonical workflow URL.'
)
)
acl_url = serializers.SerializerMethodField()
def get_acl_permission_url(self, instance):
return reverse(
'rest_api:accesscontrollist-permission-detail', kwargs={
'app_label': self.context['acl'].content_type.app_label,
'model': self.context['acl'].content_type.model,
'object_id': self.context['acl'].object_id,
'acl_pk': self.context['acl'].pk,
'permission_pk': instance.stored_permission.pk
}, request=self.context['request'], format=self.context['format']
)
def get_acl_url(self, instance):
return reverse(
'rest_api:accesscontrollist-detail', kwargs={
'app_label': self.context['acl'].content_type.app_label,
'model': self.context['acl'].content_type.model,
'object_id': self.context['acl'].object_id,
'acl_pk': self.context['acl'].pk
}, request=self.context['request'], format=self.context['format']
)
class WritableAccessControlListPermissionSerializer(AccessControlListPermissionSerializer):
permission_pk = serializers.CharField(
help_text=_(
'Primary key of the new permission to grant to the access control '
'list.'
), write_only=True
)
class Meta:
fields = ('namespace',)
read_only_fields = ('namespace',)
read_only_fields = ('object_id',)
def create(self, validated_data):
for permission in validated_data['permissions']:
self.context['acl'].permissions.add(permission)
role = self.get_external_object()
return validated_data['permissions'][0]
if role:
validated_data['role'] = role
def validate(self, attrs):
permissions_pk_list = attrs.pop('permission_pk', None)
permissions_result = []
if permissions_pk_list:
for pk in permissions_pk_list.split(','):
try:
permission = Permission.get(pk=pk)
except KeyError:
raise ValidationError(_('No such permission: %s') % pk)
else:
# Accumulate valid stored permission pks
permissions_result.append(permission.pk)
attrs['permissions'] = StoredPermission.objects.filter(
pk__in=permissions_result
)
return attrs
class WritableAccessControlListSerializer(serializers.ModelSerializer):
content_type = ContentTypeSerializer(read_only=True)
permissions_pk_list = serializers.CharField(
help_text=_(
'Comma separated list of permission primary keys to grant to this '
'access control list.'
), required=False
)
permissions_url = serializers.SerializerMethodField(
help_text=_(
'API URL pointing to the list of permissions for this access '
'control list.'
), read_only=True
)
role_pk = serializers.IntegerField(
help_text=_(
'Primary keys of the role to which this access control list '
'binds to.'
), write_only=True
)
url = serializers.SerializerMethodField()
class Meta:
fields = (
'content_type', 'id', 'object_id', 'permissions_pk_list',
'permissions_url', 'role_pk', 'url'
)
model = AccessControlList
read_only_fields = ('content_type', 'object_id')
def get_permissions_url(self, instance):
return reverse(
'rest_api:accesscontrollist-permission-list', kwargs={
'app_label': instance.content_type.app_label,
'model': instance.content_type.model,
'object_id': instance.object_id,
'acl_pk': instance.pk
}, request=self.context['request'], format=self.context['format']
return super(AccessControlListSerializer, self).create(
validated_data=validated_data
)
def get_url(self, instance):
return reverse(
'rest_api:accesscontrollist-detail', kwargs={
'app_label': instance.content_type.app_label,
'model': instance.content_type.model,
'object_id': instance.object_id,
'acl_pk': instance.pk
}, request=self.context['request'], format=self.context['format']
def update(self, instance, validated_data):
role = self.get_external_object()
if role:
validated_data['role'] = role
return super(AccessControlListSerializer, self).update(
instance=instance, validated_data=validated_data
)
def validate(self, attrs):
attrs['content_type'] = ContentType.objects.get_for_model(
self.context['content_object']
)
attrs['object_id'] = self.context['content_object'].pk
try:
attrs['role'] = Role.objects.get(pk=attrs.pop('role_pk'))
except Role.DoesNotExist as exception:
raise ValidationError(force_text(exception))
permissions_pk_list = attrs.pop('permissions_pk_list', None)
permissions_result = []
if permissions_pk_list:
for pk in permissions_pk_list.split(','):
try:
permission = Permission.get(pk=pk)
except KeyError:
raise ValidationError(_('No such permission: %s') % pk)
else:
# Accumulate valid stored permission pks
permissions_result.append(permission.pk)
instance = AccessControlList(**attrs)
try:
instance.full_clean()
except DjangoValidationError as exception:
raise ValidationError(exception)
# Add a queryset of valid stored permissions so that they get added
# after the ACL gets created.
attrs['permissions'] = StoredPermission.objects.filter(
pk__in=permissions_result
)
return attrs

View File

@@ -1,47 +1,73 @@
from __future__ import unicode_literals
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ImproperlyConfigured
from mayan.apps.permissions.models import Role
from mayan.apps.permissions.tests.literals import TEST_ROLE_LABEL
from mayan.apps.user_management.tests import (
TEST_ADMIN_EMAIL, TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME,
TEST_GROUP_NAME, TEST_USER_EMAIL, TEST_USER_PASSWORD, TEST_USER_USERNAME
from mayan.apps.common.tests.mixins import TestModelTestMixin
from mayan.apps.permissions.tests.mixins import (
PermissionTestMixin, RoleTestCaseMixin, RoleTestMixin
)
from mayan.apps.user_management.tests.mixins import UserTestCaseMixin
from ..classes import ModelPermission
from ..models import AccessControlList
from ..permissions import permission_acl_edit, permission_acl_view
class ACLBaseTestMixin(object):
auto_create_group = True
auto_create_users = True
class ACLTestCaseMixin(RoleTestCaseMixin, UserTestCaseMixin):
def setUp(self):
super(ACLBaseTestMixin, self).setUp()
if self.auto_create_users:
self.admin_user = get_user_model().objects.create_superuser(
username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL,
password=TEST_ADMIN_PASSWORD
)
self.user = get_user_model().objects.create_user(
username=TEST_USER_USERNAME, email=TEST_USER_EMAIL,
password=TEST_USER_PASSWORD
)
if self.auto_create_group:
self.group = Group.objects.create(name=TEST_GROUP_NAME)
self.role = Role.objects.create(label=TEST_ROLE_LABEL)
self.group.user_set.add(self.user)
self.role.groups.add(self.group)
super(ACLTestCaseMixin, self).setUp()
if hasattr(self, '_test_case_user'):
self._test_case_role.groups.add(self._test_case_group)
def grant_access(self, obj, permission):
return AccessControlList.objects.grant(
obj=obj, permission=permission, role=self.role
if not hasattr(self, '_test_case_role'):
raise ImproperlyConfigured(
'Enable the creation of the test case user, group, and role '
'in order to enable the usage of ACLs in tests.'
)
self._test_case_acl = AccessControlList.objects.grant(
obj=obj, permission=permission, role=self._test_case_role
)
def grant_permission(self, permission):
self.role.permissions.add(
permission.stored_permission
class ACLTestMixin(PermissionTestMixin, RoleTestMixin, TestModelTestMixin):
auto_create_test_role = True
def _create_test_acl(self):
self.test_acl = AccessControlList.objects.create(
content_object=self.test_object, role=self.test_role
)
def setUp(self):
super(ACLTestMixin, self).setUp()
if self.auto_create_test_role:
self._create_test_role()
def _inject_test_object_content_type(self):
self.test_object_content_type = ContentType.objects.get_for_model(self.test_object)
self.test_content_object_view_kwargs = {
'app_label': self.test_object_content_type.app_label,
'model_name': self.test_object_content_type.model,
'object_id': self.test_object.pk
}
def _setup_test_object(self):
self._create_test_model()
self._create_test_object()
ModelPermission.register(
model=self.test_object._meta.model, permissions=(
permission_acl_edit, permission_acl_view,
)
)
self._create_test_permission()
ModelPermission.register(
model=self.test_object._meta.model, permissions=(
self.test_permission,
)
)
self._inject_test_object_content_type()

View File

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

View File

@@ -1,205 +1,189 @@
from __future__ import absolute_import, unicode_literals
from django.contrib.contenttypes.models import ContentType
from rest_framework import status
from mayan.apps.documents.permissions import permission_document_view
from mayan.apps.documents.tests import DocumentTestMixin
from mayan.apps.permissions.tests.literals import TEST_ROLE_LABEL
from mayan.apps.rest_api.tests import BaseAPITestCase
from ..models import AccessControlList
from ..permissions import permission_acl_view
from ..permissions import permission_acl_edit, permission_acl_view
from .mixins import ACLTestMixin
class ACLAPITestCase(DocumentTestMixin, BaseAPITestCase):
class ACLAPITestCase(ACLTestMixin, BaseAPITestCase):
def setUp(self):
super(ACLAPITestCase, self).setUp()
self.login_admin_user()
self._setup_test_object()
self._create_test_acl()
self.test_acl.permissions.add(self.test_permission.stored_permission)
self.document_content_type = ContentType.objects.get_for_model(
self.document
def _request_object_acl_list_api_view(self):
return self.get(
viewname='rest_api:object-acl-list',
kwargs=self.test_content_object_view_kwargs
)
def _create_acl(self):
self.acl = AccessControlList.objects.create(
content_object=self.document,
role=self.role
)
def test_object_acl_list_api_view_no_permission(self):
response = self._request_object_acl_list_api_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.acl.permissions.add(permission_document_view.stored_permission)
def test_object_acl_list_api_view_with_access(self):
self.grant_access(obj=self.test_object, permission=permission_acl_view)
def test_object_acl_list_view(self):
self._create_acl()
response = self.get(
viewname='rest_api:accesscontrollist-list',
kwargs={
'app_label': self.document_content_type.app_label,
'model': self.document_content_type.model,
'object_id': self.document.pk
}
)
response = self._request_object_acl_list_api_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data['results'][0]['content_type']['app_label'],
self.document_content_type.app_label
self.test_object_content_type.app_label
)
self.assertEqual(
response.data['results'][0]['role']['label'], TEST_ROLE_LABEL
response.data['results'][0]['role']['label'],
self.test_acl.role.label
)
def test_object_acl_delete_view(self):
self._create_acl()
def _request_acl_delete_api_view(self):
kwargs = self.test_content_object_view_kwargs.copy()
kwargs['acl_id'] = self.test_acl.pk
response = self.delete(
viewname='rest_api:accesscontrollist-detail',
kwargs={
'app_label': self.document_content_type.app_label,
'model': self.document_content_type.model,
'object_id': self.document.pk,
'acl_pk': self.acl.pk
}
return self.delete(
viewname='rest_api:object-acl-detail',
kwargs=kwargs
)
def test_object_acl_delete_api_view_with_access(self):
self.expected_content_type = None
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
response = self._request_acl_delete_api_view()
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertEqual(AccessControlList.objects.count(), 0)
self.assertTrue(self.test_acl not in AccessControlList.objects.all())
def test_object_acl_detail_view(self):
self._create_acl()
def test_object_acl_delete_api_view_no_permission(self):
response = self._request_acl_delete_api_view()
response = self.get(
viewname='rest_api:accesscontrollist-detail',
kwargs={
'app_label': self.document_content_type.app_label,
'model': self.document_content_type.model,
'object_id': self.document.pk,
'acl_pk': self.acl.pk
}
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertTrue(self.test_acl in AccessControlList.objects.all())
def _request_object_acl_detail_api_view(self):
kwargs = self.test_content_object_view_kwargs.copy()
kwargs['acl_id'] = self.test_acl.pk
return self.get(
viewname='rest_api:object-acl-detail',
kwargs=kwargs
)
def test_object_acl_detail_api_view_with_access(self):
self.grant_access(obj=self.test_object, permission=permission_acl_view)
response = self._request_object_acl_detail_api_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data['content_type']['app_label'],
self.document_content_type.app_label
self.test_object_content_type.app_label
)
self.assertEqual(
response.data['role']['label'], TEST_ROLE_LABEL
response.data['role']['label'], self.test_acl.role.label
)
def test_object_acl_permission_delete_view(self):
self._create_acl()
permission = self.acl.permissions.first()
def test_object_acl_detail_api_view_no_permission(self):
response = self._request_object_acl_detail_api_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
response = self.delete(
viewname='rest_api:accesscontrollist-permission-detail',
kwargs={
'app_label': self.document_content_type.app_label,
'model': self.document_content_type.model,
'object_id': self.document.pk,
'acl_pk': self.acl.pk, 'permission_pk': permission.pk
}
)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertEqual(self.acl.permissions.count(), 0)
def _request_object_acl_permission_list_api_view(self):
kwargs = self.test_content_object_view_kwargs.copy()
kwargs['acl_id'] = self.test_acl.pk
def test_object_acl_permission_detail_view(self):
self._create_acl()
permission = self.acl.permissions.first()
response = self.get(
viewname='rest_api:accesscontrollist-permission-detail',
kwargs={
'app_label': self.document_content_type.app_label,
'model': self.document_content_type.model,
'object_id': self.document.pk, 'acl_pk': self.acl.pk,
'permission_pk': permission.pk
}
return self.get(
viewname='rest_api:object-acl-permission-list',
kwargs=kwargs
)
def test_object_acl_permission_list_api_view_with_access(self):
self.grant_access(obj=self.test_object, permission=permission_acl_view)
response = self._request_object_acl_permission_list_api_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data['permission_pk'], permission_document_view.pk
response.data['results'][0]['pk'],
self.test_permission.pk
)
def test_object_acl_permission_list_view(self):
self._create_acl()
def test_object_acl_permission_list_api_view_no_permission(self):
response = self._request_object_acl_permission_list_api_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
response = self.get(
viewname='rest_api:accesscontrollist-permission-list',
kwargs={
'app_label': self.document_content_type.app_label,
'model': self.document_content_type.model,
'object_id': self.document.pk,
'acl_pk': self.acl.pk
}
def _request_object_acl_permission_remove_api_view(self):
kwargs = self.test_content_object_view_kwargs.copy()
kwargs['acl_id'] = self.test_acl.pk
return self.post(
viewname='rest_api:object-acl-permission-remove',
kwargs=kwargs, data={'permission_id_list': self.test_permission.pk}
)
def test_object_acl_permission_remove_api_view_with_access(self):
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
response = self._request_object_acl_permission_remove_api_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(self.test_permission.stored_permission not in self.test_acl.permissions.all())
def test_object_acl_permission_remove_api_view_no_permission(self):
response = self._request_object_acl_permission_remove_api_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertTrue(self.test_permission.stored_permission in self.test_acl.permissions.all())
def _request_object_acl_permission_add_api_view(self):
kwargs = self.test_content_object_view_kwargs.copy()
kwargs['acl_id'] = self.test_acl.pk
return self.post(
viewname='rest_api:object-acl-permission-add',
kwargs=kwargs, data={'permission_id_list': self.test_permission.pk}
)
def test_object_acl_permission_add_api_view_with_access(self):
self.test_acl.permissions.clear()
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
response = self._request_object_acl_permission_add_api_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(self.test_permission.stored_permission in self.test_acl.permissions.all())
def test_object_acl_permission_add_api_view_no_permission(self):
self.test_acl.permissions.clear()
response = self._request_object_acl_permission_add_api_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertTrue(self.test_permission.stored_permission not in self.test_acl.permissions.all())
def _request_object_acl_inherited_permission_list_api_view(self):
kwargs = self.test_content_object_view_kwargs.copy()
kwargs['acl_id'] = self.test_acl.pk
return self.get(
viewname='rest_api:object-acl-permission-inherited-list',
kwargs=kwargs
)
def test_object_acl_inherited_permission_list_api_view_with_access(self):
self.test_acl.permissions.clear()
self.test_role.grant(permission=self.test_permission)
self.grant_access(obj=self.test_object, permission=permission_acl_view)
response = self._request_object_acl_inherited_permission_list_api_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data['results'][0]['permission_pk'],
permission_document_view.pk
response.data['results'][0]['pk'],
self.test_permission.pk
)
def test_object_acl_permission_list_post_view(self):
self._create_acl()
def test_object_acl_inherited_permission_list_api_view_no_permission(self):
self.test_acl.permissions.clear()
self.test_role.grant(permission=self.test_permission)
response = self.post(
viewname='rest_api:accesscontrollist-permission-list',
kwargs={
'app_label': self.document_content_type.app_label,
'model': self.document_content_type.model,
'object_id': self.document.pk, 'acl_pk': self.acl.pk
}, data={'permission_pk': permission_acl_view.pk}
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertQuerysetEqual(
ordered=False, qs=self.acl.permissions.all(), values=(
repr(permission_document_view.stored_permission),
repr(permission_acl_view.stored_permission)
)
)
def test_object_acl_post_no_permissions_added_view(self):
response = self.post(
viewname='rest_api:accesscontrollist-list',
kwargs={
'app_label': self.document_content_type.app_label,
'model': self.document_content_type.model,
'object_id': self.document.pk
}, data={'role_pk': self.role.pk}
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(
self.document.acls.first().role, self.role
)
self.assertEqual(
self.document.acls.first().content_object, self.document
)
self.assertEqual(
self.document.acls.first().permissions.count(), 0
)
def test_object_acl_post_with_permissions_added_view(self):
response = self.post(
viewname='rest_api:accesscontrollist-list',
kwargs={
'app_label': self.document_content_type.app_label,
'model': self.document_content_type.model,
'object_id': self.document.pk
}, data={
'role_pk': self.role.pk,
'permissions_pk_list': permission_acl_view.pk
}
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(
self.document.acls.first().content_object, self.document
)
self.assertEqual(
self.document.acls.first().role, self.role
)
self.assertEqual(
self.document.acls.first().permissions.first(),
permission_acl_view.stored_permission
)
response = self._request_object_acl_inherited_permission_list_api_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)

View File

@@ -1,100 +1,84 @@
from __future__ import unicode_literals
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from mayan.apps.documents.tests import GenericDocumentViewTestCase
from mayan.apps.common.tests import GenericViewTestCase
from ..links import (
link_acl_create, link_acl_delete, link_acl_list, link_acl_permissions
)
from ..models import AccessControlList
from ..permissions import permission_acl_edit, permission_acl_view
from .mixins import ACLTestMixin
class ACLsLinksTestCase(GenericDocumentViewTestCase):
def test_document_acl_create_link(self):
acl = AccessControlList.objects.create(
content_object=self.document, role=self.role
)
acl.permissions.add(permission_acl_edit.stored_permission)
self.login_user()
class AccessControlListLinksTestCase(ACLTestMixin, GenericViewTestCase):
auto_create_test_role = False
self.add_test_view(test_object=self.document)
def setUp(self):
super(AccessControlListLinksTestCase, self).setUp()
self._setup_test_object()
def test_object_acl_create_link(self):
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
self.add_test_view(test_object=self.test_object)
context = self.get_test_view()
resolved_link = link_acl_create.resolve(context=context)
self.assertNotEqual(resolved_link, None)
content_type = ContentType.objects.get_for_model(self.document)
kwargs = {
'app_label': content_type.app_label,
'model': content_type.model,
'object_id': self.document.pk
}
self.assertEqual(
resolved_link.url, reverse(viewname='acls:acl_create', kwargs=kwargs)
resolved_link.url, reverse(
viewname='acls:acl_create',
kwargs=self.test_content_object_view_kwargs
)
)
def test_document_acl_delete_link(self):
acl = AccessControlList.objects.create(
content_object=self.document, role=self.role
)
def test_object_acl_delete_link(self):
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
acl.permissions.add(permission_acl_edit.stored_permission)
self.login_user()
self.add_test_view(test_object=acl)
self.add_test_view(test_object=self._test_case_acl)
context = self.get_test_view()
resolved_link = link_acl_delete.resolve(context=context)
self.assertNotEqual(resolved_link, None)
self.assertEqual(
resolved_link.url, reverse(viewname='acls:acl_delete', kwargs={'acl_pk': acl.pk})
resolved_link.url, reverse(
viewname='acls:acl_delete',
kwargs={'acl_id': self._test_case_acl.pk}
)
)
def test_document_acl_edit_link(self):
acl = AccessControlList.objects.create(
content_object=self.document, role=self.role
)
def test_object_acl_edit_link(self):
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
acl.permissions.add(permission_acl_edit.stored_permission)
self.login_user()
self.add_test_view(test_object=acl)
self.add_test_view(test_object=self._test_case_acl)
context = self.get_test_view()
resolved_link = link_acl_permissions.resolve(context=context)
self.assertNotEqual(resolved_link, None)
self.assertEqual(
resolved_link.url, reverse(viewname='acls:acl_permissions', kwargs={'acl_pk': acl.pk})
resolved_link.url, reverse(
viewname='acls:acl_permissions',
kwargs={'acl_id': self._test_case_acl.pk}
)
)
def test_document_acl_list_link(self):
acl = AccessControlList.objects.create(
content_object=self.document, role=self.role
)
def test_object_acl_list_link(self):
self.grant_access(obj=self.test_object, permission=permission_acl_view)
acl.permissions.add(permission_acl_view.stored_permission)
self.login_user()
self.add_test_view(test_object=self.document)
self.add_test_view(test_object=self.test_object)
context = self.get_test_view()
resolved_link = link_acl_list.resolve(context=context)
self.assertNotEqual(resolved_link, None)
content_type = ContentType.objects.get_for_model(self.document)
kwargs = {
'app_label': content_type.app_label,
'model': content_type.model,
'object_id': self.document.pk
}
self.assertEqual(
resolved_link.url, reverse(viewname='acls:acl_list', kwargs=kwargs)
resolved_link.url, reverse(
viewname='acls:acl_list',
kwargs=self.test_content_object_view_kwargs
)
)

View File

@@ -1,159 +1,401 @@
from __future__ import absolute_import, unicode_literals
from django.core.exceptions import PermissionDenied
from django.db import models
from mayan.apps.common.tests import BaseTestCase
from mayan.apps.documents.models import Document, DocumentType
from mayan.apps.documents.permissions import permission_document_view
from mayan.apps.documents.tests import (
TEST_DOCUMENT_TYPE_2_LABEL, TEST_DOCUMENT_TYPE_LABEL,
TEST_SMALL_DOCUMENT_PATH
DocumentTestMixin, TEST_DOCUMENT_TYPE_2_LABEL, TEST_DOCUMENT_TYPE_LABEL
)
from ..classes import ModelPermission
from ..models import AccessControlList
from .mixins import ACLTestMixin
class PermissionTestCase(DocumentTestMixin, BaseTestCase):
auto_create_document_type = False
class PermissionTestCase(BaseTestCase):
def setUp(self):
super(PermissionTestCase, self).setUp()
self.document_type_1 = DocumentType.objects.create(
self.test_document_type_1 = DocumentType.objects.create(
label=TEST_DOCUMENT_TYPE_LABEL
)
self.document_type_2 = DocumentType.objects.create(
self.test_document_type_2 = DocumentType.objects.create(
label=TEST_DOCUMENT_TYPE_2_LABEL
)
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
self.document_1 = self.document_type_1.new_document(
file_object=file_object
)
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
self.document_2 = self.document_type_1.new_document(
file_object=file_object
)
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
self.document_3 = self.document_type_2.new_document(
file_object=file_object
)
def tearDown(self):
for document_type in DocumentType.objects.all():
document_type.delete()
super(PermissionTestCase, self).tearDown()
self.test_document_1 = self.upload_document(
document_type=self.test_document_type_1
)
self.test_document_2 = self.upload_document(
document_type=self.test_document_type_1
)
self.test_document_3 = self.upload_document(
document_type=self.test_document_type_2
)
def test_check_access_without_permissions(self):
with self.assertRaises(PermissionDenied):
AccessControlList.objects.check_access(
permissions=(permission_document_view,),
user=self.user, obj=self.document_1
obj=self.test_document_1, permission=permission_document_view,
user=self._test_case_user
)
def test_filtering_without_permissions(self):
self.assertQuerysetEqual(
AccessControlList.objects.filter_by_access(
permission=permission_document_view, user=self.user,
queryset=Document.objects.all()
), []
self.assertEqual(
AccessControlList.objects.restrict_queryset(
permission=permission_document_view,
queryset=Document.objects.all(), user=self._test_case_user,
).count(), 0
)
def test_check_access_with_acl(self):
acl = AccessControlList.objects.create(
content_object=self.document_1, role=self.role
content_object=self.test_document_1, role=self._test_case_role
)
acl.permissions.add(permission_document_view.stored_permission)
try:
AccessControlList.objects.check_access(
permissions=(permission_document_view,), user=self.user,
obj=self.document_1
obj=self.test_document_1, permission=permission_document_view,
user=self._test_case_user
)
except PermissionDenied:
self.fail('PermissionDenied exception was not expected.')
def test_filtering_with_permissions(self):
acl = AccessControlList.objects.create(
content_object=self.document_1, role=self.role
content_object=self.test_document_1, role=self._test_case_role
)
acl.permissions.add(permission_document_view.stored_permission)
self.assertQuerysetEqual(
AccessControlList.objects.filter_by_access(
permission=permission_document_view, user=self.user,
queryset=Document.objects.all()
), (repr(self.document_1),)
AccessControlList.objects.restrict_queryset(
permission=permission_document_view,
queryset=Document.objects.all(), user=self._test_case_user
), (repr(self.test_document_1),)
)
def test_check_access_with_inherited_acl(self):
acl = AccessControlList.objects.create(
content_object=self.document_type_1, role=self.role
content_object=self.test_document_type_1, role=self._test_case_role
)
acl.permissions.add(permission_document_view.stored_permission)
try:
AccessControlList.objects.check_access(
permissions=(permission_document_view,), user=self.user,
obj=self.document_1
obj=self.test_document_1, permission=permission_document_view,
user=self._test_case_user
)
except PermissionDenied:
self.fail('PermissionDenied exception was not expected.')
def test_check_access_with_inherited_acl_and_local_acl(self):
acl = AccessControlList.objects.create(
content_object=self.document_type_1, role=self.role
def test_check_access_with_inherited_acl_and_direct_acl(self):
test_acl_1 = AccessControlList.objects.create(
content_object=self.test_document_type_1, role=self._test_case_role
)
acl.permissions.add(permission_document_view.stored_permission)
test_acl_1.permissions.add(permission_document_view.stored_permission)
acl = AccessControlList.objects.create(
content_object=self.document_3, role=self.role
test_acl_2 = AccessControlList.objects.create(
content_object=self.test_document_3, role=self._test_case_role
)
acl.permissions.add(permission_document_view.stored_permission)
test_acl_2.permissions.add(permission_document_view.stored_permission)
try:
AccessControlList.objects.check_access(
permissions=(permission_document_view,), user=self.user,
obj=self.document_3
obj=self.test_document_3, permission=permission_document_view,
user=self._test_case_user
)
except PermissionDenied:
self.fail('PermissionDenied exception was not expected.')
def test_filtering_with_inherited_permissions(self):
acl = AccessControlList.objects.create(
content_object=self.document_type_1, role=self.role
content_object=self.test_document_type_1, role=self._test_case_role
)
acl.permissions.add(permission_document_view.stored_permission)
result = AccessControlList.objects.filter_by_access(
permission=permission_document_view, user=self.user,
queryset=Document.objects.all()
result = AccessControlList.objects.restrict_queryset(
permission=permission_document_view, queryset=Document.objects.all(),
user=self._test_case_user
)
# Since document_1 and document_2 are of document_type_1
# they are the only ones that should be returned
self.assertTrue(self.document_1 in result)
self.assertTrue(self.document_2 in result)
self.assertTrue(self.document_3 not in result)
self.assertTrue(self.test_document_1 in result)
self.assertTrue(self.test_document_2 in result)
self.assertTrue(self.test_document_3 not in result)
def test_filtering_with_inherited_permissions_and_local_acl(self):
self.role.permissions.add(permission_document_view.stored_permission)
self._test_case_role.permissions.add(
permission_document_view.stored_permission
)
acl = AccessControlList.objects.create(
content_object=self.document_type_1, role=self.role
content_object=self.test_document_type_1, role=self._test_case_role
)
acl.permissions.add(permission_document_view.stored_permission)
acl = AccessControlList.objects.create(
content_object=self.document_3, role=self.role
content_object=self.test_document_3, role=self._test_case_role
)
acl.permissions.add(permission_document_view.stored_permission)
result = AccessControlList.objects.filter_by_access(
permission=permission_document_view, user=self.user,
queryset=Document.objects.all()
result = AccessControlList.objects.restrict_queryset(
permission=permission_document_view, queryset=Document.objects.all(),
user=self._test_case_user,
)
self.assertTrue(self.document_1 in result)
self.assertTrue(self.document_2 in result)
self.assertTrue(self.document_3 in result)
self.assertTrue(self.test_document_1 in result)
self.assertTrue(self.test_document_2 in result)
self.assertTrue(self.test_document_3 in result)
class InheritedPermissionTestCase(ACLTestMixin, BaseTestCase):
def test_retrieve_inherited_role_permission_not_model_applicable(self):
self._create_test_model()
self.test_object = self.TestModel.objects.create()
self._create_test_acl()
self._create_test_permission()
self.test_role.grant(permission=self.test_permission)
queryset = AccessControlList.objects.get_inherited_permissions(
obj=self.test_object, role=self.test_role
)
self.assertTrue(self.test_permission.stored_permission not in queryset)
def test_retrieve_inherited_role_permission_model_applicable(self):
self._create_test_model()
self.test_object = self.TestModel.objects.create()
self._create_test_acl()
self._create_test_permission()
ModelPermission.register(
model=self.test_object._meta.model, permissions=(
self.test_permission,
)
)
self.test_role.grant(permission=self.test_permission)
queryset = AccessControlList.objects.get_inherited_permissions(
obj=self.test_object, role=self.test_role
)
self.assertTrue(self.test_permission.stored_permission in queryset)
def test_retrieve_inherited_related_parent_child_permission(self):
self._create_test_permission()
self._create_test_model(model_name='TestModelParent')
self._create_test_model(
fields={
'parent': models.ForeignKey(
on_delete=models.CASCADE, related_name='children',
to='TestModelParent',
)
}, model_name='TestModelChild'
)
ModelPermission.register(
model=self.TestModelParent, permissions=(
self.test_permission,
)
)
ModelPermission.register(
model=self.TestModelChild, permissions=(
self.test_permission,
)
)
ModelPermission.register_inheritance(
model=self.TestModelChild, related='parent',
)
parent = self.TestModelParent.objects.create()
child = self.TestModelChild.objects.create(parent=parent)
AccessControlList.objects.grant(
obj=parent, permission=self.test_permission, role=self.test_role
)
queryset = AccessControlList.objects.get_inherited_permissions(
obj=child, role=self.test_role
)
self.assertTrue(self.test_permission.stored_permission in queryset)
def test_retrieve_inherited_related_grandparent_parent_child_permission(self):
self._create_test_permission()
self._create_test_model(model_name='TestModelGrandParent')
self._create_test_model(
fields={
'parent': models.ForeignKey(
on_delete=models.CASCADE, related_name='children',
to='TestModelGrandParent',
)
}, model_name='TestModelParent'
)
self._create_test_model(
fields={
'parent': models.ForeignKey(
on_delete=models.CASCADE, related_name='children',
to='TestModelParent',
)
}, model_name='TestModelChild'
)
ModelPermission.register(
model=self.TestModelGrandParent, permissions=(
self.test_permission,
)
)
ModelPermission.register(
model=self.TestModelParent, permissions=(
self.test_permission,
)
)
ModelPermission.register(
model=self.TestModelChild, permissions=(
self.test_permission,
)
)
ModelPermission.register_inheritance(
model=self.TestModelChild, related='parent',
)
ModelPermission.register_inheritance(
model=self.TestModelParent, related='parent',
)
grandparent = self.TestModelGrandParent.objects.create()
parent = self.TestModelParent.objects.create(parent=grandparent)
child = self.TestModelChild.objects.create(parent=parent)
AccessControlList.objects.grant(
obj=grandparent, permission=self.test_permission,
role=self.test_role
)
queryset = AccessControlList.objects.get_inherited_permissions(
obj=child, role=self.test_role
)
self.assertTrue(self.test_permission.stored_permission in queryset)
class MultipleAccessTestCase(ACLTestMixin, BaseTestCase):
def setUp(self):
super(MultipleAccessTestCase, self).setUp()
self._create_test_permission()
self._create_test_permission_2()
self._create_test_model(model_name='TestModelParent1')
self._create_test_model(model_name='TestModelParent2')
self._create_test_model(
fields={
'parent_1': models.ForeignKey(
on_delete=models.CASCADE, related_name='children1',
to='TestModelParent1',
),
'parent_2': models.ForeignKey(
on_delete=models.CASCADE, related_name='children2',
to='TestModelParent2',
)
}, model_name='TestModelChild'
)
ModelPermission.register(
model=self.TestModelParent1, permissions=(
self.test_permission,
)
)
ModelPermission.register(
model=self.TestModelParent2, permissions=(
self.test_permission_2,
)
)
self.test_object_parent_1 = self.TestModelParent1.objects.create()
self.test_object_parent_2 = self.TestModelParent2.objects.create()
self.test_object_child = self.TestModelChild.objects.create(
parent_1=self.test_object_parent_1, parent_2=self.test_object_parent_2
)
ModelPermission.register_inheritance(
model=self.TestModelChild, related='parent_1'
)
ModelPermission.register_inheritance(
model=self.TestModelChild, related='parent_2'
)
def test_restrict_queryset_and_operator_first_permission(self):
self.grant_access(obj=self.test_object_parent_1, permission=self.test_permission)
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
operator=AccessControlList.OPERATOR_AND,
permissions=(self.test_permission, self.test_permission_2),
queryset=self.TestModelChild.objects.all(),
user=self._test_case_user
)
self.assertTrue(self.test_object_child not in queryset)
def test_restrict_queryset_and_operator_second_permission(self):
self.grant_access(obj=self.test_object_parent_2, permission=self.test_permission_2)
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
operator=AccessControlList.OPERATOR_AND,
permissions=(self.test_permission, self.test_permission_2),
queryset=self.TestModelChild.objects.all(),
user=self._test_case_user
)
self.assertTrue(self.test_object_child not in queryset)
def test_restrict_queryset_and_operator_both_permissions(self):
self.grant_access(obj=self.test_object_parent_1, permission=self.test_permission)
self.grant_access(obj=self.test_object_parent_2, permission=self.test_permission_2)
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
operator=AccessControlList.OPERATOR_AND,
permissions=(self.test_permission, self.test_permission_2),
queryset=self.TestModelChild.objects.all(),
user=self._test_case_user
)
self.assertTrue(self.test_object_child in queryset)
def test_restrict_queryset_or_operator_first_permission(self):
self.grant_access(obj=self.test_object_parent_1, permission=self.test_permission)
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
operator=AccessControlList.OPERATOR_OR,
permissions=(self.test_permission, self.test_permission_2),
queryset=self.TestModelChild.objects.all(),
user=self._test_case_user
)
self.assertTrue(self.test_object_child in queryset)
def test_restrict_queryset_or_operator_second_permission(self):
self.grant_access(obj=self.test_object_parent_2, permission=self.test_permission_2)
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
operator=AccessControlList.OPERATOR_OR,
permissions=(self.test_permission, self.test_permission_2),
queryset=self.TestModelChild.objects.all(),
user=self._test_case_user
)
self.assertTrue(self.test_object_child in queryset)
def test_restrict_queryset_or_operator_both_permissions(self):
self.grant_access(obj=self.test_object_parent_1, permission=self.test_permission)
self.grant_access(obj=self.test_object_parent_2, permission=self.test_permission_2)
queryset = AccessControlList.objects.restrict_queryset_by_accesses(
operator=AccessControlList.OPERATOR_OR,
permissions=(self.test_permission, self.test_permission_2),
queryset=self.TestModelChild.objects.all(),
user=self._test_case_user
)
self.assertTrue(self.test_object_child in queryset)

View File

@@ -1,136 +1,159 @@
from __future__ import absolute_import, unicode_literals
from django.contrib.contenttypes.models import ContentType
from django.utils.encoding import force_text
from mayan.apps.documents.tests import GenericDocumentViewTestCase
from mayan.apps.permissions.tests.mixins import RoleTestMixin
from mayan.apps.common.tests import GenericViewTestCase
from ..classes import ModelPermission
from ..models import AccessControlList
from ..permissions import permission_acl_edit, permission_acl_view
from .mixins import ACLTestMixin
class AccessControlListViewTestCase(RoleTestMixin, GenericDocumentViewTestCase):
class AccessControlListViewTestCase(ACLTestMixin, GenericViewTestCase):
def setUp(self):
super(AccessControlListViewTestCase, self).setUp()
self.login_user()
self._create_test_role()
self.test_object = self.document
self._create_test_model()
self._create_test_object()
ModelPermission.register(
model=self.test_object._meta.model, permissions=(
permission_acl_edit, permission_acl_view,
)
)
content_type = ContentType.objects.get_for_model(self.test_object)
self._create_test_permission()
ModelPermission.register(
model=self.test_object._meta.model, permissions=(
self.test_permission,
)
)
self.view_content_object_arguments = {
'app_label': content_type.app_label,
'model': content_type.model,
'object_id': self.test_object.pk
}
self._inject_test_object_content_type()
def _request_get_acl_create_view(self):
self._create_test_acl()
self.test_acl.permissions.add(self.test_permission.stored_permission)
def _request_acl_create_get_view(self):
return self.get(
viewname='acls:acl_create',
kwargs=self.view_content_object_arguments, data={
kwargs=self.test_content_object_view_kwargs, data={
'role': self.test_role.pk
}
)
def test_acl_create_view_get_no_permission(self):
response = self._request_get_acl_create_view()
def test_acl_create_get_view_no_permission(self):
self.test_acl.delete()
response = self._request_acl_create_get_view()
self.assertEqual(response.status_code, 404)
self.assertEqual(AccessControlList.objects.count(), 0)
def test_acl_create_view_get_with_document_access(self):
self.assertFalse(self.test_object.acls.filter(role=self.test_role).exists())
def test_acl_create_get_view_with_object_access(self):
self.test_acl.delete()
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
response = self._request_get_acl_create_view()
response = self._request_acl_create_get_view()
self.assertContains(
response=response, text=force_text(self.test_object),
status_code=200
)
def _request_post_acl_create_view(self):
self.assertFalse(self.test_object.acls.filter(role=self.test_role).exists())
def _request_acl_create_post_view(self):
return self.post(
viewname='acls:acl_create',
kwargs=self.view_content_object_arguments, data={
kwargs=self.test_content_object_view_kwargs, data={
'role': self.test_role.pk
}
)
def test_acl_create_view_post_no_permission(self):
response = self._request_post_acl_create_view()
self.test_acl.delete()
response = self._request_acl_create_post_view()
self.assertEqual(response.status_code, 404)
self.assertEqual(AccessControlList.objects.count(), 0)
def test_acl_create_view_post_with_document_access(self):
self.assertFalse(self.test_object.acls.filter(role=self.test_role).exists())
def test_acl_create_view_post_with_access(self):
self.test_acl.delete()
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
response = self._request_post_acl_create_view()
response = self._request_acl_create_post_view()
self.assertEqual(response.status_code, 302)
# 2 ACLs: 1 created by the test and the other by the self.grant_access
self.assertEqual(AccessControlList.objects.count(), 2)
def test_acl_create_duplicate_view_with_permission(self):
self.assertTrue(self.test_object.acls.filter(role=self.test_role).exists())
def test_acl_create_duplicate_view_with_access(self):
"""
Test creating a duplicate ACL entry: same object & role
Result: Should redirect to existing ACL for object + role combination
"""
self._create_test_acl()
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
response = self._request_post_acl_create_view()
response = self._request_acl_create_post_view()
self.assertNotContains(
response=response, text=force_text(self.test_acl.role),
status_code=200
)
# 2 ACLs: 1 created by the test and the other by the self.grant_access
self.assertEqual(AccessControlList.objects.count(), 2)
self.assertEqual(
AccessControlList.objects.first().pk, self.test_acl.pk
# Sorted by role PK
expected_results = sorted(
[
{
# Test role, created and then requested,
# but created only once
'object_id': self.test_object.pk,
'role': self.test_role.pk
},
{
# Test case ACL for the test case role, ignored
'object_id': self.test_object.pk,
'role': self._test_case_role.pk
},
], key=lambda item: item['role']
)
def _create_test_acl(self):
self.test_acl = AccessControlList.objects.create(
content_object=self.test_object, role=self.test_role
self.assertQuerysetEqual(
qs=AccessControlList.objects.order_by('role__id').values(
'object_id', 'role',
), transform=dict, values=expected_results
)
def _request_acl_delete_view(self):
return self.post(
viewname='acls:acl_delete', kwargs={'acl_pk': self.test_acl.pk}
viewname='acls:acl_delete', kwargs={'acl_id': self.test_acl.pk}
)
def test_acl_delete_view_no_permission(self):
self._create_test_acl()
response = self._request_acl_delete_view()
self.assertNotContains(
response=response, text=force_text(self.test_object),
status_code=404
)
# 1 ACL: the test one
self.assertQuerysetEqual(
qs=AccessControlList.objects.all(), values=(repr(self.test_acl),)
)
self.assertTrue(self.test_object.acls.filter(role=self.test_role).exists())
def test_acl_delete_view_with_access(self):
self._create_test_acl()
acl = self.grant_access(
self.grant_access(
obj=self.test_object, permission=permission_acl_edit
)
response = self._request_acl_delete_view()
self.assertEqual(response.status_code, 302)
# 1 ACL: the one created by the self.grant_access
self.assertQuerysetEqual(
qs=AccessControlList.objects.all(), values=(repr(acl),)
)
self.assertFalse(self.test_object.acls.filter(role=self.test_role).exists())
def _request_acl_list_view(self):
return self.get(
viewname='acls:acl_list', kwargs=self.view_content_object_arguments
viewname='acls:acl_list', kwargs=self.test_content_object_view_kwargs
)
def test_acl_list_view_no_permission(self):
@@ -151,28 +174,66 @@ class AccessControlListViewTestCase(RoleTestMixin, GenericDocumentViewTestCase):
status_code=200
)
def _request_get_acl_permissions_view(self):
def _request_get_acl_permissions_get_view(self):
return self.get(
viewname='acls:acl_permissions',
kwargs={'acl_pk': self.test_acl.pk}
kwargs={'acl_id': self.test_acl.pk}
)
def test_acl_permissions_view_get_no_permission(self):
self._create_test_acl()
def test_acl_permissions_get_view_no_permission(self):
self.test_acl.permissions.clear()
response = self._request_get_acl_permissions_view()
response = self._request_get_acl_permissions_get_view()
self.assertNotContains(
response=response, text=force_text(self.test_object),
status_code=404
)
def test_acl_permissions_view_get_with_access(self):
self._create_test_acl()
self.assertFalse(
self.test_object.acls.filter(permissions=self.test_permission.stored_permission).exists()
)
def test_acl_permissions_get_view_with_access(self):
self.test_acl.permissions.clear()
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
response = self._request_get_acl_permissions_view()
response = self._request_get_acl_permissions_get_view()
self.assertContains(
response=response, text=force_text(self.test_object),
status_code=200
)
self.assertFalse(
self.test_object.acls.filter(permissions=self.test_permission.stored_permission).exists()
)
def _request_post_acl_permissions_post_view(self):
return self.post(
viewname='acls:acl_permissions',
kwargs={'acl_id': self.test_acl.pk},
data={'available-selection': self.test_permission.stored_permission.pk}
)
def test_acl_permissions_post_view_no_permission(self):
self.test_acl.permissions.clear()
response = self._request_post_acl_permissions_post_view()
self.assertNotContains(
response=response, text=force_text(self.test_object),
status_code=404
)
self.assertFalse(
self.test_object.acls.filter(permissions=self.test_permission.stored_permission).exists()
)
def test_acl_permissions_post_view_with_access(self):
self.test_acl.permissions.clear()
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
response = self._request_post_acl_permissions_post_view()
self.assertEqual(response.status_code, 302)
self.assertTrue(
self.test_object.acls.filter(permissions=self.test_permission.stored_permission).exists()
)

View File

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

View File

@@ -1,24 +1,20 @@
from __future__ import absolute_import, unicode_literals
import itertools
import logging
from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404
from django.template import RequestContext
from django.urls import reverse
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.mixins import (
ContentTypeViewMixin, ExternalObjectViewMixin
ContentTypeViewMixin, ExternalObjectMixin
)
from mayan.apps.common.views import (
AssignRemoveView, SingleObjectCreateView, SingleObjectDeleteView,
from mayan.apps.common.generics import (
AddRemoveView, SingleObjectCreateView, SingleObjectDeleteView,
SingleObjectListView
)
from mayan.apps.permissions import Permission, PermissionNamespace
from mayan.apps.permissions.models import Role, StoredPermission
from mayan.apps.permissions.models import Role
from .classes import ModelPermission
from .forms import ACLCreateForm
@@ -30,7 +26,11 @@ from .permissions import permission_acl_edit, permission_acl_view
logger = logging.getLogger(__name__)
class ACLCreateView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectCreateView):
class ACLCreateView(ContentTypeViewMixin, ExternalObjectMixin, SingleObjectCreateView):
content_type_url_kw_args = {
'app_label': 'app_label',
'model': 'model_name'
}
external_object_permission = permission_acl_edit
external_object_pk_url_kwarg = 'object_id'
form_class = ACLCreateForm
@@ -44,7 +44,7 @@ class ACLCreateView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectC
def get_external_object_queryset(self):
# Here we get a queryset the object model for which an ACL will be
# created.
return self.get_content_type().model_class().objects.all()
return self.get_content_type().get_all_objects_for_this_type()
def get_extra_context(self):
return {
@@ -61,7 +61,8 @@ class ACLCreateView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectC
'queryset': Role.objects.exclude(
pk__in=self.get_external_object().acls.values('role')
),
'widget_attributes': {'class': 'select2'}
'widget_attributes': {'class': 'select2'},
'user': self.request.user
}
def get_instance_extra_data(self):
@@ -77,15 +78,17 @@ class ACLCreateView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectC
class ACLDeleteView(SingleObjectDeleteView):
object_permission = permission_acl_edit
object_permission_related = 'content_object'
object_permission_raise_404 = True
model = AccessControlList
pk_url_kwarg = 'acl_pk'
object_permission = permission_acl_edit
pk_url_kwarg = 'acl_id'
def get_extra_context(self):
acl = self.get_object()
return {
'object': self.get_object().content_object,
'acl': acl,
'object': acl.content_object,
'navigation_object_list': ('object', 'acl'),
'title': _('Delete ACL: %s') % self.get_object(),
}
@@ -94,20 +97,24 @@ class ACLDeleteView(SingleObjectDeleteView):
return reverse(
'acls:acl_list', kwargs={
'app_label': instance.content_type.app_label,
'model': instance.content_type.model,
'model_name': instance.content_type.model,
'object_id': instance.object_id
}
)
class ACLListView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectListView):
class ACLListView(ContentTypeViewMixin, ExternalObjectMixin, SingleObjectListView):
content_type_url_kw_args = {
'app_label': 'app_label',
'model': 'model_name'
}
external_object_permission = permission_acl_view
external_object_pk_url_kwarg = 'object_id'
def get_external_object_queryset(self):
# Here we get a queryset the object model for which an ACL will be
# created.
return self.get_content_type().model_class().objects.all()
return self.get_content_type().get_all_objects_for_this_type()
def get_extra_context(self):
return {
@@ -135,118 +142,88 @@ class ACLListView(ContentTypeViewMixin, ExternalObjectViewMixin, SingleObjectLis
),
}
def get_object_list(self):
def get_source_queryset(self):
return self.get_external_object().acls.all()
class ACLPermissionsView(AssignRemoveView):
grouped = True
left_list_title = _('Available permissions')
right_list_title = _('Granted permissions')
class ACLPermissionsView(AddRemoveView):
action_add_method = 'permissions_add'
action_remove_method = 'permissions_remove'
main_object_model = AccessControlList
main_object_permission = permission_acl_edit
main_object_pk_url_kwarg = 'acl_id'
list_added_title = _('Granted permissions')
list_available_title = _('Available permissions')
related_field = 'permissions'
@staticmethod
def generate_choices(entries):
results = []
def generate_choices(self, queryset):
namespaces_dictionary = {}
entries = sorted(
entries, key=lambda x: (
x.volatile_permission.namespace.label,
x.volatile_permission.label
)
# Sort permissions by their translatable label
object_list = sorted(
queryset, key=lambda permission: permission.volatile_permission.label
)
for namespace, permissions in itertools.groupby(entries, lambda entry: entry.namespace):
permission_options = [
(force_text(permission.pk), permission) for permission in permissions
]
results.append(
(PermissionNamespace.get(name=namespace), permission_options)
# Group permissions by namespace
for permission in object_list:
namespaces_dictionary.setdefault(
permission.volatile_permission.namespace.label,
[]
)
namespaces_dictionary[permission.volatile_permission.namespace.label].append(
(permission.pk, force_text(permission))
)
return results
# Sort permissions by their translatable namespace label
return sorted(namespaces_dictionary.items())
def add(self, item):
permission = get_object_or_404(klass=StoredPermission, pk=item)
self.get_object().permissions.add(permission)
def get_available_list(self):
return ModelPermission.get_for_instance(
instance=self.get_object().content_object
).exclude(id__in=self.get_granted_list().values_list('pk', flat=True))
def get_actions_extra_kwargs(self):
return {'_user': self.request.user}
def get_disabled_choices(self):
"""
Get permissions from a parent's acls but remove the permissions we
already hold for this object
Get permissions from a parent's ACLs or directly granted to the role.
We return a list since that is what the form widget's can process.
"""
return map(
str, set(
self.get_object().get_inherited_permissions().values_list(
'pk', flat=True
)
).difference(
self.get_object().permissions.values_list('pk', flat=True)
)
)
return self.main_object.get_inherited_permissions().values_list('pk', flat=True)
def get_extra_context(self):
return {
'object': self.get_object().content_object,
'title': _('Role "%(role)s" permission\'s for "%(object)s"') % {
'role': self.get_object().role,
'object': self.get_object().content_object,
},
'acl': self.main_object,
'object': self.main_object.content_object,
'navigation_object_list': ('object', 'acl'),
'title': _('Role "%(role)s" permission\'s for "%(object)s".') % {
'role': self.main_object.role,
'object': self.main_object.content_object,
}
}
def get_granted_list(self):
def get_list_added_help_text(self):
if self.main_object.get_inherited_permissions():
return _(
'Disabled permissions are inherited from a parent object or '
'directly granted to the role and can\'t be removed from this '
'view. Inherited permissions need to be removed from the '
'parent object\'s ACL or from them role via the Setup menu.'
)
def get_list_added_queryset(self):
"""
Merge of permissions we hold for this object and the permissions we
hold for this object's parent via another ACL.
hold for this object's parents via another ACL. .distinct() is added
in case the permission was added to the ACL and then added to a
parent ACL's and thus inherited and would appear twice. If
order to remove the double permission from the ACL it would need to be
remove from the parent first to enable the choice in the form,
remove it from the ACL and then re-add it to the parent ACL.
"""
merged_pks = self.get_object().permissions.values_list(
'pk', flat=True
) | self.get_object().get_inherited_permissions().values_list(
'pk', flat=True
queryset_acl = super(ACLPermissionsView, self).get_list_added_queryset()
return (
queryset_acl | self.main_object.get_inherited_permissions()
).distinct()
def get_secondary_object_source_queryset(self):
return ModelPermission.get_for_instance(
instance=self.main_object.content_object
)
return StoredPermission.objects.filter(pk__in=merged_pks)
def get_object(self):
acl = get_object_or_404(
klass=AccessControlList, pk=self.kwargs['acl_pk']
)
# Get the ACL, from this get the object of the ACL, from the object
# get all ACLs it holds as a filtered queryset by access.
try:
AccessControlList.objects.check_access(
permissions=(permission_acl_edit,), obj=acl.content_object,
user=self.request.user
)
except PermissionDenied:
queryset = AccessControlList.objects.none()
else:
queryset = acl.content_object.acls.all()
return get_object_or_404(klass=queryset, pk=self.kwargs['acl_pk'])
def get_right_list_help_text(self):
if self.get_object().get_inherited_permissions():
return _(
'Disabled permissions are inherited from a parent object and '
'can\'t be removed from this view, they need to be removed '
'from the parent object\'s ACL view.'
)
return self.right_list_help_text
def left_list(self):
Permission.refresh()
return ACLPermissionsView.generate_choices(self.get_available_list())
def remove(self, item):
permission = get_object_or_404(klass=StoredPermission, pk=item)
self.get_object().permissions.remove(permission)
def right_list(self):
return ACLPermissionsView.generate_choices(self.get_granted_list())

View File

@@ -89,7 +89,8 @@ class GrantAccessAction(WorkflowAction):
try:
AccessControlList.objects.check_access(
permissions=permission_acl_edit, user=request.user, obj=obj
obj=obj, permissions=permission_acl_edit,
user=request.user
)
except Exception as exception:
raise ValidationError(exception)

View File

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

View File

@@ -8,10 +8,10 @@ class MayanApp {
ajaxMenusOptions: []
}
this.ajaxSpinnerSeletor = '#ajax-spinner';
this.ajaxExecuting = false;
this.ajaxMenusOptions = options.ajaxMenusOptions;
this.ajaxMenuHashes = {};
this.ajaxSpinnerSeletor = '#ajax-spinner';
this.window = $(window);
}
@@ -29,29 +29,6 @@ class MayanApp {
}
}
static mayanNotificationBadge (options, data) {
// Callback to add the notifications count inside a badge markup
var notifications = data[options.attributeName];
if (notifications > 0) {
// Save the original link text before adding the initial badge markup
if (!options.element.data('mn-saved-text')) {
options.element.data('mn-saved-text', options.element.html());
}
options.element.html(
options.element.data('mn-saved-text') + ' <span class="badge">' + notifications + '</span>'
);
} else {
if (options.element.data('mn-saved-text')) {
// If there is a saved original link text, restore it
options.element.html(
options.element.data('mn-saved-text')
);
}
}
}
static setupMultiItemActions () {
$('body').on('change', '.check-all-slave', function () {
MayanApp.countChecked();
@@ -81,22 +58,6 @@ class MayanApp {
});
}
static tagSelectionTemplate (tag, container) {
var $tag = $(
'<span class="label label-tag" style="background: ' + tag.element.dataset.color + ';"> ' + tag.text + '</span>'
);
container[0].style.background = tag.element.dataset.color;
return $tag;
}
static tagResultTemplate (tag) {
if (!tag.element) { return ''; }
var $tag = $(
'<span class="label label-tag" style="background: ' + tag.element.dataset.color + ';"> ' + tag.text + '</span>'
);
return $tag;
}
static updateNavbarState () {
var uri = new URI(window.location.hash);
var uriFragment = uri.fragment();
@@ -110,35 +71,6 @@ class MayanApp {
// Instance methods
AJAXperiodicWorker (options) {
var app = this;
$.ajax({
complete: function() {
if (!options.app) {
// Preserve the app reference between consecutive calls
options.app = app;
}
setTimeout(options.app.AJAXperiodicWorker, options.interval, options);
},
success: function(data) {
if (options.callback) {
// Convert the callback string to an actual function
var callbackFunction = window;
$.each(options.callback.split('.'), function (index, value) {
callbackFunction = callbackFunction[value]
});
callbackFunction(options, data);
} else {
options.element.text(data[options.attributeName]);
}
},
url: options.APIURL
});
}
callbackAJAXSpinnerUpdate () {
if (this.ajaxExecuting) {
$(this.ajaxSpinnerSeletor).fadeIn(50);
@@ -239,7 +171,6 @@ class MayanApp {
initialize () {
var self = this;
this.setupAJAXPeriodicWorkers();
this.setupAJAXSpinner();
this.setupFormHotkeys();
this.setupFullHeightResizing();
@@ -256,22 +187,6 @@ class MayanApp {
partialNavigation.initialize();
}
setupAJAXPeriodicWorkers () {
var app = this;
$('a[data-apw-url]').each(function() {
var $this = $(this);
app.AJAXperiodicWorker({
attributeName: $this.data('apw-attribute'),
APIURL: $this.data('apw-url'),
callback: $this.data('apw-callback'),
element: $this,
interval: $this.data('apw-interval'),
});
});
}
setupAJAXSpinner () {
var self = this;
@@ -445,12 +360,6 @@ class MayanApp {
dropdownAutoWidth: true,
width: '100%'
});
$('.select2-tags').select2({
templateSelection: MayanApp.tagSelectionTemplate,
templateResult: MayanApp.tagResultTemplate,
width: '100%'
});
}
resizeFullHeight () {

View File

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

View File

@@ -90,6 +90,17 @@
<i class="fa fa-times"></i> {% if cancel_label %}{{ cancel_label }}{% else %}{% trans 'Cancel' %}{% endif %}
</a>
{% endif %}
{% for button in extra_buttons %}
<button class="btn btn-default" name="{% if form.prefix %}{{ form.prefix }}-{{ button.name }}{% else %}{{ button.name }}{% endif %}" type="submit">
{% if button.icon_class %}
{{ button.icon_class.render }}
{% endif %}
{% if button.label %}{{ button.label }}{% else %}{% if object %}{% trans 'Save' %}{% else %}{% trans 'Submit' %}{% endif %}{% endif %}
</button>
{% endfor %}
</div>
{% endif %}
{% endif %}

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@
<div class="pull-left">
<div class="btn-toolbar" role="toolbar">
<div class="btn-group">
{% if links_multi_item %}
{% if links_multi_menus_results %}
<a class="btn btn-default btn-sm check-all" data-checked=false data-icon-checked="fa fa-check-square" data-icon-unchecked="far fa-square" title="{% trans 'Select/Deselect all' %}">
<i class="far fa-square"></i>
</a>
@@ -19,7 +19,7 @@
</div>
{% if links_multi_item %}
{% if links_multi_menus_results %}
<p class="pull-right" id="multi-item-title" style="margin-top: 4px;">{% trans 'Select items to activate bulk actions. Use Shift + click to select many.' %}</p>
<div class="pull-right btn-group" id="multi-item-actions" style="display: none;">
@@ -29,16 +29,20 @@
<span class="sr-only">{% trans 'Toggle Dropdown' %}</span>
</button>
<ul class="dropdown-menu" role="menu">
{% for object_navigation_links in links_multi_item %}
{% with 'true' as as_li %}
{% with 'true' as hide_active_anchor %}
{% with 'btn-sm btn-multi-item-action' as link_classes %}
{% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% endwith %}
{% endwith %}
{% for multi_item_menu_results in links_multi_menus_results %}
{% for link_group in multi_item_menu_results.link_groups %}
{% with link_group.links as object_navigation_links %}
{% with 'true' as as_li %}
{% with 'true' as hide_active_anchor %}
{% with 'btn-sm btn-multi-item-action' as link_classes %}
{% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}
{% if not forloop.last and object_navigation_links %}
{% endfor %}
{% if not forloop.last and link_group %}
<li class="divider"></li>
{% endif %}
{% endfor %}

View File

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

View File

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

View File

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

View File

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

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="alert alert-success" role="alert">{% trans 'Password reset complete! Click the link below to login.' %}</div>
<div class="text-center"><a class="btn btn-primary" href="{% url 'authentication:logout_view' %}">{% trans 'Login page' %}</a></div>
<div class="text-center"><a class="btn btn-primary" href="{% url 'authentication:login_view' %}">{% trans 'Login page' %}</a></div>
</div>
</div>
{% endblock content_plain %}

View File

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

View File

@@ -1,43 +1,44 @@
from __future__ import unicode_literals
from django.conf import settings
from django.conf.urls import url
from django.contrib.auth.views import logout
from .views import (
login_view, password_change_done, password_change_view,
password_reset_complete_view, password_reset_confirm_view,
password_reset_done_view, password_reset_view
MayanLoginView, MayanLogoutView, MayanPasswordChangeDoneView,
MayanPasswordChangeView, MayanPasswordResetCompleteView,
MayanPasswordResetConfirmView, MayanPasswordResetDoneView,
MayanPasswordResetView
)
urlpatterns = [
url(regex=r'^login/$', name='login_view', view=login_view),
url(regex=r'^login/$', name='login_view', view=MayanLoginView.as_view()),
url(
regex=r'^logout/$', kwargs={'next_page': settings.LOGIN_REDIRECT_URL},
name='logout_view', view=logout
regex=r'^logout/$', name='logout_view', view=MayanLogoutView.as_view()
),
url(
regex=r'^password/change/$', name='password_change_view',
view=password_change_view
view=MayanPasswordChangeView.as_view()
),
url(
regex=r'^password/change/done/$', name='password_change_done',
view=password_change_done
view=MayanPasswordChangeDoneView.as_view()
),
url(
regex=r'^password/reset/$', name='password_reset_view',
view=password_reset_view
view=MayanPasswordResetView.as_view()
),
url(
regex=r'^password/reset/confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
name='password_reset_confirm_view', view=password_reset_confirm_view
name='password_reset_confirm_view',
view=MayanPasswordResetConfirmView.as_view()
),
url(
regex=r'^password/reset/complete/$',
name='password_reset_complete_view', view=password_reset_complete_view
name='password_reset_complete_view',
view=MayanPasswordResetCompleteView.as_view()
),
url(
regex=r'^password/reset/done/$', name='password_reset_done_view',
view=password_reset_done_view
view=MayanPasswordResetDoneView.as_view()
),
]

View File

@@ -1,19 +1,17 @@
from __future__ import absolute_import, unicode_literals
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.views import (
login, password_change, password_reset, password_reset_complete,
password_reset_confirm, password_reset_done
LoginView, LogoutView, PasswordChangeDoneView, PasswordChangeView,
PasswordResetCompleteView, PasswordResetConfirmView, PasswordResetDoneView,
PasswordResetView
)
from django.http import HttpResponseRedirect
from django.shortcuts import redirect, resolve_url
from django.urls import reverse
from django.utils.http import is_safe_url
from django.shortcuts import redirect
from django.urls import reverse, reverse_lazy
from django.utils.translation import ugettext_lazy as _
from stronghold.decorators import public
from stronghold.views import StrongholdPublicMixin
import mayan
from mayan.apps.common.settings import (
@@ -24,143 +22,108 @@ from .forms import EmailAuthenticationForm, UsernameAuthenticationForm
from .settings import setting_login_method, setting_maximum_session_length
@public
def login_view(request):
"""
Control how the use is to be authenticated, options are 'email' and
'username'
"""
success_url_allowed_hosts = set()
kwargs = {'template_name': 'authentication/login.html'}
class MayanLoginView(StrongholdPublicMixin, LoginView):
extra_context = {
'appearance_type': 'plain'
}
template_name = 'authentication/login.html'
redirect_authenticated_user = True
if setting_login_method.value == 'email':
kwargs['authentication_form'] = EmailAuthenticationForm
else:
kwargs['authentication_form'] = UsernameAuthenticationForm
def form_valid(self, form):
result = super(MayanLoginView, self).form_valid(form=form)
remember_me = form.cleaned_data.get('remember_me')
allowed_hosts = {request.get_host()}
allowed_hosts.update(success_url_allowed_hosts)
# remember_me values:
# True - long session
# False - short session
# None - Form has no remember_me value and we let the session
# expiration default.
redirect_to = request.POST.get(
REDIRECT_FIELD_NAME, request.GET.get(REDIRECT_FIELD_NAME, '')
)
url_is_safe = is_safe_url(
url=redirect_to,
allowed_hosts=allowed_hosts,
require_https=request.is_secure(),
)
url = redirect_to if url_is_safe else ''
if not request.user.is_authenticated:
extra_context = {
'appearance_type': 'plain',
REDIRECT_FIELD_NAME: url or resolve_url(settings.LOGIN_REDIRECT_URL)
}
result = login(request, extra_context=extra_context, **kwargs)
if request.method == 'POST':
form = kwargs['authentication_form'](request, data=request.POST)
if form.is_valid():
if form.cleaned_data['remember_me']:
request.session.set_expiry(
setting_maximum_session_length.value
)
else:
request.session.set_expiry(0)
return result
else:
return HttpResponseRedirect(resolve_url(settings.LOGIN_REDIRECT_URL))
def password_change_view(request):
"""
Password change wrapper for better control
"""
extra_context = {'title': _('Current user password change')}
if request.user.user_options.block_password_change:
messages.error(
request, _(
'Changing the password is not allowed for this account.'
if remember_me is True:
self.request.session.set_expiry(
setting_maximum_session_length.value
)
elif remember_me is False:
self.request.session.set_expiry(0)
return result
def get_form_class(self):
if setting_login_method.value == 'email':
return EmailAuthenticationForm
else:
return UsernameAuthenticationForm
class MayanLogoutView(LogoutView):
"""No current change or overrides, left here for future expansion"""
class MayanPasswordChangeDoneView(PasswordChangeDoneView):
def dispatch(self, *args, **kwargs):
messages.success(
message=_('Your password has been successfully changed.'),
request=self.request
)
return HttpResponseRedirect(reverse(setting_home_view.view))
return password_change(
request, extra_context=extra_context,
template_name='appearance/generic_form.html',
post_change_redirect=reverse(viewname='authentication:password_change_done'),
)
return redirect(to='common:current_user_details')
def password_change_done(request):
"""
View called when the new user password has been accepted
"""
messages.success(
request, _('Your password has been successfully changed.')
)
return redirect('common:current_user_details')
class MayanPasswordChangeView(PasswordChangeView):
extra_context = {'title': _('Current user password change')}
success_url = reverse_lazy(viewname='authentication:password_change_done')
template_name = 'appearance/generic_form.html'
def dispatch(self, *args, **kwargs):
if self.request.user.user_options.block_password_change:
messages.error(
message=_(
'Changing the password is not allowed for this account.'
), request=self.request
)
return HttpResponseRedirect(
redirect_to=reverse(viewname=setting_home_view.view)
)
return super(MayanPasswordChangeView, self).dispatch(*args, **kwargs)
@public
def password_reset_complete_view(request):
class MayanPasswordResetCompleteView(StrongholdPublicMixin, PasswordResetCompleteView):
extra_context = {
'appearance_type': 'plain'
}
return password_reset_complete(
request=request, extra_context=extra_context,
template_name='authentication/password_reset_complete.html'
)
template_name = 'authentication/password_reset_complete.html'
@public
def password_reset_confirm_view(request, uidb64=None, token=None):
class MayanPasswordResetConfirmView(StrongholdPublicMixin, PasswordResetConfirmView):
extra_context = {
'appearance_type': 'plain'
}
return password_reset_confirm(
request=request, extra_context=extra_context,
template_name='authentication/password_reset_confirm.html',
post_reset_redirect=reverse(
'authentication:password_reset_complete_view'
), uidb64=uidb64, token=token
success_url = reverse_lazy(
viewname='authentication:password_reset_complete_view'
)
template_name = 'authentication/password_reset_confirm.html'
@public
def password_reset_done_view(request):
class MayanPasswordResetDoneView(StrongholdPublicMixin, PasswordResetDoneView):
extra_context = {
'appearance_type': 'plain'
}
return password_reset_done(
request=request, extra_context=extra_context,
template_name='authentication/password_reset_done.html'
)
template_name = 'authentication/password_reset_done.html'
@public
def password_reset_view(request):
class MayanPasswordResetView(StrongholdPublicMixin, PasswordResetView):
email_template_name = 'authentication/password_reset_email.html'
extra_context = {
'appearance_type': 'plain'
}
return password_reset(
request=request, extra_context=extra_context,
email_template_name='authentication/password_reset_email.html',
extra_email_context={
'project_title': setting_project_title.value,
'project_website': setting_project_url.value,
'project_copyright': mayan.__copyright__,
'project_license': mayan.__license__,
}, subject_template_name='authentication/password_reset_subject.txt',
template_name='authentication/password_reset_form.html',
post_reset_redirect=reverse(
'authentication:password_reset_done_view'
)
extra_email_context = {
'project_copyright': mayan.__copyright__,
'project_license': mayan.__license__,
'project_title': setting_project_title.value,
'project_website': setting_project_url.value
}
subject_template_name = 'authentication/password_reset_subject.txt'
success_url = reverse_lazy(
viewname='authentication:password_reset_done_view'
)
template_name = 'authentication/password_reset_form.html'

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,17 +24,17 @@ from .permissions import (
link_document_cabinet_list = Link(
args='resolved_object.pk', icon_class=icon_cabinet_list,
permissions=(permission_document_view,),
text=_('Cabinets'), view='cabinets:document_cabinet_list',
permission=permission_document_view, text=_('Cabinets'),
view='cabinets:document_cabinet_list',
)
link_document_cabinet_remove = Link(
args='resolved_object.pk', icon_class=icon_document_cabinet_remove,
permissions=(permission_cabinet_remove_document,),
permission=permission_cabinet_remove_document,
text=_('Remove from cabinets'), view='cabinets:document_cabinet_remove'
)
link_document_cabinet_add = Link(
args='object.pk', icon_class=icon_document_cabinet_add,
permissions=(permission_cabinet_add_document,), text=_('Add to cabinets'),
permission=permission_cabinet_add_document, text=_('Add to cabinets'),
view='cabinets:document_cabinet_add',
)
link_document_multiple_cabinet_add = Link(
@@ -61,21 +61,21 @@ link_custom_acl_list.condition = cabinet_is_root
link_cabinet_child_add = Link(
args='object.pk', icon_class=icon_cabinet_child_add,
permissions=(permission_cabinet_create,), text=_('Add new level'),
permission=permission_cabinet_create, text=_('Add new level'),
view='cabinets:cabinet_child_add'
)
link_cabinet_create = Link(
icon_class=icon_cabinet_create, permissions=(permission_cabinet_create,),
icon_class=icon_cabinet_create, permission=permission_cabinet_create,
text=_('Create cabinet'), view='cabinets:cabinet_create'
)
link_cabinet_delete = Link(
args='object.pk', icon_class=icon_cabinet_delete,
permissions=(permission_cabinet_delete,), tags='dangerous',
permission=permission_cabinet_delete, tags='dangerous',
text=_('Delete'), view='cabinets:cabinet_delete'
)
link_cabinet_edit = Link(
args='object.pk', icon_class=icon_cabinet_edit,
permissions=(permission_cabinet_edit,), text=_('Edit'),
permission=permission_cabinet_edit, text=_('Edit'),
view='cabinets:cabinet_edit'
)
link_cabinet_list = Link(
@@ -87,6 +87,6 @@ link_cabinet_list = Link(
)
link_cabinet_view = Link(
args='object.pk', icon_class=icon_cabinet_view,
permissions=(permission_cabinet_view,), text=_('Details'),
permission=permission_cabinet_view, text=_('Details'),
view='cabinets:cabinet_view'
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,19 +4,19 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.events import EventTypeNamespace
namespace = EventTypeNamespace(name='checkouts', label=_('Checkouts'))
namespace = EventTypeNamespace(label=_('Checkouts'), name='checkouts')
event_document_auto_check_in = namespace.add_event_type(
name='document_auto_check_in',
label=_('Document automatically checked in')
label=_('Document automatically checked in'),
name='document_auto_check_in'
)
event_document_check_in = namespace.add_event_type(
name='document_check_in', label=_('Document checked in')
label=_('Document checked in'), name='document_check_in'
)
event_document_check_out = namespace.add_event_type(
name='document_check_out', label=_('Document checked out')
label=_('Document checked out'), name='document_check_out'
)
event_document_forceful_check_in = namespace.add_event_type(
name='document_forceful_check_in',
label=_('Document forcefully checked in')
label=_('Document forcefully checked in'),
name='document_forceful_check_in'
)

View File

@@ -13,6 +13,6 @@ def handler_check_new_version_creation(sender, instance, **kwargs):
app_label='checkouts', model_name='NewVersionBlock'
)
if NewVersionBlock.objects.is_blocked(instance.document) and not instance.pk:
if NewVersionBlock.objects.is_blocked(document=instance.document) and not instance.pk:
# Block only new versions (no pk), not existing version being updated.
raise NewDocumentVersionNotAllowed

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
icon_checkin_document = Icon(
driver_name='fontawesome-dual', primary_symbol='shopping-cart',
secondary_symbol='minus'
)
icon_checkout_document = Icon(
driver_name='fontawesome-dual', primary_symbol='shopping-cart',
secondary_symbol='plus'
)
icon_checkout_info = Icon(driver_name='fontawesome', symbol='shopping-cart')
icon_dashboard_checkouts = Icon(
driver_name='fontawesome', symbol='shopping-cart'

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import logging
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.db import models, transaction
from django.urls import reverse
from django.utils.encoding import python_2_unicode_compatible
from django.utils.timezone import now
@@ -68,37 +68,42 @@ class DocumentCheckout(models.Model):
)
def delete(self, *args, **kwargs):
# TODO: enclose in transaction
NewVersionBlock.objects.unblock(self.document)
super(DocumentCheckout, self).delete(*args, **kwargs)
with transaction.atomic():
NewVersionBlock.objects.unblock(document=self.document)
super(DocumentCheckout, self).delete(*args, **kwargs)
def get_absolute_url(self):
return reverse('checkout:checkout_info', args=(self.document.pk,))
return reverse(
viewname='checkout:checkout_info',
kwargs={'document_id': self.document.pk}
)
def natural_key(self):
return self.document.natural_key()
natural_key.dependencies = ['documents.Document']
def save(self, *args, **kwargs):
# TODO: enclose in transaction
new_checkout = not self.pk
if not new_checkout or self.document.is_checked_out():
raise DocumentAlreadyCheckedOut
result = super(DocumentCheckout, self).save(*args, **kwargs)
if new_checkout:
event_document_check_out.commit(
actor=self.user, target=self.document
)
if self.block_new_version:
NewVersionBlock.objects.block(self.document)
logger.info(
'Document "%s" checked out by user "%s"',
self.document, self.user
raise DocumentAlreadyCheckedOut(
_('Document already checked out.')
)
return result
with transaction.atomic():
result = super(DocumentCheckout, self).save(*args, **kwargs)
if new_checkout:
event_document_check_out.commit(
actor=self.user, target=self.document
)
if self.block_new_version:
NewVersionBlock.objects.block(self.document)
logger.info(
'Document "%s" checked out by user "%s"',
self.document, self.user
)
return result
class NewVersionBlock(models.Model):

View File

@@ -6,15 +6,15 @@ from mayan.apps.permissions import PermissionNamespace
namespace = PermissionNamespace(label=_('Document checkout'), name='checkouts')
permission_document_checkin = namespace.add_permission(
name='checkin_document', label=_('Check in documents')
permission_document_check_in = namespace.add_permission(
label=_('Check in documents'), name='checkin_document'
)
permission_document_checkin_override = namespace.add_permission(
name='checkin_document_override', label=_('Forcefully check in documents')
permission_document_check_in_override = namespace.add_permission(
label=_('Forcefully check in documents'), name='checkin_document_override'
)
permission_document_checkout = namespace.add_permission(
name='checkout_document', label=_('Check out documents')
label=_('Check out documents'), name='checkout_document'
)
permission_document_checkout_detail_view = namespace.add_permission(
name='checkout_detail_view', label=_('Check out details view')
label=_('Check out details view'), name='checkout_detail_view'
)

View File

@@ -5,9 +5,9 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.task_manager.classes import CeleryQueue
queue_checkouts_periodic = CeleryQueue(
name='checkouts_periodic', label=_('Checkouts periodic'), transient=True
label=_('Checkouts periodic'), name='checkouts_periodic', transient=True
)
queue_checkouts_periodic.add_task_type(
name='mayan.apps.task_check_expired_check_outs',
label=_('Check expired checkouts')
label=_('Check expired checkouts'),
name='mayan.apps.task_check_expired_check_outs'
)

View File

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

View File

@@ -25,7 +25,7 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
def _request_checkedout_document_view(self):
return self.get(
viewname='rest_api:checkedout-document-view',
args=(self.checkout.pk,)
kwargs={'document_pk': self.checkout.pk}
)
def _checkout_document(self):
@@ -44,7 +44,8 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
def test_checkedout_document_view_with_checkout_access(self):
self._checkout_document()
self.grant_access(
permission=permission_document_checkout_detail_view, obj=self.document
obj=self.document,
permission=permission_document_checkout_detail_view
)
response = self._request_checkedout_document_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
@@ -52,7 +53,7 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
def test_checkedout_document_view_with_document_access(self):
self._checkout_document()
self.grant_access(
permission=permission_document_view, obj=self.document
obj=self.document, permission=permission_document_view
)
response = self._request_checkedout_document_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
@@ -60,14 +61,17 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
def test_checkedout_document_view_with_access(self):
self._checkout_document()
self.grant_access(
permission=permission_document_view, obj=self.document
obj=self.document, permission=permission_document_view
)
self.grant_access(
permission=permission_document_checkout_detail_view, obj=self.document
obj=self.document,
permission=permission_document_checkout_detail_view
)
response = self._request_checkedout_document_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['document']['uuid'], force_text(self.document.uuid))
self.assertEqual(
response.data['document']['uuid'], force_text(self.document.uuid)
)
def _request_document_checkout_view(self):
return self.post(
@@ -83,7 +87,9 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
self.assertEqual(DocumentCheckout.objects.count(), 0)
def test_document_checkout_with_access(self):
self.grant_access(permission=permission_document_checkout, obj=self.document)
self.grant_access(
obj=self.document, permission=permission_document_checkout
)
response = self._request_document_checkout_view()
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(
@@ -102,7 +108,7 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
def test_checkout_list_view_with_document_access(self):
self._checkout_document()
self.grant_access(
permission=permission_document_view, obj=self.document
obj=self.document, permission=permission_document_view
)
response = self._request_checkout_list_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -111,7 +117,8 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
def test_checkout_list_view_with_checkout_access(self):
self._checkout_document()
self.grant_access(
permission=permission_document_checkout_detail_view, obj=self.document
obj=self.document,
permission=permission_document_checkout_detail_view
)
response = self._request_checkout_list_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -120,10 +127,11 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
def test_checkout_list_view_with_access(self):
self._checkout_document()
self.grant_access(
permission=permission_document_view, obj=self.document
obj=self.document, permission=permission_document_view
)
self.grant_access(
permission=permission_document_checkout_detail_view, obj=self.document
obj=self.document,
permission=permission_document_checkout_detail_view
)
response = self._request_checkout_list_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@@ -23,7 +23,7 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase):
DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime,
user=self.admin_user, block_new_version=True
user=self._test_case_user, block_new_version=True
)
self.assertTrue(self.document.is_checked_out())
@@ -33,29 +33,12 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase):
)
)
def test_version_creation_blocking(self):
expiration_datetime = now() + datetime.timedelta(days=1)
# Silence unrelated logging
logging.getLogger('mayan.apps.documents.models').setLevel(
level=logging.CRITICAL
)
DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime,
user=self.admin_user, block_new_version=True
)
with self.assertRaises(NewDocumentVersionNotAllowed):
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
self.document.new_version(file_object=file_object)
def test_checkin_in(self):
expiration_datetime = now() + datetime.timedelta(days=1)
DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime,
user=self.admin_user, block_new_version=True
user=self._test_case_user, block_new_version=True
)
self.document.check_in()
@@ -72,13 +55,13 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase):
DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime,
user=self.admin_user, block_new_version=True
user=self._test_case_user, block_new_version=True
)
with self.assertRaises(DocumentAlreadyCheckedOut):
DocumentCheckout.objects.checkout_document(
document=self.document,
expiration_datetime=expiration_datetime, user=self.admin_user,
expiration_datetime=expiration_datetime, user=self._test_case_user,
block_new_version=True
)
@@ -91,7 +74,7 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase):
DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime,
user=self.admin_user, block_new_version=True
user=self._test_case_user, block_new_version=True
)
time.sleep(.11)
@@ -100,18 +83,6 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase):
self.assertFalse(self.document.is_checked_out())
def test_blocking_new_versions(self):
# Silence unrelated logging
logging.getLogger('mayan.apps.documents.models').setLevel(
level=logging.CRITICAL
)
NewVersionBlock.objects.block(document=self.document)
with self.assertRaises(NewDocumentVersionNotAllowed):
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
self.document.new_version(file_object=file_object)
class NewVersionBlockTestCase(DocumentTestMixin, BaseTestCase):
def test_blocking(self):
@@ -141,3 +112,32 @@ class NewVersionBlockTestCase(DocumentTestMixin, BaseTestCase):
self.assertFalse(
NewVersionBlock.objects.is_blocked(document=self.document)
)
def test_blocking_new_versions(self):
# Silence unrelated logging
logging.getLogger('mayan.apps.documents.models').setLevel(
level=logging.CRITICAL
)
NewVersionBlock.objects.block(document=self.document)
with self.assertRaises(NewDocumentVersionNotAllowed):
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
self.document.new_version(file_object=file_object)
def test_version_creation_blocking(self):
expiration_datetime = now() + datetime.timedelta(days=1)
# Silence unrelated logging
logging.getLogger('mayan.apps.documents.models').setLevel(
level=logging.CRITICAL
)
DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime,
user=self._test_case_user, block_new_version=True
)
with self.assertRaises(NewDocumentVersionNotAllowed):
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
self.document.new_version(file_object=file_object)

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

View File

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

View File

@@ -1,85 +1,154 @@
from __future__ import absolute_import, unicode_literals
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ugettext_lazy as _, ungettext
from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.generics import (
ConfirmView, SingleObjectCreateView, SingleObjectDetailView
MultipleObjectConfirmActionView, MultipleObjectFormActionView,
SingleObjectDetailView
)
from mayan.apps.common.utils import encapsulate
from mayan.apps.documents.models import Document
from mayan.apps.documents.views import DocumentListView
from .exceptions import DocumentAlreadyCheckedOut, DocumentNotCheckedOut
from .forms import DocumentCheckoutDefailForm, DocumentCheckoutForm
from .icons import icon_checkout_info
from .models import DocumentCheckout
from .permissions import (
permission_document_checkin, permission_document_checkin_override,
permission_document_checkout, permission_document_checkout_detail_view
permission_document_check_in, permission_document_checkout,
permission_document_checkout_detail_view
)
class CheckoutDocumentView(SingleObjectCreateView):
form_class = DocumentCheckoutForm
class DocumentCheckinView(MultipleObjectConfirmActionView):
error_message = 'Unable to check in document "%(instance)s". %(exception)s'
model = Document
object_permission = permission_document_check_in
pk_url_kwarg = 'document_id'
success_message_singular = '%(count)d document checked in.'
success_message_plural = '%(count)d documents checked in.'
def dispatch(self, request, *args, **kwargs):
self.document = get_object_or_404(klass=Document, pk=self.kwargs['pk'])
def get_extra_context(self):
queryset = self.get_object_list()
AccessControlList.objects.check_access(
permissions=permission_document_checkout, user=request.user,
obj=self.document
)
result = {
'title': ungettext(
singular='Check in %(count)d document',
plural='Check in %(count)d documents',
number=queryset.count()
) % {
'count': queryset.count(),
}
}
return super(
CheckoutDocumentView, self
).dispatch(request, *args, **kwargs)
if queryset.count() == 1:
result.update(
{
'object': queryset.first(),
'title': _(
'Check in document: %s'
) % queryset.first()
}
)
def form_valid(self, form):
try:
instance = form.save(commit=False)
instance.user = self.request.user
instance.document = self.document
instance.save()
except DocumentAlreadyCheckedOut:
messages.error(self.request, _('Document already checked out.'))
except Exception as exception:
messages.error(
self.request,
_('Error trying to check out document; %s') % exception
return result
def get_post_object_action_url(self):
if self.action_count == 1:
return reverse(
viewname='checkouts:document_checkout_info',
kwargs={'document_id': self.action_id_list[0]}
)
else:
messages.success(
self.request,
_('Document "%s" checked out successfully.') % self.document
super(DocumentCheckinView, self).get_post_action_redirect()
def object_action(self, form, instance):
DocumentCheckout.objects.check_in_document(
document=instance, user=self.request.user
)
class DocumentCheckoutView(MultipleObjectFormActionView):
error_message = 'Unable to checkout document "%(instance)s". %(exception)s'
form_class = DocumentCheckoutForm
model = Document
object_permission = permission_document_checkout
pk_url_kwarg = 'document_id'
success_message_singular = '%(count)d document checked out.'
success_message_plural = '%(count)d documents checked out.'
def get_extra_context(self):
queryset = self.get_object_list()
result = {
'title': ungettext(
singular='Checkout %(count)d document',
plural='Checkout %(count)d documents',
number=queryset.count()
) % {
'count': queryset.count(),
}
}
if queryset.count() == 1:
result.update(
{
'object': queryset.first(),
'title': _(
'Check out document: %s'
) % queryset.first()
}
)
return HttpResponseRedirect(self.get_success_url())
return result
def get_post_object_action_url(self):
if self.action_count == 1:
return reverse(
viewname='checkouts:document_checkout_info',
kwargs={'document_id': self.action_id_list[0]}
)
else:
super(DocumentCheckoutView, self).get_post_action_redirect()
def object_action(self, form, instance):
DocumentCheckout.objects.checkout_document(
block_new_version=form.cleaned_data['block_new_version'],
document=instance,
expiration_datetime=form.cleaned_data['expiration_datetime'],
user=self.request.user,
)
class DocumentCheckoutDetailView(SingleObjectDetailView):
form_class = DocumentCheckoutDefailForm
model = Document
object_permission = permission_document_checkout_detail_view
def get_extra_context(self):
return {
'object': self.document,
'title': _('Check out document: %s') % self.document
'object': self.get_object(),
'title': _(
'Check out details for document: %s'
) % self.get_object()
}
def get_post_action_redirect(self):
return reverse('checkouts:checkout_info', args=(self.document.pk,))
def get_object(self):
return get_object_or_404(klass=Document, pk=self.kwargs['document_id'])
class CheckoutListView(DocumentListView):
class DocumentCheckoutListView(DocumentListView):
def get_document_queryset(self):
return AccessControlList.objects.filter_by_access(
return AccessControlList.objects.restrict_queryset(
permission=permission_document_checkout_detail_view,
user=self.request.user,
queryset=DocumentCheckout.objects.checked_out_documents()
queryset=DocumentCheckout.objects.checked_out_documents(),
user=self.request.user
)
def get_extra_context(self):
context = super(CheckoutListView, self).get_extra_context()
context = super(DocumentCheckoutListView, self).get_extra_context()
context.update(
{
'extra_columns': (
@@ -113,76 +182,3 @@ class CheckoutListView(DocumentListView):
}
)
return context
class CheckoutDetailView(SingleObjectDetailView):
form_class = DocumentCheckoutDefailForm
model = Document
object_permission = permission_document_checkout_detail_view
def get_extra_context(self):
return {
'object': self.get_object(),
'title': _(
'Check out details for document: %s'
) % self.get_object()
}
def get_object(self):
return get_object_or_404(klass=Document, pk=self.kwargs['pk'])
class DocumentCheckinView(ConfirmView):
def get_extra_context(self):
document = self.get_object()
context = {
'object': document,
}
if document.get_checkout_info().user != self.request.user:
context['title'] = _(
'You didn\'t originally checked out this document. '
'Forcefully check in the document: %s?'
) % document
else:
context['title'] = _('Check in the document: %s?') % document
return context
def get_object(self):
return get_object_or_404(klass=Document, pk=self.kwargs['pk'])
def get_post_action_redirect(self):
return reverse('checkouts:checkout_info', args=(self.get_object().pk,))
def view_action(self):
document = self.get_object()
if document.get_checkout_info().user == self.request.user:
AccessControlList.objects.check_access(
permissions=permission_document_checkin,
user=self.request.user, obj=document
)
else:
AccessControlList.objects.check_access(
permissions=permission_document_checkin_override,
user=self.request.user, obj=document
)
try:
document.check_in(user=self.request.user)
except DocumentNotCheckedOut:
messages.error(
self.request, _('Document has not been checked out.')
)
except Exception as exception:
messages.error(
self.request,
_('Error trying to check in document; %s') % exception
)
else:
messages.success(
self.request,
_('Document "%s" checked in successfully.') % document
)

View File

@@ -2,42 +2,42 @@ from __future__ import unicode_literals
from django.contrib.contenttypes.models import ContentType
from rest_framework import generics
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from .classes import Template
from .serializers import ContentTypeSerializer, TemplateSerializer
class APIContentTypeList(generics.ListAPIView):
class ContentTypeAPIViewSet(viewsets.ReadOnlyModelViewSet):
"""
Returns a list of all the available content types.
list:
Return a list of all the available content types.
retrieve:
Return the given content type details.
"""
serializer_class = ContentTypeSerializer
lookup_url_kwarg = 'content_type_id'
queryset = ContentType.objects.order_by('app_label', 'model')
serializer_class = ContentTypeSerializer
class APITemplateListView(generics.ListAPIView):
class TemplateAPIViewSet(viewsets.ReadOnlyModelViewSet):
"""
Returns a list of partial templates.
get: Returns a list of partial templates.
list:
Return a list of partial templates.
retrieve:
Return the given partial template details.
"""
serializer_class = TemplateSerializer
lookup_url_kwarg = 'template_name'
permission_classes = (IsAuthenticated,)
serializer_class = TemplateSerializer
def get_object(self):
return Template.get(name=self.kwargs['template_name']).render(
request=self.request
)
def get_queryset(self):
return Template.all(rendered=True, request=self.request)
class APITemplateView(generics.RetrieveAPIView):
"""
Returns the selected partial template details.
get: Retrieve the details of the partial template.
"""
serializer_class = TemplateSerializer
permission_classes = (IsAuthenticated,)
def get_object(self):
return Template.get(name=self.kwargs['name']).render(
request=self.request
)

View File

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

View File

@@ -72,16 +72,6 @@ class ErrorLogNamespace(object):
return ErrorLogEntry.objects.filter(namespace=self.name)
class FakeStorageSubclass(object):
"""
Placeholder class to allow serializing the real storage subclass to
support migrations.
"""
def __eq__(self, other):
return True
class MissingItem(object):
_registry = []
@@ -302,7 +292,7 @@ class Template(object):
def get_absolute_url(self):
return reverse(
viewname='rest_api:template-detail', kwargs={'template_pk': self.name}
viewname='rest_api:template-detail', kwargs={'template_name': self.name}
)
def render(self, request):

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,12 @@ from __future__ import absolute_import, unicode_literals
from mayan.apps.appearance.classes import Icon
icon_about = Icon(driver_name='fontawesome', symbol='info')
icon_add_all = Icon(
driver_name='fontawesome-layers', data=[
{'class': 'far fa-circle'},
{'class': 'fas fa-plus', 'transform': 'shrink-6'}
]
)
icon_assign_remove_add = Icon(driver_name='fontawesome', symbol='plus')
icon_assign_remove_remove = Icon(driver_name='fontawesome', symbol='minus')
icon_check_version = Icon(driver_name='fontawesome', symbol='sync')
@@ -43,6 +49,12 @@ icon_ok = Icon(
icon_packages_licenses = Icon(
driver_name='fontawesome', symbol='certificate'
)
icon_remove_all = Icon(
driver_name='fontawesome-layers', data=[
{'class': 'far fa-circle'},
{'class': 'fas fa-minus', 'transform': 'shrink-6'}
]
)
icon_setup = Icon(
driver_name='fontawesome', symbol='cog'
)

View File

@@ -57,12 +57,12 @@ link_documentation = Link(
link_object_error_list = Link(
icon_class=icon_object_error_list,
kwargs=get_kwargs_factory('resolved_object'),
permissions=(permission_error_log_view,), text=_('Errors'),
permission=permission_error_log_view, text=_('Errors'),
view='common:object_error_list',
)
link_object_error_list_clear = Link(
kwargs=get_kwargs_factory('resolved_object'),
permissions=(permission_error_log_view,), text=_('Clear all'),
permission=permission_error_log_view, text=_('Clear all'),
view='common:object_error_list_clear',
)
link_forum = Link(

View File

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

View File

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

View File

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

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -5,13 +5,13 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.task_manager.classes import CeleryQueue
queue_default = CeleryQueue(
name='default', label=_('Default'), is_default_queue=True
is_default_queue=True, label=_('Default'), name='default'
)
queue_tools = CeleryQueue(name='tools', label=_('Tools'))
queue_tools = CeleryQueue(label=_('Tools'), name='tools')
queue_common_periodic = CeleryQueue(
name='common_periodic', label=_('Common periodic'), transient=True
label=_('Common periodic'), name='common_periodic', transient=True
)
queue_common_periodic.add_task_type(
name='mayan.apps.common.tasks.task_delete_stale_uploads',
label=_('Delete stale uploads')
label=_('Delete stale uploads'),
name='mayan.apps.common.tasks.task_delete_stale_uploads'
)

View File

@@ -5,9 +5,15 @@ from django.contrib.contenttypes.models import ContentType
from rest_framework import serializers
class ContentTypeSerializer(serializers.ModelSerializer):
class ContentTypeSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
fields = ('app_label', 'id', 'model')
extra_kwargs = {
'url': {
'lookup_url_kwarg': 'content_type_id',
'view_name': 'rest_api:content_type-detail'
}
}
fields = ('app_label', 'id', 'model', 'url')
model = ContentType
@@ -15,3 +21,7 @@ class TemplateSerializer(serializers.Serializer):
hex_hash = serializers.CharField(read_only=True)
name = serializers.CharField(read_only=True)
html = serializers.CharField(read_only=True)
url = serializers.HyperlinkedIdentityField(
lookup_field='name', lookup_url_kwarg='template_name',
view_name='rest_api:template-detail'
)

View File

@@ -1,7 +1,6 @@
from __future__ import unicode_literals
import os
import tempfile
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
@@ -11,7 +10,7 @@ from mayan.apps.smart_settings import Namespace
from .literals import DEFAULT_COMMON_HOME_VIEW
namespace = Namespace(name='common', label=_('Common'))
namespace = Namespace(label=_('Common'), name='common')
setting_auto_logging = namespace.add_setting(
global_name='COMMON_AUTO_LOGGING',
@@ -53,8 +52,7 @@ setting_production_error_log_path = namespace.add_setting(
global_name='COMMON_PRODUCTION_ERROR_LOG_PATH',
default=os.path.join(settings.MEDIA_ROOT, 'error.log'), help_text=_(
'Path to the logfile that will track errors during production.'
),
is_path=True
)
)
setting_project_title = namespace.add_setting(
global_name='COMMON_PROJECT_TITLE',
@@ -77,16 +75,8 @@ setting_shared_storage_arguments = namespace.add_setting(
global_name='COMMON_SHARED_STORAGE_ARGUMENTS',
default={'location': os.path.join(settings.MEDIA_ROOT, 'shared_files')}
)
setting_temporary_directory = namespace.add_setting(
global_name='COMMON_TEMPORARY_DIRECTORY', default=tempfile.gettempdir(),
help_text=_(
'Temporary directory used site wide to store thumbnails, previews '
'and temporary files.'
),
is_path=True
)
namespace = Namespace(name='django', label=_('Django'))
namespace = Namespace(label=_('Django'), name='django')
setting_django_allowed_hosts = namespace.add_setting(
global_name='ALLOWED_HOSTS', default=settings.ALLOWED_HOSTS,
@@ -357,7 +347,20 @@ setting_django_login_redirect_url = namespace.add_setting(
'for example. This setting also accepts named URL patterns which '
'can be used to reduce configuration duplication since you don\'t '
'have to define the URL in two places (settings and URLconf).'
),
)
)
setting_django_logout_redirect_url = namespace.add_setting(
global_name='LOGOUT_REDIRECT_URL',
default=settings.LOGOUT_REDIRECT_URL,
help_text=_(
'Default: None. The URL where requests are redirected after a user '
'logs out using LogoutView (if the view doesn\'t get a next_page '
'argument). If None, no redirect will be performed and the logout '
'view will be rendered. This setting also accepts named URL '
'patterns which can be used to reduce configuration duplication '
'since you don\'t have to define the URL in two places (settings '
'and URLconf).'
)
)
setting_django_static_url = namespace.add_setting(
global_name='STATIC_URL',
@@ -402,7 +405,7 @@ setting_django_wsgi_application = namespace.add_setting(
),
)
namespace = Namespace(name='celery', label=_('Celery'))
namespace = Namespace(label=_('Celery'), name='celery')
setting_celery_always_eager = namespace.add_setting(
global_name='CELERY_TASK_ALWAYS_EAGER',

View File

@@ -1,7 +1,8 @@
from __future__ import unicode_literals
from mayan.apps.storage.utils import get_storage_subclass
from .settings import setting_shared_storage, setting_shared_storage_arguments
from .utils import get_storage_subclass
storage_sharedupload = get_storage_subclass(
dotted_path=setting_shared_storage.value

View File

@@ -1,5 +1,7 @@
from __future__ import unicode_literals
import logging
from django.template import Context, Library, VariableDoesNotExist, Variable
from django.template.defaultfilters import truncatechars
from django.template.loader import get_template
@@ -14,6 +16,7 @@ from ..icons import icon_list_mode_items, icon_list_mode_list
from ..literals import MESSAGE_SQLITE_WARNING
from ..utils import check_for_sqlite, resolve_attribute
logger = logging.getLogger(__name__)
register = Library()
@@ -48,6 +51,14 @@ def common_calculate_title(context):
return _('Create')
@register.simple_tag
def common_get_object_verbose_name(obj):
try:
return obj._meta.verbose_name
except AttributeError:
return type(obj)
@register.simple_tag
def get_collections():
return Collection.get_all()

View File

@@ -1,30 +1,20 @@
from __future__ import absolute_import, unicode_literals
from django.conf.urls import url
from django.contrib.auth import get_user_model
from django.http import HttpResponse
from django.template import Context, Template
from django.test import TestCase
from django.test.utils import ContextList
from django.urls import clear_url_caches, reverse
from django_downloadview import assert_download_response
from mayan.apps.acls.tests.mixins import ACLBaseTestMixin
from mayan.apps.acls.tests.mixins import ACLTestCaseMixin
from mayan.apps.permissions.classes import Permission
from mayan.apps.smart_settings.classes import Namespace
from mayan.apps.user_management.tests import (
TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME, TEST_USER_PASSWORD,
TEST_USER_USERNAME
)
from .literals import TEST_VIEW_NAME, TEST_VIEW_URL
from .mixins import (
ContentTypeCheckMixin, DatabaseConversionMixin, OpenFileCheckMixin,
TempfileCheckMixin
ClientMethodsTestCaseMixin, ContentTypeCheckMixin, DatabaseConversionMixin,
OpenFileCheckTestCaseMixin, RandomPrimaryKeyModelMonkeyPatchMixin,
TempfileCheckTestCaseMixin, TestViewTestCaseMixin
)
class BaseTestCase(DatabaseConversionMixin, ACLBaseTestMixin, ContentTypeCheckMixin, OpenFileCheckMixin, TempfileCheckMixin, TestCase):
class BaseTestCase(RandomPrimaryKeyModelMonkeyPatchMixin, DatabaseConversionMixin, ACLTestCaseMixin, OpenFileCheckTestCaseMixin, TempfileCheckTestCaseMixin, TestCase):
"""
This is the most basic test case class any test in the project should use.
"""
@@ -36,81 +26,9 @@ class BaseTestCase(DatabaseConversionMixin, ACLBaseTestMixin, ContentTypeCheckMi
Permission.invalidate_cache()
class GenericViewTestCase(BaseTestCase):
def setUp(self):
super(GenericViewTestCase, self).setUp()
self.has_test_view = False
def tearDown(self):
from mayan.urls import urlpatterns
self.client.logout()
if self.has_test_view:
urlpatterns.pop(0)
super(GenericViewTestCase, self).tearDown()
def add_test_view(self, test_object):
from mayan.urls import urlpatterns
def test_view(request):
template = Template('{{ object }}')
context = Context(
{'object': test_object, 'resolved_object': test_object}
)
return HttpResponse(template.render(context=context))
urlpatterns.insert(0, url(TEST_VIEW_URL, test_view, name=TEST_VIEW_NAME))
clear_url_caches()
self.has_test_view = True
def get_test_view(self):
response = self.get(TEST_VIEW_NAME)
if isinstance(response.context, ContextList):
# template widget rendering causes test client response to be
# ContextList rather than RequestContext. Typecast to dictionary
# before updating.
result = dict(response.context).copy()
result.update({'request': response.wsgi_request})
return Context(result)
else:
response.context.update({'request': response.wsgi_request})
return Context(response.context)
def get(self, viewname=None, path=None, *args, **kwargs):
data = kwargs.pop('data', {})
follow = kwargs.pop('follow', False)
if viewname:
path = reverse(viewname=viewname, *args, **kwargs)
return self.client.get(
path=path, data=data, follow=follow
)
def login(self, username, password):
logged_in = self.client.login(username=username, password=password)
user = get_user_model().objects.get(username=username)
self.assertTrue(logged_in)
self.assertTrue(user.is_authenticated)
def login_user(self):
self.login(username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD)
def login_admin_user(self):
self.login(username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD)
def logout(self):
self.client.logout()
def post(self, viewname=None, path=None, *args, **kwargs):
data = kwargs.pop('data', {})
follow = kwargs.pop('follow', False)
if viewname:
path = reverse(viewname=viewname, *args, **kwargs)
return self.client.post(
path=path, data=data, follow=follow
)
class GenericViewTestCase(ClientMethodsTestCaseMixin, ContentTypeCheckMixin, TestViewTestCaseMixin, BaseTestCase):
"""
A generic view test case built on top of the base test case providing
a single, user customizable view to test object resolution and shorthand
HTTP method functions.
"""

View File

@@ -2,20 +2,26 @@ from __future__ import unicode_literals
import glob
import os
import random
from furl import furl
from django.apps import apps
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.conf.urls import url
from django.contrib.contenttypes.models import ContentType
from django.core import management
from django.db import connection
from django.db import models
from django.http import HttpResponse
from django.template import Context, Template
from django.test.utils import ContextList
from django.urls import clear_url_caches, reverse
from django.utils.encoding import force_bytes
from mayan.apps.user_management.tests import (
TEST_ADMIN_EMAIL, TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME,
TEST_GROUP_NAME, TEST_USER_EMAIL, TEST_USER_PASSWORD,
TEST_USER_USERNAME
)
from ..settings import setting_temporary_directory
from mayan.apps.storage.settings import setting_temporary_directory
from .literals import TEST_VIEW_NAME, TEST_VIEW_URL
from .utils import mute_stdout
@@ -23,6 +29,56 @@ if getattr(settings, 'COMMON_TEST_FILE_HANDLES', False):
import psutil
class ClientMethodsTestCaseMixin(object):
def _build_verb_kwargs(self, viewname=None, path=None, *args, **kwargs):
data = kwargs.pop('data', {})
follow = kwargs.pop('follow', False)
query = kwargs.pop('query', {})
if viewname:
path = reverse(viewname=viewname, *args, **kwargs)
path = furl(url=path)
path.args.update(query)
return {'follow': follow, 'data': data, 'path': path.tostr()}
def delete(self, viewname=None, path=None, *args, **kwargs):
return self.client.delete(
**self._build_verb_kwargs(
path=path, viewname=viewname, *args, **kwargs
)
)
def get(self, viewname=None, path=None, *args, **kwargs):
return self.client.get(
**self._build_verb_kwargs(
path=path, viewname=viewname, *args, **kwargs
)
)
def patch(self, viewname=None, path=None, *args, **kwargs):
return self.client.patch(
**self._build_verb_kwargs(
path=path, viewname=viewname, *args, **kwargs
)
)
def post(self, viewname=None, path=None, *args, **kwargs):
return self.client.post(
**self._build_verb_kwargs(
path=path, viewname=viewname, *args, **kwargs
)
)
def put(self, viewname=None, path=None, *args, **kwargs):
return self.client.put(
**self._build_verb_kwargs(
path=path, viewname=viewname, *args, **kwargs
)
)
class ContentTypeCheckMixin(object):
expected_content_type = 'text/html; charset=utf-8'
@@ -34,13 +90,14 @@ class ContentTypeCheckMixin(object):
def request(self, *args, **kwargs):
response = super(CustomClient, self).request(*args, **kwargs)
content_type = response._headers['content-type'][1]
test_instance.assertEqual(
content_type, test_instance.expected_content_type,
msg='Unexpected response content type: {}, expected: {}.'.format(
content_type, test_instance.expected_content_type
content_type = response._headers.get('content-type', [None, ''])[1]
if test_instance.expected_content_type:
test_instance.assertEqual(
content_type, test_instance.expected_content_type,
msg='Unexpected response content type: {}, expected: {}.'.format(
content_type, test_instance.expected_content_type
)
)
)
return response
@@ -55,7 +112,7 @@ class DatabaseConversionMixin(object):
)
class OpenFileCheckMixin(object):
class OpenFileCheckTestCaseMixin(object):
def _get_descriptor_count(self):
process = psutil.Process()
return process.num_fds()
@@ -65,7 +122,7 @@ class OpenFileCheckMixin(object):
return process.open_files()
def setUp(self):
super(OpenFileCheckMixin, self).setUp()
super(OpenFileCheckTestCaseMixin, self).setUp()
if getattr(settings, 'COMMON_TEST_FILE_HANDLES', False):
self._open_files = self._get_open_files()
@@ -80,10 +137,61 @@ class OpenFileCheckMixin(object):
self._skip_file_descriptor_test = False
super(OpenFileCheckMixin, self).tearDown()
super(OpenFileCheckTestCaseMixin, self).tearDown()
class TempfileCheckMixin(object):
class RandomPrimaryKeyModelMonkeyPatchMixin(object):
random_primary_key_random_floor = 100
random_primary_key_random_ceiling = 10000
random_primary_key_maximum_attempts = 100
@staticmethod
def get_unique_primary_key(model):
pk_list = model._meta.default_manager.values_list('pk', flat=True)
attempts = 0
while True:
primary_key = random.randint(
RandomPrimaryKeyModelMonkeyPatchMixin.random_primary_key_random_floor,
RandomPrimaryKeyModelMonkeyPatchMixin.random_primary_key_random_ceiling
)
if primary_key not in pk_list:
break
attempts = attempts + 1
if attempts > RandomPrimaryKeyModelMonkeyPatchMixin.random_primary_key_maximum_attempts:
raise Exception(
'Maximum number of retries for an unique random primary '
'key reached.'
)
return primary_key
def setUp(self):
self.method_save_original = models.Model.save
def method_save_new(instance, *args, **kwargs):
if instance.pk:
return self.method_save_original(instance, *args, **kwargs)
else:
instance.pk = RandomPrimaryKeyModelMonkeyPatchMixin.get_unique_primary_key(
model=instance._meta.model
)
instance.id = instance.pk
return instance.save_base(force_insert=True)
setattr(models.Model, 'save', method_save_new)
super(RandomPrimaryKeyModelMonkeyPatchMixin, self).setUp()
def tearDown(self):
models.Model.save = self.method_save_original
super(RandomPrimaryKeyModelMonkeyPatchMixin, self).tearDown()
class TempfileCheckTestCaseMixin(object):
# Ignore the jvmstat instrumentation and GitLab's CI .config files
# Ignore LibreOffice fontconfig cache dir
ignore_globs = ('hsperfdata_*', '.config', '.cache')
@@ -108,7 +216,7 @@ class TempfileCheckMixin(object):
) - set(ignored_result)
def setUp(self):
super(TempfileCheckMixin, self).setUp()
super(TempfileCheckTestCaseMixin, self).setUp()
if getattr(settings, 'COMMON_TEST_TEMP_FILES', False):
self._temporary_items = self._get_temporary_entries()
@@ -123,4 +231,101 @@ class TempfileCheckMixin(object):
','.join(final_temporary_items - self._temporary_items)
)
)
super(TempfileCheckMixin, self).tearDown()
super(TempfileCheckTestCaseMixin, self).tearDown()
class TestModelTestMixin(object):
def _create_test_model(self, fields=None, model_name='TestModel', options=None):
# Obtain the app_config and app_label from the test's module path
app_config = apps.get_containing_app_config(
object_name=self.__class__.__module__
)
app_label = app_config.label
class Meta:
pass
setattr(Meta, 'app_label', app_label)
if options is not None:
for key, value in options.items():
setattr(Meta, key, value)
def save(instance, *args, **kwargs):
# Custom .save() method to use random primary key values.
if instance.pk:
return models.Model.self(instance, *args, **kwargs)
else:
instance.pk = RandomPrimaryKeyModelMonkeyPatchMixin.get_unique_primary_key(
model=instance._meta.model
)
instance.id = instance.pk
return instance.save_base(force_insert=True)
attrs = {
'__module__': self.__class__.__module__, 'save': save, 'Meta': Meta
}
if fields:
attrs.update(fields)
# Clear previous model registration before re-registering it again to
# avoid conflict with test models with the same name, in the same app
# but from another test module.
apps.all_models[app_label].pop(model_name.lower(), None)
TestModel = type(
force_bytes(model_name), (models.Model,), attrs
)
setattr(self, model_name, TestModel)
with connection.schema_editor() as schema_editor:
schema_editor.create_model(model=TestModel)
ContentType.objects.clear_cache()
def _create_test_object(self, model_name='TestModel', **kwargs):
TestModel = getattr(self, model_name)
self.test_object = TestModel.objects.create(**kwargs)
class TestViewTestCaseMixin(object):
has_test_view = False
def tearDown(self):
from mayan.urls import urlpatterns
self.client.logout()
if self.has_test_view:
urlpatterns.pop(0)
super(TestViewTestCaseMixin, self).tearDown()
def add_test_view(self, test_object):
from mayan.urls import urlpatterns
def test_view(request):
template = Template('{{ object }}')
context = Context(
{'object': test_object, 'resolved_object': test_object}
)
return HttpResponse(template.render(context=context))
urlpatterns.insert(0, url(TEST_VIEW_URL, test_view, name=TEST_VIEW_NAME))
clear_url_caches()
self.has_test_view = True
def get_test_view(self):
response = self.get(TEST_VIEW_NAME)
if isinstance(response.context, ContextList):
# template widget rendering causes test client response to be
# ContextList rather than RequestContext. Typecast to dictionary
# before updating.
result = dict(response.context).copy()
result.update({'request': response.wsgi_request})
return Context(result)
else:
response.context.update({'request': response.wsgi_request})
return Context(response.context)

View File

@@ -1,7 +1,6 @@
from __future__ import unicode_literals
from django.test import override_settings
from django.urls import reverse
from mayan.apps.rest_api.tests import BaseAPITestCase
@@ -11,23 +10,40 @@ TEST_TEMPLATE_RESULT = '<div'
class CommonAPITestCase(BaseAPITestCase):
auto_login_user = False
def test_content_type_list_view(self):
response = self.client.get(reverse('rest_api:content-type-list'))
response = self.get(viewname='rest_api:content_type-list')
self.assertEqual(response.status_code, 200)
@override_settings(LANGUAGE_CODE='de')
def _request_template_detail_view(self):
return self.get(path=self.test_template.get_absolute_url())
def test_template_detail_view(self):
self.login_user()
template_main_menu = Template.get(name='menu_main')
response = self.client.get(template_main_menu.get_absolute_url())
self.test_template = Template.get(name='menu_main')
response = self._request_template_detail_view()
self.assertContains(
response=response, text=TEST_TEMPLATE_RESULT, status_code=200
)
@override_settings(LANGUAGE_CODE='de')
def test_template_detail_german_view(self):
self.login_user()
self.test_template = Template.get(name='menu_main')
response = self._request_template_detail_view()
self.assertContains(
response=response, text=TEST_TEMPLATE_RESULT, status_code=200
)
def test_template_detail_anonymous_view(self):
template_main_menu = Template.get(name='menu_main')
response = self.client.get(template_main_menu.get_absolute_url())
self.test_template = Template.get(name='menu_main')
response = self._request_template_detail_view()
self.assertNotContains(
response=response, text=TEST_TEMPLATE_RESULT, status_code=403
)

View File

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

View File

@@ -14,8 +14,6 @@ from .literals import TEST_ERROR_LOG_ENTRY_RESULT
class CommonViewTestCase(GenericViewTestCase):
def test_about_view(self):
self.login_user()
response = self.get('common:about_view')
self.assertContains(response, text='About', status_code=200)
@@ -25,27 +23,36 @@ class CommonViewTestCase(GenericViewTestCase):
)
ErrorLogEntry.objects.register(model=get_user_model())
self.error_log_entry = self.user.error_logs.create(
self.error_log_entry = self._test_case_user.error_logs.create(
result=TEST_ERROR_LOG_ENTRY_RESULT
)
def _request_object_error_log_list(self):
content_type = ContentType.objects.get_for_model(model=self.user)
content_type = ContentType.objects.get_for_model(model=self._test_case_user)
return self.get(
'common:object_error_list', kwargs={
'app_label': content_type.app_label,
'model': content_type.model,
'object_id': self.user.pk
'object_id': self._test_case_user.pk
}, follow=True
)
def test_object_error_list_view_with_permissions(self):
def test_object_error_list_view_no_permissions(self):
self._create_error_log_entry()
response = self._request_object_error_log_list()
self.assertNotContains(
response=response, text=TEST_ERROR_LOG_ENTRY_RESULT,
status_code=403
)
def test_object_error_list_view_with_access(self):
self._create_error_log_entry()
self.login_user()
self.grant_access(
obj=self.user, permission=permission_error_log_view
obj=self._test_case_user, permission=permission_error_log_view
)
response = self._request_object_error_log_list()
@@ -54,15 +61,3 @@ class CommonViewTestCase(GenericViewTestCase):
response=response, text=TEST_ERROR_LOG_ENTRY_RESULT,
status_code=200
)
def test_object_error_list_view_no_permissions(self):
self._create_error_log_entry()
self.login_user()
response = self._request_object_error_log_list()
self.assertNotContains(
response=response, text=TEST_ERROR_LOG_ENTRY_RESULT,
status_code=403
)

View File

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

View File

@@ -1,18 +1,15 @@
from __future__ import unicode_literals
import logging
import os
import shutil
import tempfile
from django.conf import settings
from django.core.exceptions import FieldDoesNotExist
from django.db.models.constants import LOOKUP_SEP
from django.urls import resolve as django_resolve
from django.urls.base import get_script_prefix
from django.utils.datastructures import MultiValueDict
from django.utils.http import urlencode as django_urlencode
from django.utils.http import urlquote as django_urlquote
from django.utils.module_loading import import_string
from django.utils.six.moves import reduce as reduce_function
from django.utils.six.moves import xmlrpc_client
@@ -20,7 +17,6 @@ import mayan
from .exceptions import NotLatestVersion, UnknownLatestVersion
from .literals import DJANGO_SQLITE_BACKEND, MAYAN_PYPI_NAME, PYPI_URL
from .settings import setting_temporary_directory
logger = logging.getLogger(__name__)
@@ -39,27 +35,6 @@ def check_version():
raise NotLatestVersion(upstream_version=versions[0])
# http://stackoverflow.com/questions/123198/how-do-i-copy-a-file-in-python
def copyfile(source, destination, buffer_size=1024 * 1024):
"""
Copy a file from source to dest. source and dest
can either be strings or any object with a read or
write method, like StringIO for example.
"""
source_descriptor = get_descriptor(source)
destination_descriptor = get_descriptor(destination, read=False)
while True:
copy_buffer = source_descriptor.read(buffer_size)
if copy_buffer:
destination_descriptor.write(copy_buffer)
else:
break
source_descriptor.close()
destination_descriptor.close()
def encapsulate(function):
# Workaround Django ticket 15791
# Changeset 16045
@@ -68,74 +43,49 @@ def encapsulate(function):
return lambda: function
def fs_cleanup(filename, file_descriptor=None, suppress_exceptions=True):
"""
Tries to remove the given filename. Ignores non-existent files
"""
if file_descriptor:
os.close(file_descriptor)
def get_related_field(model, related_field_name):
try:
os.remove(filename)
except OSError:
local_field_name, remaining_field_path = related_field_name.split(
LOOKUP_SEP, 1
)
except ValueError:
local_field_name = related_field_name
remaining_field_path = None
related_field = model._meta.get_field(local_field_name)
if remaining_field_path:
return get_related_field(
model=related_field.related_model,
related_field_name=remaining_field_path
)
return related_field
def introspect_attribute(attribute_name, obj):
try:
# Try as a related field
obj._meta.get_field(field_name=attribute_name)
except (AttributeError, FieldDoesNotExist):
attribute_name = attribute_name.replace('__', '.')
try:
shutil.rmtree(filename)
except OSError:
if suppress_exceptions:
pass
else:
raise
def get_descriptor(file_input, read=True):
try:
# Is it a file like object?
file_input.seek(0)
except AttributeError:
# If not, try open it.
if read:
return open(file_input, mode='rb')
# If there are separators in the attribute name, traverse them
# to the final attribute
attribute_part, attribute_remaining = attribute_name.split(
'.', 1
)
except ValueError:
return attribute_name, obj
else:
return open(file_input, mode='wb')
related_field = obj._meta.get_field(field_name=attribute_part)
return introspect_attribute(
attribute_name=attribute_part,
obj=related_field.related_model,
)
else:
return file_input
def get_storage_subclass(dotted_path):
"""
Import a storage class and return a subclass that will always return eq
True to avoid creating a new migration when for runtime storage class
changes.
"""
imported_storage_class = import_string(dotted_path=dotted_path)
class StorageSubclass(imported_storage_class):
def __init__(self, *args, **kwargs):
return super(StorageSubclass, self).__init__(*args, **kwargs)
def __eq__(self, other):
return True
def deconstruct(self):
return ('mayan.apps.common.classes.FakeStorageSubclass', (), {})
return StorageSubclass
def TemporaryFile(*args, **kwargs):
kwargs.update({'dir': setting_temporary_directory.value})
return tempfile.TemporaryFile(*args, **kwargs)
def mkdtemp(*args, **kwargs):
kwargs.update({'dir': setting_temporary_directory.value})
return tempfile.mkdtemp(*args, **kwargs)
def mkstemp(*args, **kwargs):
kwargs.update({'dir': setting_temporary_directory.value})
return tempfile.mkstemp(*args, **kwargs)
return attribute_name, obj
def resolve(path, urlconf=None):
@@ -230,24 +180,3 @@ def urlquote(link=None, get=None):
return '%s%s' % (link, django_urlencode(get, doseq=True))
else:
return django_urlquote(link)
def validate_path(path):
if not os.path.exists(path):
# If doesn't exist try to create it
try:
os.mkdir(path)
except Exception as exception:
logger.debug('unhandled exception: %s', exception)
return False
# Check if it is writable
try:
fd, test_filepath = tempfile.mkstemp(dir=path)
os.close(fd)
os.unlink(test_filepath)
except Exception as exception:
logger.debug('unhandled exception: %s', exception)
return False
return True

View File

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

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.template import Context, Template
from django.utils.encoding import force_text
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from .icons import icon_fail as default_icon_fail

View File

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

View File

@@ -1,9 +1,9 @@
from __future__ import unicode_literals
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common import MayanAppConfig, menu_object, menu_sidebar
from mayan.apps.acls.classes import ModelPermission
from mayan.apps.common import MayanAppConfig, menu_object, menu_secondary
from mayan.apps.navigation import SourceColumn
from .links import (
@@ -23,25 +23,31 @@ class ConverterApp(MayanAppConfig):
def ready(self):
super(ConverterApp, self).ready()
Transformation = self.get_model('Transformation')
Transformation = self.get_model(model_name='Transformation')
ModelPermission.register_inheritance(
model=Transformation, related='content_object'
)
SourceColumn(source=Transformation, label=_('Order'), attribute='order')
SourceColumn(
source=Transformation, label=_('Transformation'),
func=lambda context: force_text(context['object'])
attribute='order', include_label=True, source=Transformation
)
SourceColumn(
source=Transformation, label=_('Arguments'), attribute='arguments'
attribute='get_transformation_label', is_identifier=True,
source=Transformation
)
SourceColumn(
attribute='arguments', include_label=True, source=Transformation
)
menu_object.bind_links(
links=(link_transformation_edit, link_transformation_delete),
sources=(Transformation,)
)
menu_sidebar.bind_links(
menu_secondary.bind_links(
links=(link_transformation_create,), sources=(Transformation,)
)
menu_sidebar.bind_links(
menu_secondary.bind_links(
links=(link_transformation_create,),
sources=(
'converter:transformation_create',

View File

@@ -11,7 +11,7 @@ import sh
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.utils import fs_cleanup, mkstemp
from mayan.apps.storage.utils import fs_cleanup, mkstemp
from ..classes import ConverterBase
from ..exceptions import PageCountError

View File

@@ -9,9 +9,9 @@ import sh
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.settings import setting_temporary_directory
from mayan.apps.common.utils import fs_cleanup, mkdtemp, mkstemp
from mayan.apps.mimetype.api import get_mimetype
from mayan.apps.storage.settings import setting_temporary_directory
from mayan.apps.storage.utils import fs_cleanup, mkdtemp, mkstemp
from .exceptions import InvalidOfficeFormat, OfficeConversionError
from .literals import (

View File

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

View File

@@ -3,4 +3,9 @@ from __future__ import absolute_import, unicode_literals
from mayan.apps.appearance.classes import Icon
icon_transformation = Icon(driver_name='fontawesome', symbol='crop')
icon_transformation_create = Icon(driver_name='fontawesome', symbol='plus')
icon_transformation_create = Icon(
driver_name='fontawesome-dual', primary_symbol='crop',
secondary_symbol='plus'
)
icon_transformation_delete = Icon(driver_name='fontawesome', symbol='times')
icon_transformation_edit = Icon(driver_name='fontawesome', symbol='pencil-alt')

Some files were not shown because too many files have changed in this diff Show More