Compare commits

..

1 Commits

Author SHA1 Message Date
Roberto Rosario
78b9df972c Initial generic error log refactor
Signed-off-by: Roberto Rosario <Roberto.Rosario.Gonzalez@gmail.com>
2019-01-09 23:17:54 -04:00
557 changed files with 15489 additions and 21928 deletions

View File

@@ -1,6 +1,5 @@
4.0 (2019-XX-XX)
================
<<<<<<< HEAD
- Documents: Add a server side template for invalid documents.
The new template can be accessed via the templates API.
Improve the way invalid documents are detected in the JavaScript
@@ -214,49 +213,6 @@
- Event handler to highlight panels when selected.
- Improve duplicated document display.
- Filter document duplicted count by access.
- Updated the tags app to comply with MERCs 5 and 6.
- 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).
- Reject emails attachments of size 0. Thanks to Robert Schoeftner
(@robert.schoeftner)for the report and solution. GitLab issue #574.
- Fix multiple tag selection wizard step.
3.1.9 (2018-11-01)

View File

@@ -62,16 +62,15 @@ 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 $(ARGUMENTS)
./manage.py test $(MODULE) --settings=mayan.settings.testing.development --nomigrations
test-all:
./manage.py test --mayan-apps --settings=mayan.settings.testing.development --nomigrations $(ARGUMENTS)
./manage.py test --mayan-apps --settings=mayan.settings.testing.development --nomigrations
test-launch-postgres:
@docker rm -f test-postgres || true

View File

@@ -1445,7 +1445,7 @@ sudo -u mayan \
dialog --infobox "Preparing static files" 3 70
sudo -u mayan \
MAYAN_MEDIA_ROOT=$MAYAN_MEDIA_ROOT \
$MAYAN_BIN preparestatic --noinput > /dev/null
$MAYAN_BIN collectstatic --noinput > /dev/null
# Create supervisor file for gunicorn (frontend), 3 background workers, and the scheduler for periodic tasks
cat > /etc/supervisor/conf.d/mayan.conf <<EOF

View File

@@ -161,7 +161,7 @@ priority = 998
EOF
echo -e "\n -> Collecting the static files \n"
mayan-edms.py preparestatic --noinput
mayan-edms.py collectstatic --noinput
echo -e "\n -> Making the installation directory readable and writable by the webserver user \n"
chown www-data:www-data ${INSTALLATION_DIRECTORY} -R

View File

@@ -48,7 +48,6 @@ apt-get install -y --no-install-recommends \
supervisor \
tesseract-ocr \
zlib1g-dev \
libssl-dev \
&& \
apt-get clean autoclean && \
apt-get autoremove --purge -y && \
@@ -69,6 +68,7 @@ ln -s /usr/lib/aarch64-linux-gnu/libjpeg.so /usr/lib/ \
# Pillow can't find zlib or libjpeg on armv7l (ODROID HC1)
RUN if [ "$(uname -m)" = "armv7l" ]; then \
apt-get install libssl-dev -y && \
ln -s /usr/lib/arm-linux-gnueabihf/libz.so /usr/lib/ && \
ln -s /usr/lib/arm-linux-gnueabihf/libjpeg.so /usr/lib/ \
; fi
@@ -132,11 +132,11 @@ COPY --from=BUILDER_IMAGE /code/docker/version .
RUN chown -R mayan:mayan $PROJECT_INSTALL_DIR
# Install build Mayan EDMS
RUN sudo -u mayan $PYTHON_PIP install --no-cache-dir --no-use-pep517 *.whl && \
RUN sudo -u mayan $PYTHON_PIP install --no-cache-dir *.whl && \
rm *.whl
# Install Python clients for librabbitmq, MySQL, PostgreSQL, REDIS
RUN sudo -u mayan $PYTHON_PIP install --no-cache-dir --no-use-pep517 librabbitmq==1.6.1 mysql-python==1.2.5 psycopg2==2.7.3.2 redis==2.10.6
RUN sudo -u mayan $PYTHON_PIP install --no-cache-dir librabbitmq==1.6.1 mysql-python==1.2.5 psycopg2==2.7.3.2 redis==2.10.6
# Setup supervisor
COPY docker/etc/supervisor/mayan.conf /etc/supervisor/conf.d

View File

@@ -55,13 +55,13 @@ chown mayan:mayan /var/lib/mayan -R
initialize() {
echo "mayan: initialize()"
su mayan -c "${MAYAN_BIN} initialsetup --force"
su mayan -c "${MAYAN_BIN} preparestatic --noinput --clear"
su mayan -c "${MAYAN_BIN} collectstatic --noinput --clear"
}
upgrade() {
echo "mayan: upgrade()"
su mayan -c "${MAYAN_BIN} performupgrade"
su mayan -c "${MAYAN_BIN} preparestatic --noinput --clear"
su mayan -c "${MAYAN_BIN} collectstatic --noinput --clear"
}
start() {

View File

@@ -82,7 +82,7 @@ Collect the static files:
::
sudo -u mayan MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
/opt/mayan-edms/bin/mayan-edms.py preparestatic --noinput
/opt/mayan-edms/bin/mayan-edms.py collectstatic --noinput
Create the supervisor file at ``/etc/supervisor/conf.d/mayan.conf``:
--------------------------------------------------------------------
@@ -244,7 +244,7 @@ Collect the static files:
::
sudo -u mayan MAYAN_MEDIA_ROOT=/opt/mayan-edms/media \
/opt/mayan-edms/bin/mayan-edms.py preparestatic --noinput
/opt/mayan-edms/bin/mayan-edms.py collectstatic --noinput
Create the RabbitMQ user and vhost:
-----------------------------------

View File

@@ -31,9 +31,9 @@ for Mayan EDMS. Most MERCs will be Feature MERCs.
2. An **Informational** MERC describes a Mayan EDMS design issue, or
provides general guidelines or information to the Mayan EDMS community,
but does not propose a new feature. Informational MERCs do not
necessarily represent a community consensus or recommendation, so users
and implementers are free to ignore Informational MERCs or follow their
advice.
necessarily represent a community consensus or
recommendation, so users and implementers are free to ignore
Informational MERCs or follow their advice.
3. A **Process** MERC describes a process surrounding Mayan EDMS, or
proposes a change to (or an event in) a process. Process MERCs are

View File

@@ -1,6 +1,6 @@
====================
=====================
MERC 2: Test writing
====================
=====================
:MERC: 2
:Author: Michael Price

View File

@@ -1,149 +0,0 @@
==========================
MERC 5: Explicit arguments
==========================
:MERC: 5
:Author: Roberto Rosario
:Status: Accepted
:Type: Feature
:Created: 2018-12-30
:Last-Modified: 2018-12-31
.. contents:: Table of Contents
:depth: 3
:local:
Abstract
========
This MERC proposes the adoption of a new methodology when performing calls.
It seeks to reduce the use of positional arguments in favor of keyword
arguments in as many places as possible.
Motivation
==========
As the project grows, legibility of code becomes more important. Keyword
argument help document the use of services, clases and functions. Refactors
that affect the interface of services are also easier to find and update and
fix. Positional argument can cause a call to continue working as long as the
datatype of the argument remains the same. Usage of keyword arguments will
automatically raise and error that will prevent such situations. Keyword
argument further eliminate the relevance of position or the arguments, and
the arguments can be sorted alphabetically for easier visual scanning or by
semantic significance improving code readability.
Specification
=============
Adoption of this MERC will require an audit of existing calls and the use
of the method proposed for new calls. Every call regardless of the type or
origin of the source callable will name each argument used. By type it is
meant: classes, functions, methods. Origin means: local from the project,
from the framework, third party libraries or the standard library.
Backwards Compatibility
=======================
No backwards compatibility issues are expected. New errors arising from the use
if keyword arguments could be interpreted as existing latent issues that
have not been uncovered.
Reference Implementation
========================
Example:
Before:
.. code-block:: python
from mayan.apps.common.classes import Template
Template(
'menu_main', 'appearance/menu_main.html'
)
After:
.. code-block:: python
from mayan.apps.common.classes import Template
Template(
name='menu_main', template_name='appearance/menu_main.html'
)
When calls use a mixture or positional and keyword arguments, the keywords
arguments can only be found after the positional arguments. Complete use
of keyword arguments allow the reposition of arguments for semantic
purposes.
Example:
Before:
.. code-block:: python
from django.conf.urls import url
from .views import AboutView, HomeView, RootView
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'),
]
After:
.. code-block:: python
from django.conf.urls import url
from .views import AboutView, HomeView, RootView
urlpatterns = [
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()),
]
Keyword arguments should also be used for callables that pass those to others
down the line like Django's ``reverse`` function. Any change to the name of
the ``pk`` URL parameter will raise an exception in this code alerting to
any posible incompatible use.
Example:
.. code-block:: python
def get_absolute_url(self):
return reverse(
viewname='documents:document_preview', kwargs={'pk': self.pk}
)
This becomes even more important when multiple URL parameters are used. Since
the API documentation is auto generated from the code itself, it would make
sense to rename the first URL parameter from ``pk`` to ``document_pk``. Such
change will cause all address to view resolutions to break forcing their
update and allowing all consumers' interface usage to remain synchonized to the
callable's interface.
.. code-block:: python
url(
regex=r'^documents/(?P<pk>[0-9]+)/versions/(?P<document_version_pk>[0-9]+)/pages/(?P<document_page_pk>[0-9]+)/image/$',
name='documentpage-image', view=APIDocumentPageImageView.as_view()
),

View File

@@ -1,81 +0,0 @@
==================================
MERC 6: Lower information disclose
==================================
:MERC: 6
:Author: Michael Price
:Status: Accepted
:Type: Feature
:Created: 2018-12-30
:Last-Modified: 2018-12-31
.. contents:: Table of Contents
:depth: 3
:local:
Abstract
========
This MERC proposes the use of errors that don't disclose the existance of a
resource in the event that the requester doesn't have the required credentials.
Motivation
==========
When an user tries to perform an action like opening a view to a document for
which the required permission is missing, a permission required or access
denied error is presented. This is semantically correct, but from the stand
point of security it is still failing because it is letting the user know
that such document exists in the first place. This MERC proposes changing the
error message for existing resource to one that doesn't divulge any information
to unauthorized parties, like "Not Found".
Specification
=============
Out of the 4 basic CRUD operations, Read, Update and Delete should return an
HTTP 404 error instead of an HTTP 403 error. Only the Create operation will
continue returning the current HTTP 403 error, unless it is creating a
new resource that is related to an existing resource.
Since most view use the internal custom CRUD classes making a change to the
``ObjectPermissionCheckMixin`` class to raise an HTTP 404 on object access
failure will fulfill the proposal of this MERC.
Adding the ``object_permission_raise_404`` class attribute and setting it
to default to False will allow fulfullin the goal of this MERC while
keeping the existing functionality intact.
Example:
.. code-block:: python
class ObjectPermissionCheckMixin(object):
"""
If object_permission_raise_404 is True an HTTP 404 error will be raised
instead of the normal 403.
"""
object_permission = None
object_permission_raise_404 = False
def get_permission_object(self):
return self.get_object()
def dispatch(self, request, *args, **kwargs):
if self.object_permission:
try:
AccessControlList.objects.check_access(
permissions=self.object_permission, user=request.user,
obj=self.get_permission_object(),
related=getattr(self, 'object_permission_related', None)
)
except PermissionDenied:
if self.object_permission_raise_404:
raise Http404
else:
raise
return super(
ObjectPermissionCheckMixin, self
).dispatch(request, *args, **kwargs)

View File

@@ -20,8 +20,6 @@ Accepted
../mercs/0001-merc-process
../mercs/0002-test-writing
../mercs/0003-using-javascript-libraries
../mercs/0005-explicit-arguments
../mercs/0006-lower-information-disclose
Draft
-----
@@ -51,5 +49,3 @@ Feature
../mercs/0002-test-writing
../mercs/0003-using-javascript-libraries
../mercs/0005-explicit-arguments
../mercs/0006-lower-information-disclose

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 ``restrict_queryset``
5. Update the role checks in the ``check_access`` and ``filter_by_access``
``AccessControlList`` model manager methods.

View File

@@ -7,24 +7,6 @@ Released: XX XX, 2019
Changes
-------
Switch to full app paths
^^^^^^^^^^^^^^^^^^^^^^^^
Instead of inserting the path of the apps into the Python app,
the apps are now referenced by their full import path.
This solves name clashes with external or native Python libraries.
Example: Mayan statistics app vs. Python new statistics library.
Every app reference is now prepended with 'mayan.apps'.
Existing config.yml files need to be updated manually.
Other changes
^^^^^^^^^^^^^
* Split source models into different modules.
* Fix multiple tag selection wizard step.
Removals
--------
@@ -81,7 +63,7 @@ Migrate existing database schema with::
Add new static media::
$ mayan-edms.py preparestatic --noinput
$ mayan-edms.py collectstatic --noinput
The upgrade procedure is now complete.

View File

@@ -185,7 +185,7 @@ Django's development server doesn't serve static files unless the DEBUG option
is set to True, this mode of operation should only be used for development or
testing. For production deployments the management command::
$ mayan-edms.py preparestatic
$ mayan-edms.py collectstatic
should be used and the resulting static folder served from a webserver.
For more information check the

View File

@@ -1,121 +1,202 @@
from __future__ import absolute_import, unicode_literals
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.response import Response
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 django.contrib.contenttypes.models import ContentType
from django.shortcuts import get_object_or_404
from rest_framework import generics
from .models import AccessControlList
from .permissions import permission_acl_edit, permission_acl_view
from .serializers import AccessControlListSerializer
from .serializers import (
AccessControlListPermissionSerializer, AccessControlListSerializer,
WritableAccessControlListPermissionSerializer,
WritableAccessControlListSerializer
)
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 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(),
}
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']
)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def get_external_object_permission(self):
action = getattr(self, 'action', None)
if action is None:
return None
elif action in ['list', 'retrieve', 'permission_list', 'permission_inherited_list']:
return permission_acl_view
content_object = get_object_or_404(
klass=content_type.model_class(), pk=self.kwargs['object_pk']
)
if self.request.method == 'GET':
permission_required = permission_acl_view
else:
return permission_acl_edit
permission_required = permission_acl_edit
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()
AccessControlList.objects.check_access(
permissions=permission_required, user=self.request.user,
obj=content_object
)
return content_object
def get_queryset(self):
return self.get_external_object().acls.all()
return self.get_content_object().acls.all()
@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
def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
context = super(APIObjectACLListView, self).get_serializer_context()
if self.kwargs:
context.update(
{
'content_object': self.get_content_object(),
}
)
return context
def get_serializer(self, *args, **kwargs):
if not self.request:
return None
return super(APIObjectACLListView, self).get_serializer(*args, **kwargs)
def get_serializer_class(self):
if self.request.method == 'GET':
return AccessControlListSerializer
else:
return WritableAccessControlListSerializer
class APIObjectACLView(generics.RetrieveDestroyAPIView):
"""
delete: Delete the selected access control list.
get: Returns the details of the selected access control list.
"""
serializer_class = AccessControlListSerializer
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']
)
@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}
content_object = get_object_or_404(
klass=content_type.model_class(), pk=self.kwargs['object_pk']
)
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}
AccessControlList.objects.check_access(
permissions=permission_required, user=self.request.user,
obj=content_object
)
if page is not None:
return self.get_paginated_response(serializer.data)
return content_object
return Response(serializer.data)
def get_queryset(self):
return self.get_content_object().acls.all()
@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
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['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_pk']
)
AccessControlList.objects.check_access(
permissions=permission_acl_view, user=self.request.user,
obj=content_object
)
return content_object
def get_queryset(self):
return self.get_acl().permissions.all()
def get_serializer(self, *args, **kwargs):
if not self.request:
return None
return super(APIObjectACLPermissionListView, self).get_serializer(*args, **kwargs)
def get_serializer_class(self):
if self.request.method == 'GET':
return AccessControlListPermissionSerializer
else:
return WritableAccessControlListPermissionSerializer
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['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_pk']
)
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_context(self):
context = super(APIObjectACLPermissionView, self).get_serializer_context()
if self.kwargs:
context.update(
{
'acl': self.get_acl(),
}
)
return context

View File

@@ -2,15 +2,9 @@ from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
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.common import MayanAppConfig, menu_object, menu_sidebar
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
@@ -24,33 +18,21 @@ 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',
)
AccessControlList = self.get_model('AccessControlList')
SourceColumn(
attribute='role', is_identifier=True, is_sortable=True,
source=AccessControlList
source=AccessControlList, label=_('Role'), attribute='role'
)
SourceColumn(
source=AccessControlList, label=_('Permissions'),
attribute='get_permission_titles'
)
menu_object.bind_links(
links=(
link_acl_permissions, link_acl_delete,
link_events_for_object,
link_object_event_types_user_subcriptions_list
),
links=(link_acl_permissions, link_acl_delete),
sources=(AccessControlList,)
)
menu_secondary.bind_links(
menu_sidebar.bind_links(
links=(link_acl_create,), sources=('acls:acl_list',)
)
registry.register(AccessControlList)

View File

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

View File

@@ -1,16 +0,0 @@
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

@@ -1,13 +0,0 @@
from __future__ import unicode_literals
from django import forms
from mayan.apps.common.forms import FilteredSelectionForm
from .models import AccessControlList
class ACLCreateForm(FilteredSelectionForm, forms.ModelForm):
class Meta:
fields = ('role',)
model = AccessControlList

View File

@@ -2,7 +2,6 @@ from __future__ import absolute_import, unicode_literals
from mayan.apps.appearance.classes import Icon
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

@@ -4,9 +4,8 @@ from django.apps import apps
from django.utils.translation import ugettext_lazy as _
from mayan.apps.navigation import Link
from mayan.apps.permissions.icons import icon_permission
from .icons import icon_acl_delete, icon_acl_list, icon_acl_new
from .icons import icon_acl_list, icon_acl_new
from .permissions import permission_acl_edit, permission_acl_view
@@ -21,7 +20,7 @@ def get_kwargs_factory(variable_name):
)
return {
'app_label': '"{}"'.format(content_type.app_label),
'model_name': '"{}"'.format(content_type.model),
'model': '"{}"'.format(content_type.model),
'object_id': '{}.pk'.format(variable_name)
}
@@ -29,21 +28,21 @@ def get_kwargs_factory(variable_name):
link_acl_delete = Link(
icon_class=icon_acl_delete, kwargs={'acl_id': 'resolved_object.pk'},
permission=permission_acl_edit, tags='dangerous', text=_('Delete'),
args='resolved_object.pk', permissions=(permission_acl_edit,),
permissions_related='content_object', tags='dangerous', text=_('Delete'),
view='acls:acl_delete',
)
link_acl_list = Link(
icon_class=icon_acl_list, kwargs=get_kwargs_factory(
variable_name='resolved_object'
), permission=permission_acl_view, text=_('ACLs'), view='acls:acl_list'
icon_class=icon_acl_list, kwargs=get_kwargs_factory('resolved_object'),
permissions=(permission_acl_view,), text=_('ACLs'), view='acls:acl_list'
)
link_acl_create = Link(
icon_class=icon_acl_new, kwargs=get_kwargs_factory('resolved_object'),
permission=permission_acl_edit, text=_('New ACL'), view='acls:acl_create'
permissions=(permission_acl_edit,), text=_('New ACL'),
view='acls:acl_create'
)
link_acl_permissions = Link(
args='resolved_object.pk', icon_class=icon_permission,
permission=permission_acl_edit, text=_('Permissions'),
args='resolved_object.pk', permissions=(permission_acl_edit,),
permissions_related='content_object', text=_('Permissions'),
view='acls:acl_permissions',
)

View File

@@ -1,21 +1,15 @@
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 CharField, Value as V, Q
from django.db.models.functions import Concat
from django.http import Http404
from django.db.models import Q
from django.utils.translation import ugettext
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.utils import (
get_related_field, resolve_attribute, return_related
)
from mayan.apps.common.warnings import InterfaceWarning
from mayan.apps.common.utils import resolve_attribute, return_related
from mayan.apps.permissions import Permission
from mayan.apps.permissions.models import StoredPermission
@@ -30,189 +24,200 @@ class AccessControlListManager(models.Manager):
Implement a 3 tier permission system, involving a permissions, an actor
and an object
"""
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
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
)
return True
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')
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')
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
)
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:
return Permission.check_permissions(
requester=user, permissions=permissions
)
except PermissionDenied:
try:
related_fields = ModelPermission.get_inheritances(
model=queryset.model
stored_permissions = [
permission.stored_permission for permission in permissions
]
except TypeError:
# Not a list of permissions, just one
stored_permissions = (permissions.stored_permission,)
if related:
obj = resolve_attribute(obj=obj, attribute=related)
try:
parent_accessor = ModelPermission.get_inheritance(
model=obj._meta.model
)
except AttributeError:
# AttributeError means non model objects: ie Statistics
# These can't have ACLs so we raise PermissionDenied
raise PermissionDenied(_('Insufficient access for: %s') % obj)
except KeyError:
pass
else:
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
try:
return self.check_access(
obj=getattr(obj, parent_accessor),
permissions=permissions, user=user
)
relation_result.append(reduce(operator.and_, inherited_acl_queries))
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
result.append(reduce(operator.or_, relation_result))
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
return result
user_roles.append(role)
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
)
if not self.filter(content_type=ContentType.objects.get_for_model(obj), object_id=obj.pk, permissions__in=stored_permissions, role__in=user_roles).exists():
logger.debug(
'Permissions "%s" on "%s" denied for user "%s"',
permissions, obj, user
)
raise PermissionDenied(ugettext('Insufficient access for: %s') % obj)
if queryset.filter(pk=obj.pk).exists():
return True
else:
if raise_404:
raise Http404
else:
raise PermissionDenied
logger.debug(
'Permissions "%s" on "%s" granted to user "%s" through roles "%s" by direct ACL',
permissions, obj, user, user_roles
)
def get_inherited_permissions(self, obj, role):
queryset = self._get_inherited_object_permissions(obj=obj, role=role)
queryset = queryset | role.permissions.all()
# Filter the permissions to the ones that apply to the model
queryset = ModelPermission.get_for_instance(
instance=obj
).filter(
pk__in=queryset
)
return queryset
def _get_inherited_object_permissions(self, obj, role):
queryset = StoredPermission.objects.none()
if not obj:
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:
related_fields = ModelPermission.get_inheritances(
model=type(obj)
Permission.check_permissions(
requester=user, permissions=(permission,)
)
except KeyError:
pass
else:
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
except PermissionDenied:
user_roles = []
for group in user.groups.all():
for role in group.roles.all():
user_roles.append(role)
queryset = queryset | self._get_inherited_object_permissions(
obj=parent_object, role=role
try:
parent_accessor = ModelPermission.get_inheritance(
model=queryset.model
)
except KeyError:
parent_acl_query = Q()
else:
instance = queryset.first()
if instance:
parent_object = return_related(
instance=instance, related_field=parent_accessor
)
return queryset
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(
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(permissions=permission, user=user, obj=entry)
except PermissionDenied:
pass
else:
result.append(entry.pk)
def grant(self, obj, permission, role):
return queryset.filter(pk__in=result)
else:
parent_acl_query = Q()
# Directly granted access
content_type = ContentType.objects.get_for_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
)
return queryset.filter(parent_acl_query | acl_query)
else:
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(type(instance))
except KeyError:
return StoredPermission.objects.none()
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()
def grant(self, permission, role, obj):
class_permissions = ModelPermission.get_for_class(klass=obj.__class__)
if permission not in class_permissions:
raise PermissionNotValidForClass
@@ -225,44 +230,7 @@ class AccessControlListManager(models.Manager):
acl.permissions.add(permission.stored_permission)
return acl
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):
def revoke(self, permission, role, obj):
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,18 +1,15 @@
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, transaction
from django.urls import reverse
from django.db import models
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__)
@@ -32,11 +29,6 @@ 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
@@ -65,17 +57,13 @@ class AccessControlList(models.Model):
def __str__(self):
return _(
'Role "%(role)s" permission\'s for "%(object)s"'
'Permissions "%(permissions)s" to role "%(role)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_id': self.pk}
)
def get_inherited_permissions(self):
return AccessControlList.objects.get_inherited_permissions(
role=self.role, obj=self.content_object
@@ -90,33 +78,3 @@ 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

@@ -7,8 +7,8 @@ from mayan.apps.permissions import PermissionNamespace
namespace = PermissionNamespace(label=_('Access control lists'), name='acls')
permission_acl_edit = namespace.add_permission(
label=_('Edit ACLs'), name='acl_edit'
name='acl_edit', label=_('Edit ACLs')
)
permission_acl_view = namespace.add_permission(
label=_('View ACLs'), name='acl_view'
name='acl_view', label=_('View ACLs')
)

View File

@@ -1,143 +1,206 @@
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.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 mayan.apps.permissions import Permission
from mayan.apps.permissions.models import Role, StoredPermission
from mayan.apps.permissions.serializers import (
PermissionSerializer, RoleSerializer
)
from .models import AccessControlList
class AccessControlListSerializer(ExternalObjectSerializerMixin, serializers.ModelSerializer):
class AccessControlListSerializer(serializers.ModelSerializer):
content_type = ContentTypeSerializer(read_only=True)
role = RoleSerializer(read_only=True)
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'),
permissions_url = serializers.SerializerMethodField(
help_text=_(
'Primary key of the role of the ACL that will be created or edited.'
), required=False, write_only=True
'API URL pointing to the list of permissions for this access '
'control 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-detail'
role = RoleSerializer(read_only=True)
url = serializers.SerializerMethodField()
class Meta:
fields = (
'content_type', 'id', 'object_id', 'permissions_url', 'role', 'url'
)
model = AccessControlList
def get_permissions_url(self, instance):
return reverse(
'rest_api:accesscontrollist-permission-list', args=(
instance.content_type.app_label, instance.content_type.model,
instance.object_id, instance.pk
), request=self.context['request'], format=self.context['format']
)
def get_url(self, instance):
return reverse(
'rest_api:accesscontrollist-detail', args=(
instance.content_type.app_label, instance.content_type.model,
instance.object_id, instance.pk
), request=self.context['request'], format=self.context['format']
)
class AccessControlListPermissionSerializer(PermissionSerializer):
acl_permission_url = serializers.SerializerMethodField(
help_text=_(
'API URL pointing to a permission in relation to the '
'access control list to which it is attached. This URL is '
'different than the canonical workflow URL.'
)
)
acl_url = serializers.SerializerMethodField()
def get_acl_permission_url(self, instance):
return reverse(
'rest_api:accesscontrollist-permission-detail', args=(
self.context['acl'].content_type.app_label,
self.context['acl'].content_type.model,
self.context['acl'].object_id, self.context['acl'].pk,
instance.stored_permission.pk
), request=self.context['request'], format=self.context['format']
)
def get_acl_url(self, instance):
return reverse(
'rest_api:accesscontrollist-detail', args=(
self.context['acl'].content_type.app_label,
self.context['acl'].content_type.model,
self.context['acl'].object_id, self.context['acl'].pk
), request=self.context['request'], format=self.context['format']
)
class WritableAccessControlListPermissionSerializer(AccessControlListPermissionSerializer):
permission_pk = serializers.CharField(
help_text=_(
'Primary key of the new permission to grant to the access control '
'list.'
), write_only=True
)
class Meta:
external_object_model = Role
external_object_pk_field = 'role_id'
external_object_permission = permission_role_edit
fields = (
'content_type', 'id', 'object_id', 'permission_add_url',
'permission_list_url', 'permission_list_inherited_url',
'permission_remove_url', 'role', 'role_id',
'url'
)
model = AccessControlList
read_only_fields = ('object_id',)
fields = ('namespace',)
read_only_fields = ('namespace',)
def create(self, validated_data):
role = self.get_external_object()
for permission in validated_data['permissions']:
self.context['acl'].permissions.add(permission)
if role:
validated_data['role'] = role
return validated_data['permissions'][0]
return super(AccessControlListSerializer, self).create(
validated_data=validated_data
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', args=(
instance.content_type.app_label, instance.content_type.model,
instance.object_id, 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 get_url(self, instance):
return reverse(
'rest_api:accesscontrollist-detail', args=(
instance.content_type.app_label, instance.content_type.model,
instance.object_id, instance.pk
), request=self.context['request'], format=self.context['format']
)
def validate(self, attrs):
attrs['content_type'] = ContentType.objects.get_for_model(
self.context['content_object']
)
attrs['object_id'] = self.context['content_object'].pk
try:
attrs['role'] = Role.objects.get(pk=attrs.pop('role_pk'))
except Role.DoesNotExist as exception:
raise ValidationError(force_text(exception))
permissions_pk_list = attrs.pop('permissions_pk_list', None)
permissions_result = []
if permissions_pk_list:
for pk in permissions_pk_list.split(','):
try:
permission = Permission.get(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,73 +0,0 @@
from __future__ import unicode_literals
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ImproperlyConfigured
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 ACLTestCaseMixin(RoleTestCaseMixin, UserTestCaseMixin):
def setUp(self):
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):
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
)
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,13 +9,16 @@ 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._test_case_role.pk],
'permissions': [permission_document_view.pk],
'roles': [self.role.pk],
'permissions': [permission_document_view.uuid],
}
)
action.execute(context={'entry_log': self.entry_log})
@@ -25,7 +28,7 @@ class ACLActionTestCase(ActionTestCase):
list(self.document.acls.first().permissions.all()),
[permission_document_view.stored_permission]
)
self.assertEqual(self.document.acls.first().role, self._test_case_role)
self.assertEqual(self.document.acls.first().role, self.role)
def test_revoke_access_action(self):
self.grant_access(
@@ -36,8 +39,8 @@ class ACLActionTestCase(ActionTestCase):
form_data={
'content_type': ContentType.objects.get_for_model(model=self.document).pk,
'object_id': self.document.pk,
'roles': [self._test_case_role.pk],
'permissions': [permission_document_view.pk],
'roles': [self.role.pk],
'permissions': [permission_document_view.uuid],
}
)
action.execute(context={'entry_log': self.entry_log})

View File

@@ -1,189 +1,203 @@
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_edit, permission_acl_view
from .mixins import ACLTestMixin
from ..permissions import permission_acl_view
class ACLAPITestCase(ACLTestMixin, BaseAPITestCase):
class ACLAPITestCase(DocumentTestMixin, BaseAPITestCase):
def setUp(self):
super(ACLAPITestCase, self).setUp()
self._setup_test_object()
self._create_test_acl()
self.test_acl.permissions.add(self.test_permission.stored_permission)
self.login_admin_user()
def _request_object_acl_list_api_view(self):
return self.get(
viewname='rest_api:object-acl-list',
kwargs=self.test_content_object_view_kwargs
self.document_content_type = ContentType.objects.get_for_model(
self.document
)
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)
def _create_acl(self):
self.acl = AccessControlList.objects.create(
content_object=self.document,
role=self.role
)
def test_object_acl_list_api_view_with_access(self):
self.grant_access(obj=self.test_object, permission=permission_acl_view)
self.acl.permissions.add(permission_document_view.stored_permission)
def test_object_acl_list_view(self):
self._create_acl()
response = self.get(
viewname='rest_api:accesscontrollist-list',
args=(
self.document_content_type.app_label,
self.document_content_type.model,
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.test_object_content_type.app_label
self.document_content_type.app_label
)
self.assertEqual(
response.data['results'][0]['role']['label'],
self.test_acl.role.label
response.data['results'][0]['role']['label'], TEST_ROLE_LABEL
)
def _request_acl_delete_api_view(self):
kwargs = self.test_content_object_view_kwargs.copy()
kwargs['acl_id'] = self.test_acl.pk
def test_object_acl_delete_view(self):
self._create_acl()
return self.delete(
viewname='rest_api:object-acl-detail',
kwargs=kwargs
response = self.delete(
viewname='rest_api:accesscontrollist-detail',
args=(
self.document_content_type.app_label,
self.document_content_type.model,
self.document.pk, self.acl.pk
)
)
def test_object_acl_delete_api_view_with_access(self):
self.expected_content_type = None
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
response = self._request_acl_delete_api_view()
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertTrue(self.test_acl not in AccessControlList.objects.all())
self.assertEqual(AccessControlList.objects.count(), 0)
def test_object_acl_delete_api_view_no_permission(self):
response = self._request_acl_delete_api_view()
def test_object_acl_detail_view(self):
self._create_acl()
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
response = self.get(
viewname='rest_api:accesscontrollist-detail',
args=(
self.document_content_type.app_label,
self.document_content_type.model,
self.document.pk, self.acl.pk
)
)
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.test_object_content_type.app_label
self.document_content_type.app_label
)
self.assertEqual(
response.data['role']['label'], self.test_acl.role.label
response.data['role']['label'], TEST_ROLE_LABEL
)
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)
def test_object_acl_permission_delete_view(self):
self._create_acl()
permission = self.acl.permissions.first()
def _request_object_acl_permission_list_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-permission-detail',
args=(
self.document_content_type.app_label,
self.document_content_type.model,
self.document.pk, self.acl.pk,
permission.pk
)
)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertEqual(self.acl.permissions.count(), 0)
return self.get(
viewname='rest_api:object-acl-permission-list',
kwargs=kwargs
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',
args=(
self.document_content_type.app_label,
self.document_content_type.model,
self.document.pk, self.acl.pk,
permission.pk
)
)
def test_object_acl_permission_list_api_view_with_access(self):
self.grant_access(obj=self.test_object, permission=permission_acl_view)
self.assertEqual(
response.data['pk'], permission_document_view.pk
)
def test_object_acl_permission_list_view(self):
self._create_acl()
response = self.get(
viewname='rest_api:accesscontrollist-permission-list',
args=(
self.document_content_type.app_label,
self.document_content_type.model,
self.document.pk, self.acl.pk
)
)
response = self._request_object_acl_permission_list_api_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(
response.data['results'][0]['pk'],
self.test_permission.pk
permission_document_view.pk
)
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)
def test_object_acl_permission_list_post_view(self):
self._create_acl()
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}
response = self.post(
viewname='rest_api:accesscontrollist-permission-list',
args=(
self.document_content_type.app_label,
self.document_content_type.model,
self.document.pk, self.acl.pk
), data={'permission_pk': permission_acl_view.pk}
)
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}
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_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_post_no_permissions_added_view(self):
response = self.post(
viewname='rest_api:accesscontrollist-list',
args=(
self.document_content_type.app_label,
self.document_content_type.model,
self.document.pk
), data={'role_pk': self.role.pk}
)
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.status_code, status.HTTP_201_CREATED)
self.assertEqual(
response.data['results'][0]['pk'],
self.test_permission.pk
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_inherited_permission_list_api_view_no_permission(self):
self.test_acl.permissions.clear()
self.test_role.grant(permission=self.test_permission)
def test_object_acl_post_with_permissions_added_view(self):
response = self.post(
viewname='rest_api:accesscontrollist-list',
args=(
self.document_content_type.app_label,
self.document_content_type.model,
self.document.pk
), data={
'role_pk': self.role.pk,
'permissions_pk_list': permission_acl_view.pk
response = self._request_object_acl_inherited_permission_list_api_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
}
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(
self.document.acls.first().content_object, self.document
)
self.assertEqual(
self.document.acls.first().role, self.role
)
self.assertEqual(
self.document.acls.first().permissions.first(),
permission_acl_view.stored_permission
)

View File

@@ -1,84 +1,100 @@
from __future__ import unicode_literals
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from mayan.apps.common.tests import GenericViewTestCase
from mayan.apps.documents.tests import GenericDocumentViewTestCase
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
)
class AccessControlListLinksTestCase(ACLTestMixin, GenericViewTestCase):
auto_create_test_role = False
acl.permissions.add(permission_acl_edit.stored_permission)
self.login_user()
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)
self.add_test_view(test_object=self.document)
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=self.test_content_object_view_kwargs
)
resolved_link.url, reverse('acls:acl_create', kwargs=kwargs)
)
def test_object_acl_delete_link(self):
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
def test_document_acl_delete_link(self):
acl = AccessControlList.objects.create(
content_object=self.document, role=self.role
)
self.add_test_view(test_object=self._test_case_acl)
acl.permissions.add(permission_acl_edit.stored_permission)
self.login_user()
self.add_test_view(test_object=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_id': self._test_case_acl.pk}
)
resolved_link.url, reverse('acls:acl_delete', args=(acl.pk,))
)
def test_object_acl_edit_link(self):
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
def test_document_acl_edit_link(self):
acl = AccessControlList.objects.create(
content_object=self.document, role=self.role
)
self.add_test_view(test_object=self._test_case_acl)
acl.permissions.add(permission_acl_edit.stored_permission)
self.login_user()
self.add_test_view(test_object=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_id': self._test_case_acl.pk}
)
resolved_link.url, reverse('acls:acl_permissions', args=(acl.pk,))
)
def test_object_acl_list_link(self):
self.grant_access(obj=self.test_object, permission=permission_acl_view)
def test_document_acl_list_link(self):
acl = AccessControlList.objects.create(
content_object=self.document, role=self.role
)
self.add_test_view(test_object=self.test_object)
acl.permissions.add(permission_acl_view.stored_permission)
self.login_user()
self.add_test_view(test_object=self.document)
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=self.test_content_object_view_kwargs
)
resolved_link.url, reverse('acls:acl_list', kwargs=kwargs)
)

View File

@@ -1,401 +1,159 @@
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 (
DocumentTestMixin, TEST_DOCUMENT_TYPE_2_LABEL, TEST_DOCUMENT_TYPE_LABEL
TEST_DOCUMENT_TYPE_2_LABEL, TEST_DOCUMENT_TYPE_LABEL,
TEST_SMALL_DOCUMENT_PATH
)
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.test_document_type_1 = DocumentType.objects.create(
self.document_type_1 = DocumentType.objects.create(
label=TEST_DOCUMENT_TYPE_LABEL
)
self.test_document_type_2 = DocumentType.objects.create(
self.document_type_2 = DocumentType.objects.create(
label=TEST_DOCUMENT_TYPE_2_LABEL
)
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
)
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()
def test_check_access_without_permissions(self):
with self.assertRaises(PermissionDenied):
AccessControlList.objects.check_access(
obj=self.test_document_1, permission=permission_document_view,
user=self._test_case_user
permissions=(permission_document_view,),
user=self.user, obj=self.document_1
)
def test_filtering_without_permissions(self):
self.assertEqual(
AccessControlList.objects.restrict_queryset(
permission=permission_document_view,
queryset=Document.objects.all(), user=self._test_case_user,
).count(), 0
self.assertQuerysetEqual(
AccessControlList.objects.filter_by_access(
permission=permission_document_view, user=self.user,
queryset=Document.objects.all()
), []
)
def test_check_access_with_acl(self):
acl = AccessControlList.objects.create(
content_object=self.test_document_1, role=self._test_case_role
content_object=self.document_1, role=self.role
)
acl.permissions.add(permission_document_view.stored_permission)
try:
AccessControlList.objects.check_access(
obj=self.test_document_1, permission=permission_document_view,
user=self._test_case_user
permissions=(permission_document_view,), user=self.user,
obj=self.document_1
)
except PermissionDenied:
self.fail('PermissionDenied exception was not expected.')
def test_filtering_with_permissions(self):
acl = AccessControlList.objects.create(
content_object=self.test_document_1, role=self._test_case_role
content_object=self.document_1, role=self.role
)
acl.permissions.add(permission_document_view.stored_permission)
self.assertQuerysetEqual(
AccessControlList.objects.restrict_queryset(
permission=permission_document_view,
queryset=Document.objects.all(), user=self._test_case_user
), (repr(self.test_document_1),)
AccessControlList.objects.filter_by_access(
permission=permission_document_view, user=self.user,
queryset=Document.objects.all()
), (repr(self.document_1),)
)
def test_check_access_with_inherited_acl(self):
acl = AccessControlList.objects.create(
content_object=self.test_document_type_1, role=self._test_case_role
content_object=self.document_type_1, role=self.role
)
acl.permissions.add(permission_document_view.stored_permission)
try:
AccessControlList.objects.check_access(
obj=self.test_document_1, permission=permission_document_view,
user=self._test_case_user
permissions=(permission_document_view,), user=self.user,
obj=self.document_1
)
except PermissionDenied:
self.fail('PermissionDenied exception was not expected.')
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
def test_check_access_with_inherited_acl_and_local_acl(self):
acl = AccessControlList.objects.create(
content_object=self.document_type_1, role=self.role
)
test_acl_1.permissions.add(permission_document_view.stored_permission)
acl.permissions.add(permission_document_view.stored_permission)
test_acl_2 = AccessControlList.objects.create(
content_object=self.test_document_3, role=self._test_case_role
acl = AccessControlList.objects.create(
content_object=self.document_3, role=self.role
)
test_acl_2.permissions.add(permission_document_view.stored_permission)
acl.permissions.add(permission_document_view.stored_permission)
try:
AccessControlList.objects.check_access(
obj=self.test_document_3, permission=permission_document_view,
user=self._test_case_user
permissions=(permission_document_view,), user=self.user,
obj=self.document_3
)
except PermissionDenied:
self.fail('PermissionDenied exception was not expected.')
def test_filtering_with_inherited_permissions(self):
acl = AccessControlList.objects.create(
content_object=self.test_document_type_1, role=self._test_case_role
content_object=self.document_type_1, role=self.role
)
acl.permissions.add(permission_document_view.stored_permission)
result = AccessControlList.objects.restrict_queryset(
permission=permission_document_view, queryset=Document.objects.all(),
user=self._test_case_user
result = AccessControlList.objects.filter_by_access(
permission=permission_document_view, user=self.user,
queryset=Document.objects.all()
)
# Since document_1 and document_2 are of document_type_1
# they are the only ones that should be returned
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)
self.assertTrue(self.document_1 in result)
self.assertTrue(self.document_2 in result)
self.assertTrue(self.document_3 not in result)
def test_filtering_with_inherited_permissions_and_local_acl(self):
self._test_case_role.permissions.add(
permission_document_view.stored_permission
)
self.role.permissions.add(permission_document_view.stored_permission)
acl = AccessControlList.objects.create(
content_object=self.test_document_type_1, role=self._test_case_role
content_object=self.document_type_1, role=self.role
)
acl.permissions.add(permission_document_view.stored_permission)
acl = AccessControlList.objects.create(
content_object=self.test_document_3, role=self._test_case_role
content_object=self.document_3, role=self.role
)
acl.permissions.add(permission_document_view.stored_permission)
result = AccessControlList.objects.restrict_queryset(
permission=permission_document_view, queryset=Document.objects.all(),
user=self._test_case_user,
result = AccessControlList.objects.filter_by_access(
permission=permission_document_view, user=self.user,
queryset=Document.objects.all()
)
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)
self.assertTrue(self.document_1 in result)
self.assertTrue(self.document_2 in result)
self.assertTrue(self.document_3 in result)

View File

@@ -1,239 +1,191 @@
from __future__ import absolute_import, unicode_literals
from django.utils.encoding import force_text
from django.contrib.contenttypes.models import ContentType
from mayan.apps.common.tests import GenericViewTestCase
from mayan.apps.documents.tests import GenericDocumentViewTestCase
from ..classes import ModelPermission
from ..models import AccessControlList
from ..permissions import permission_acl_edit, permission_acl_view
from .mixins import ACLTestMixin
class AccessControlListViewTestCase(ACLTestMixin, GenericViewTestCase):
class AccessControlListViewTestCase(GenericDocumentViewTestCase):
def setUp(self):
super(AccessControlListViewTestCase, self).setUp()
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.document)
self._create_test_permission()
ModelPermission.register(
model=self.test_object._meta.model, permissions=(
self.test_permission,
)
)
self.view_arguments = {
'app_label': content_type.app_label,
'model': content_type.model,
'object_id': self.document.pk
}
self._inject_test_object_content_type()
def test_acl_create_view_no_permission(self):
self.login_user()
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.test_content_object_view_kwargs, data={
'role': self.test_role.pk
response = self.get(
viewname='acls:acl_create', kwargs=self.view_arguments, data={
'role': self.role.pk
}
)
def test_acl_create_get_view_no_permission(self):
self.test_acl.delete()
self.assertEquals(response.status_code, 403)
self.assertEqual(AccessControlList.objects.count(), 0)
response = self._request_acl_create_get_view()
self.assertEqual(response.status_code, 404)
def test_acl_create_view_with_permission(self):
self.login_user()
self.assertFalse(self.test_object.acls.filter(role=self.test_role).exists())
self.role.permissions.add(
permission_acl_edit.stored_permission
)
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.get(
viewname='acls:acl_create', kwargs=self.view_arguments, data={
'role': self.role.pk
}, follow=True
)
response = self._request_acl_create_get_view()
self.assertContains(
response=response, text=force_text(self.test_object),
status_code=200
)
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.test_content_object_view_kwargs, data={
'role': self.test_role.pk
}
response, text=self.document.label, status_code=200
)
def test_acl_create_view_post_no_permission(self):
self.test_acl.delete()
self.login_user()
response = self._request_acl_create_post_view()
self.assertEqual(response.status_code, 404)
response = self.post(
viewname='acls:acl_create', kwargs=self.view_arguments, data={
'role': self.role.pk
}
)
self.assertFalse(self.test_object.acls.filter(role=self.test_role).exists())
self.assertEquals(response.status_code, 403)
self.assertEqual(AccessControlList.objects.count(), 0)
def test_acl_create_view_post_with_access(self):
self.test_acl.delete()
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
def test_acl_create_view_with_post_permission(self):
self.login_user()
response = self._request_acl_create_post_view()
self.assertEqual(response.status_code, 302)
self.role.permissions.add(
permission_acl_edit.stored_permission
)
self.assertTrue(self.test_object.acls.filter(role=self.test_role).exists())
response = self.post(
viewname='acls:acl_create', kwargs=self.view_arguments, data={
'role': self.role.pk
}, follow=True
)
def test_acl_create_duplicate_view_with_access(self):
self.assertContains(response, text='created', status_code=200)
self.assertEqual(AccessControlList.objects.count(), 1)
def test_acl_create_duplicate_view_with_permission(self):
"""
Test creating a duplicate ACL entry: same object & role
Result: Should redirect to existing ACL for object + role combination
"""
self.grant_access(obj=self.test_object, permission=permission_acl_edit)
response = self._request_acl_create_post_view()
self.assertNotContains(
response=response, text=force_text(self.test_acl.role),
status_code=200
acl = AccessControlList.objects.create(
content_object=self.document, role=self.role
)
# 2 ACLs: 1 created by the test and the other by the self.grant_access
self.assertEqual(AccessControlList.objects.count(), 2)
self.login_user()
# 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']
self.role.permissions.add(
permission_acl_edit.stored_permission
)
self.assertQuerysetEqual(
qs=AccessControlList.objects.order_by('role__id').values(
'object_id', 'role',
), transform=dict, values=expected_results
response = self.post(
viewname='acls:acl_create', kwargs=self.view_arguments, data={
'role': self.role.pk
}, follow=True
)
def _request_acl_delete_view(self):
return self.post(
viewname='acls:acl_delete', kwargs={'acl_id': self.test_acl.pk}
self.assertContains(
response, text='vailable permissions', status_code=200
)
self.assertEqual(AccessControlList.objects.count(), 1)
self.assertEqual(AccessControlList.objects.first().pk, acl.pk)
def test_orphan_acl_create_view_with_permission(self):
"""
Test creating an ACL entry for an object with no model permissions.
Result: Should display a blank permissions list (not optgroup)
"""
self.login_user()
self.role.permissions.add(
permission_acl_edit.stored_permission
)
def test_acl_delete_view_no_permission(self):
response = self._request_acl_delete_view()
self.assertNotContains(
response=response, text=force_text(self.test_object),
status_code=404
recent_entry = self.document.add_as_recent_document_for_user(self.user)
content_type = ContentType.objects.get_for_model(recent_entry)
view_arguments = {
'app_label': content_type.app_label,
'model': content_type.model,
'object_id': recent_entry.pk
}
response = self.post(
viewname='acls:acl_create', kwargs=view_arguments, data={
'role': self.role.pk
}, follow=True
)
self.assertTrue(self.test_object.acls.filter(role=self.test_role).exists())
def test_acl_delete_view_with_access(self):
self.grant_access(
obj=self.test_object, permission=permission_acl_edit
)
response = self._request_acl_delete_view()
self.assertEqual(response.status_code, 302)
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.test_content_object_view_kwargs
)
self.assertNotContains(response, text='optgroup', status_code=200)
self.assertEqual(AccessControlList.objects.count(), 1)
def test_acl_list_view_no_permission(self):
response = self._request_acl_list_view()
self.login_user()
self.assertNotContains(
response=response, text=force_text(self.test_object),
status_code=404
document = self.document.add_as_recent_document_for_user(
self.user
).document
acl = AccessControlList.objects.create(
content_object=document, role=self.role
)
acl.permissions.add(permission_acl_edit.stored_permission)
content_type = ContentType.objects.get_for_model(document)
view_arguments = {
'app_label': content_type.app_label,
'model': content_type.model,
'object_id': document.pk
}
response = self.get(
viewname='acls:acl_list', kwargs=view_arguments
)
def test_acl_list_view_with_access(self):
self.grant_access(obj=self.test_object, permission=permission_acl_view)
self.assertNotContains(response, text=document.label, status_code=403)
self.assertNotContains(response, text='otal: 1', status_code=403)
response = self._request_acl_list_view()
def test_acl_list_view_with_permission(self):
self.login_user()
self.assertContains(
response=response, text=force_text(self.test_object),
status_code=200
self.role.permissions.add(
permission_acl_view.stored_permission
)
def _request_get_acl_permissions_get_view(self):
return self.get(
viewname='acls:acl_permissions',
kwargs={'acl_id': self.test_acl.pk}
document = self.document.add_as_recent_document_for_user(
self.user
).document
acl = AccessControlList.objects.create(
content_object=document, role=self.role
)
acl.permissions.add(permission_acl_view.stored_permission)
def test_acl_permissions_get_view_no_permission(self):
self.test_acl.permissions.clear()
content_type = ContentType.objects.get_for_model(document)
response = self._request_get_acl_permissions_get_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_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_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_arguments = {
'app_label': content_type.app_label,
'model': content_type.model,
'object_id': document.pk
}
response = self.get(
viewname='acls:acl_list', kwargs=view_arguments
)
self.assertContains(response, text=document.label, status_code=200)

View File

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

View File

@@ -1,23 +1,24 @@
from __future__ import absolute_import, unicode_literals
import itertools
import logging
from django.contrib.contenttypes.models import ContentType
from django.http import Http404, HttpResponseRedirect
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, ExternalObjectMixin
)
from mayan.apps.common.generics import (
AddRemoveView, SingleObjectCreateView, SingleObjectDeleteView,
from mayan.apps.common.views import (
AssignRemoveView, SingleObjectCreateView, SingleObjectDeleteView,
SingleObjectListView
)
from mayan.apps.permissions.models import Role
from mayan.apps.permissions import Permission, PermissionNamespace
from mayan.apps.permissions.models import StoredPermission
from .classes import ModelPermission
from .forms import ACLCreateForm
from .icons import icon_acl_list
from .links import link_acl_create
from .models import AccessControlList
@@ -26,95 +27,113 @@ from .permissions import permission_acl_edit, permission_acl_view
logger = logging.getLogger(__name__)
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
class ACLCreateView(SingleObjectCreateView):
fields = ('role',)
model = AccessControlList
def get_error_message_duplicate(self):
return _(
'An ACL for "%(object)s" using role "%(role)s" already exists. '
'Edit that ACL entry instead.'
) % {'object': self.get_external_object(), 'role': self.object.role}
def dispatch(self, request, *args, **kwargs):
self.object_content_type = get_object_or_404(
klass=ContentType, app_label=self.kwargs['app_label'],
model=self.kwargs['model']
)
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().get_all_objects_for_this_type()
try:
self.content_object = self.object_content_type.get_object_for_this_type(
pk=self.kwargs['object_id']
)
except self.object_content_type.model_class().DoesNotExist:
raise Http404
def get_extra_context(self):
return {
'object': self.get_external_object(),
'title': _(
'New access control lists for: %s'
) % self.get_external_object()
}
AccessControlList.objects.check_access(
permissions=permission_acl_edit, user=request.user,
obj=self.content_object
)
def get_form_extra_kwargs(self):
return {
'field_name': 'role',
'label': _('Role'),
'queryset': Role.objects.exclude(
pk__in=self.get_external_object().acls.values('role')
),
'widget_attributes': {'class': 'select2'},
'user': self.request.user
}
return super(ACLCreateView, self).dispatch(request, *args, **kwargs)
def get_instance_extra_data(self):
return {
'content_object': self.get_external_object()
'content_object': self.content_object
}
def get_queryset(self):
self.get_external_object().acls.all()
def form_valid(self, form):
try:
acl = AccessControlList.objects.get(
content_type=self.object_content_type,
object_id=self.content_object.pk,
role=form.cleaned_data['role']
)
except AccessControlList.DoesNotExist:
return super(ACLCreateView, self).form_valid(form)
else:
return HttpResponseRedirect(
reverse('acls:acl_permissions', args=(acl.pk,))
)
def get_extra_context(self):
return {
'object': self.content_object,
'title': _(
'New access control lists for: %s'
) % self.content_object
}
def get_success_url(self):
return self.object.get_absolute_url()
if self.object.pk:
return reverse('acls:acl_permissions', args=(self.object.pk,))
else:
return super(ACLCreateView, self).get_success_url()
class ACLDeleteView(SingleObjectDeleteView):
model = AccessControlList
object_permission = permission_acl_edit
pk_url_kwarg = 'acl_id'
def dispatch(self, request, *args, **kwargs):
acl = get_object_or_404(klass=AccessControlList, pk=self.kwargs['pk'])
AccessControlList.objects.check_access(
permissions=permission_acl_edit, user=request.user,
obj=acl.content_object
)
return super(ACLDeleteView, self).dispatch(request, *args, **kwargs)
def get_extra_context(self):
acl = self.get_object()
return {
'acl': acl,
'object': acl.content_object,
'navigation_object_list': ('object', 'acl'),
'object': self.get_object().content_object,
'title': _('Delete ACL: %s') % self.get_object(),
}
def get_post_action_redirect(self):
instance = self.get_object()
return reverse(
'acls:acl_list', kwargs={
'app_label': instance.content_type.app_label,
'model_name': instance.content_type.model,
'object_id': instance.object_id
}
'acls:acl_list', args=(
instance.content_type.app_label,
instance.content_type.model, instance.object_id
)
)
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'
class ACLListView(SingleObjectListView):
def dispatch(self, request, *args, **kwargs):
self.object_content_type = get_object_or_404(
klass=ContentType, app_label=self.kwargs['app_label'],
model=self.kwargs['model']
)
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().get_all_objects_for_this_type()
try:
self.content_object = self.object_content_type.get_object_for_this_type(
pk=self.kwargs['object_id']
)
except self.object_content_type.model_class().DoesNotExist:
raise Http404
AccessControlList.objects.check_access(
permissions=permission_acl_view, user=request.user,
obj=self.content_object
)
return super(ACLListView, self).dispatch(request, *args, **kwargs)
def get_extra_context(self):
return {
@@ -122,9 +141,7 @@ class ACLListView(ContentTypeViewMixin, ExternalObjectMixin, SingleObjectListVie
'no_results_icon': icon_acl_list,
'no_results_main_link': link_acl_create.resolve(
context=RequestContext(
self.request, {
'resolved_object': self.get_external_object()
}
self.request, {'resolved_object': self.content_object}
)
),
'no_results_title': _(
@@ -132,98 +149,116 @@ class ACLListView(ContentTypeViewMixin, ExternalObjectMixin, SingleObjectListVie
),
'no_results_text': _(
'ACL stands for Access Control List and is a precise method '
' to control user access to objects in the system. ACLs '
'allow granting a permission to a role but only for a '
'specific object or set of objects.'
),
'object': self.get_external_object(),
'title': _(
'Access control lists for: %s' % self.get_external_object()
' to control user access to objects in the system.'
),
'object': self.content_object,
'title': _('Access control lists for: %s' % self.content_object),
}
def get_source_queryset(self):
return self.get_external_object().acls.all()
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'
def generate_choices(self, queryset):
namespaces_dictionary = {}
# Sort permissions by their translatable label
object_list = sorted(
queryset, key=lambda permission: permission.volatile_permission.label
def get_object_list(self):
return AccessControlList.objects.filter(
content_type=self.object_content_type,
object_id=self.content_object.pk
)
# Group permissions by namespace
for permission in object_list:
namespaces_dictionary.setdefault(
permission.volatile_permission.namespace.label,
[]
class ACLPermissionsView(AssignRemoveView):
grouped = True
left_list_title = _('Available permissions')
right_list_title = _('Granted permissions')
@staticmethod
def generate_choices(entries):
results = []
entries = sorted(
entries, key=lambda x: (
x.volatile_permission.namespace.label,
x.volatile_permission.label
)
namespaces_dictionary[permission.volatile_permission.namespace.label].append(
(permission.pk, force_text(permission))
)
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)
)
# Sort permissions by their translatable namespace label
return sorted(namespaces_dictionary.items())
return results
def get_actions_extra_kwargs(self):
return {'_user': self.request.user}
def add(self, item):
permission = get_object_or_404(klass=StoredPermission, pk=item)
self.get_object().permissions.add(permission)
def dispatch(self, request, *args, **kwargs):
acl = get_object_or_404(klass=AccessControlList, pk=self.kwargs['pk'])
AccessControlList.objects.check_access(
permissions=permission_acl_edit, user=request.user,
obj=acl.content_object
)
return super(
ACLPermissionsView, self
).dispatch(request, *args, **kwargs)
def get_available_list(self):
return ModelPermission.get_for_instance(
instance=self.get_object().content_object
).exclude(id__in=self.get_granted_list().values_list('pk', flat=True))
def get_disabled_choices(self):
"""
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.
Get permissions from a parent's acls but remove the permissions we
already hold for this object
"""
return self.main_object.get_inherited_permissions().values_list('pk', flat=True)
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)
)
)
def get_extra_context(self):
return {
'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,
}
'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,
},
}
def get_list_added_help_text(self):
if self.main_object.get_inherited_permissions():
def get_granted_list(self):
"""
Merge or permissions we hold for this object and the permissions we
hold for this object's parent via another ACL
"""
merged_pks = self.get_object().permissions.values_list('pk', flat=True) | self.get_object().get_inherited_permissions().values_list('pk', flat=True)
return StoredPermission.objects.filter(pk__in=merged_pks)
def get_object(self):
return get_object_or_404(klass=AccessControlList, pk=self.kwargs['pk'])
def get_right_list_help_text(self):
if self.get_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.'
'Disabled permissions are inherited from a parent object.'
)
def get_list_added_queryset(self):
"""
Merge of permissions we hold for this object and the permissions we
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.
"""
queryset_acl = super(ACLPermissionsView, self).get_list_added_queryset()
return None
return (
queryset_acl | self.main_object.get_inherited_permissions()
).distinct()
def left_list(self):
Permission.refresh()
return ACLPermissionsView.generate_choices(self.get_available_list())
def get_secondary_object_source_queryset(self):
return ModelPermission.get_for_instance(
instance=self.main_object.content_object
)
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,8 +89,7 @@ class GrantAccessAction(WorkflowAction):
try:
AccessControlList.objects.check_access(
obj=obj, permissions=permission_acl_edit,
user=request.user
permissions=permission_acl_edit, user=request.user, obj=obj
)
except Exception as exception:
raise ValidationError(exception)
@@ -99,9 +98,7 @@ class GrantAccessAction(WorkflowAction):
def get_form_schema(self, *args, **kwargs):
self.fields['content_type']['kwargs']['queryset'] = ModelPermission.get_classes(as_content_type=True)
self.fields['permissions']['kwargs']['choices'] = Permission.all(
as_choices=True
)
self.fields['permissions']['kwargs']['choices'] = Permission.all(as_choices=True)
return super(GrantAccessAction, self).get_form_schema(*args, **kwargs)
def get_execute_data(self):

View File

@@ -23,7 +23,7 @@ class FontAwesomeDriver(IconDriver):
self.symbol = symbol
def render(self):
return get_template(template_name=self.template_name).render(
return get_template(self.template_name).render(
context={'symbol': self.symbol}
)
@@ -37,7 +37,7 @@ class FontAwesomeDualDriver(IconDriver):
self.secondary_symbol = secondary_symbol
def render(self):
return get_template(template_name=self.template_name).render(
return get_template(self.template_name).render(
context={
'data': (
{
@@ -55,6 +55,7 @@ class FontAwesomeDualDriver(IconDriver):
)
class FontAwesomeCSSDriver(IconDriver):
name = 'fontawesomecss'
template_name = 'appearance/icons/font_awesome_css.html'
@@ -63,7 +64,7 @@ class FontAwesomeCSSDriver(IconDriver):
self.css_classes = css_classes
def render(self):
return get_template(template_name=self.template_name).render(
return get_template(self.template_name).render(
context={'css_classes': self.css_classes}
)
@@ -76,7 +77,7 @@ class FontAwesomeMasksDriver(IconDriver):
self.data = data
def render(self):
return get_template(template_name=self.template_name).render(
return get_template(self.template_name).render(
context={'data': self.data}
)
@@ -89,7 +90,7 @@ class FontAwesomeLayersDriver(IconDriver):
self.data = data
def render(self):
return get_template(template_name=self.template_name).render(
return get_template(self.template_name).render(
context={'data': self.data}
)

View File

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

View File

@@ -6,7 +6,7 @@ from mayan.apps.smart_settings import Namespace
from .literals import DEFAULT_MAXIMUM_TITLE_LENGTH
namespace = Namespace(label=_('Appearance'), name='appearance')
namespace = Namespace(name='appearance', label=_('Appearance'))
setting_max_title_length = namespace.add_setting(
default=DEFAULT_MAXIMUM_TITLE_LENGTH,

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,6 +29,29 @@ 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();
@@ -58,6 +81,22 @@ 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();
@@ -71,6 +110,35 @@ 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);
@@ -171,6 +239,7 @@ class MayanApp {
initialize () {
var self = this;
this.setupAJAXPeriodicWorkers();
this.setupAJAXSpinner();
this.setupFormHotkeys();
this.setupFullHeightResizing();
@@ -187,6 +256,22 @@ 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;
@@ -360,6 +445,12 @@ 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>
{% navigation_resolve_menus names='facet,list facet' sort_results=True as facet_menus_link_results %}
{% get_menus_links names='facet,list facet' sort_results=True as links_facet %}
<style>
@@ -45,12 +45,12 @@
<div class="row">
<div class="col-xs-12 {% if facet_menus_link_results %}has-sidebar{% endif %}" id="viewport">
<div class="col-xs-12 {% if links_facet %}has-sidebar{% endif %}" id="viewport">
{% include 'appearance/calculate_form_title.html' %}
{# action menu #}
{% navigation_resolve_menus names='object,secondary' sort_results=True as action_menus_link_results %}
{% if action_menus_link_results %}
{% get_menus_links names='object,sidebar,secondary' sort_results=True as links_actions %}
{% if links_actions %}
<div class="pull-right btn-group" id="menu-actions">
<button aria-expanded="true" class="btn btn-danger btn-sm dropdown-toggle" data-toggle="dropdown" type="button">
{% trans 'Actions' %}
@@ -58,39 +58,19 @@
<span class="sr-only">{% trans 'Toggle Dropdown' %}</span>
</button>
<ul class="dropdown-menu" role="menu">
{% 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 %}
{% 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 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 %}
{% if not forloop.last and object_navigation_links %}
<li class="divider"></li>
{% endif %}
{% endfor %}
</ul>
</div>
<div class="clearfix"></div>
@@ -100,21 +80,17 @@
{% block footer %}{% endblock %}
</div>
{% if facet_menus_link_results %}
{% if links_facet %}
<div id="sidebar">
<div class="pull-right list-group">
{% 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 %}
{% 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 %}
{% endfor %}
</div>
</div>

View File

@@ -90,17 +90,6 @@
<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,10 +11,8 @@
<div class="well center-block">
<div class="row">
{% with 'navigation/large_button_link.html' as link_template %}
{% for menu_results in resolved_links %}
{% with menu_results.links as object_navigation_links %}
{% include 'navigation/generic_navigation.html' %}
{% endwith %}
{% for object_navigation_links in resolved_links %}
{% include 'navigation/generic_navigation.html' %}
{% 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 %}
{% navigation_resolve_menu name='multi item' sort_results=True source=object_list.0 as links_multi_menus_results %}
{% get_menu_links name='multi item' sort_results=True source=object_list.0 as links_multi_item %}
{% endif %}
{% endif %}
@@ -45,7 +45,7 @@
<div class="form-group">
<div class="checkbox">
<label for="id_indexes_0">
{% if links_multi_menus_results %}
{% if links_multi_item %}
<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 %}
{% navigation_resolve_menus names='list facet,object' source=object as facet_menus_link_results %}
{% get_menus_links names='list facet,object' source=object as links %}
{% if facet_menus_link_results %}
{% if links %}
<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,25 +94,18 @@
<span class="sr-only">{% trans 'Toggle Dropdown' %}</span>
</button>
<ul class="dropdown-menu" role="menu">
{% for facet_menu_link_result in facet_menus_link_results %}
{% for link_group in facet_menu_link_result.link_groups %}
{% 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 %}
{% 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 %}
{% if not forloop.last and object_navigation_links %}
<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 %}
{% navigation_resolve_menu name='multi item' sort_results=True source=object_list.0 as links_multi_menus_results %}
{% get_menu_links name='multi item' sort_results=True source=object_list.0 as links_multi_item %}
{% endif %}
{% endif %}
@@ -42,36 +42,21 @@
<thead>
{% if not hide_header %}
<tr>
{% if links_multi_menus_results %}
{% if links_multi_item %}
<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 exclude_identifier=True as source_columns %}
{% get_source_columns source=object_list 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.get_sort_field == sort_field %}
{% if column.attribute == sort_field %}
{% if icon_sort %}{{ icon_sort.render }}{% endif %}
{% endif %}
</a>
@@ -95,7 +80,7 @@
<tbody>
{% for object in object_list %}
<tr>
{% if links_multi_menus_results %}
{% if links_multi_item %}
<td>
<input class="form-multi-object-action-checkbox check-all-slave checkbox" name="pk_{{ object.pk }}" type="checkbox" value="" />
</td>
@@ -115,7 +100,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>
@@ -131,29 +116,21 @@
{% endfor %}
{% if not hide_links %}
<td class="last">
{% 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 %}
{% 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 %}
{% endfor %}
{% 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 %}
{% 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 %}
{% 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_menus_results %}
{% if links_multi_item %}
<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_menus_results %}
{% if links_multi_item %}
<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,20 +29,16 @@
<span class="sr-only">{% trans 'Toggle Dropdown' %}</span>
</button>
<ul class="dropdown-menu" role="menu">
{% 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 %}
{% 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 %}
{% endfor %}
{% if not forloop.last and link_group %}
{% if not forloop.last and object_navigation_links %}
<li class="divider"></li>
{% endif %}
{% endfor %}

View File

@@ -8,62 +8,58 @@
{% spaceless %}
<div class="panel-group" id="accordion-sidebar" role="tablist" aria-multiselectable="true">
{% 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>
{% 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>
</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>
</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>
</div>
{% endif %}
{% endwith %}
{% endwith %}
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% endwith %}
{% endfor %}
{% endfor %}
</div>

View File

@@ -18,22 +18,20 @@
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
{% 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 %}
{% 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 %}
{% endfor %}
{% endfor %}
</ul>

View File

@@ -12,11 +12,7 @@ def get_choice_value(field):
try:
return dict(field.field.choices)[field.value()]
except TypeError:
return ', '.join(
[
subwidget.data['label'] for subwidget in field.subwidgets if subwidget.data['selected']
]
)
return ', '.join([subwidget.data['label'] for subwidget in field.subwidgets if subwidget.data['selected']])
except KeyError:
return _('None')
@@ -28,4 +24,4 @@ def get_form_media_js(form):
@register.simple_tag
def get_icon(icon_path):
return import_string(dotted_path=icon_path).render()
return import_string(icon_path).render()

View File

@@ -23,10 +23,8 @@ 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.'),
}
@@ -58,10 +56,8 @@ 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

@@ -6,7 +6,7 @@ from mayan.apps.smart_settings import Namespace
from .literals import DEFAULT_LOGIN_METHOD, DEFAULT_MAXIMUM_SESSION_LENGTH
namespace = Namespace(label=_('Authentication'), name='authentication')
namespace = Namespace(name='authentication', label=_('Authentication'))
setting_login_method = namespace.add_setting(
global_name='AUTHENTICATION_LOGIN_METHOD', default=DEFAULT_LOGIN_METHOD,
help_text=_(

View File

@@ -14,7 +14,8 @@
<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:login_view' %}">{% trans 'Login page' %}</a></div>
<div class="text-center"><a class="btn btn-primary" href="{% url 'authentication:logout_view' %}">{% trans 'Login page' %}</a></div>
</div>
</div>
{% endblock content_plain %}

View File

@@ -8,7 +8,8 @@ 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_CASE_USER_EMAIL, TEST_CASE_USER_PASSWORD, TEST_CASE_USER_USERNAME,
TEST_ADMIN_EMAIL, TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME,
TEST_USER_PASSWORD_EDITED
)
from ..settings import setting_maximum_session_length
@@ -18,101 +19,97 @@ from .literals import TEST_EMAIL_AUTHENTICATION_BACKEND
class UserLoginTestCase(GenericViewTestCase):
"""
Test that users can login using the supported authentication methods
Test that users can login via 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()
Namespace.invalidate_cache_all()
def _request_authenticated_view(self):
return self.get(viewname='documents:document_list')
@override_settings(AUTHENTICATION_LOGIN_METHOD='username')
def test_normal_behavior(self):
response = self._request_authenticated_view()
response = self.client.get(reverse('documents:document_list'))
self.assertRedirects(
response=response, expected_url=self.authenticated_url
response,
'http://testserver/authentication/login/?next=/documents/list/'
)
@override_settings(AUTHENTICATION_LOGIN_METHOD='username')
def test_username_login(self):
logged_in = self.login(
username=TEST_CASE_USER_USERNAME, password=TEST_CASE_USER_PASSWORD
logged_in = self.client.login(
username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD
)
self.assertTrue(logged_in)
response = self._request_authenticated_view()
response = self.client.get(reverse('documents:document_list'))
# We didn't get redirected to the login URL
self.assertEqual(response.status_code, 200)
@override_settings(AUTHENTICATION_LOGIN_METHOD='email')
def test_email_login(self):
with self.settings(AUTHENTICATION_BACKENDS=(TEST_EMAIL_AUTHENTICATION_BACKEND,)):
logged_in = self.login(
username=TEST_CASE_USER_USERNAME, password=TEST_CASE_USER_PASSWORD
logged_in = self.client.login(
username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD
)
self.assertFalse(logged_in)
logged_in = self.login(
email=TEST_CASE_USER_EMAIL, password=TEST_CASE_USER_PASSWORD
logged_in = self.client.login(
email=TEST_ADMIN_EMAIL, password=TEST_ADMIN_PASSWORD
)
self.assertTrue(logged_in)
response = self._request_authenticated_view()
response = self.client.get(reverse('documents:document_list'))
# We didn't get redirected to the login URL
self.assertEqual(response.status_code, 200)
@override_settings(AUTHENTICATION_LOGIN_METHOD='username')
def test_username_login_via_views(self):
response = self._request_authenticated_view()
response = self.client.get(reverse('documents:document_list'))
self.assertRedirects(
response=response, expected_url=self.authenticated_url
response,
'http://testserver/authentication/login/?next=/documents/list/'
)
response = self.post(
viewname=settings.LOGIN_URL, data={
'username': TEST_CASE_USER_USERNAME,
'password': TEST_CASE_USER_PASSWORD
response = self.client.post(
reverse(settings.LOGIN_URL), {
'username': TEST_ADMIN_USERNAME,
'password': TEST_ADMIN_PASSWORD
}
)
response = self._request_authenticated_view()
response = self.client.get(reverse('documents:document_list'))
# We didn't get redirected to the login URL
self.assertEqual(response.status_code, 200)
@override_settings(AUTHENTICATION_LOGIN_METHOD='email')
def test_email_login_via_views(self):
with self.settings(AUTHENTICATION_BACKENDS=(TEST_EMAIL_AUTHENTICATION_BACKEND,)):
response = self._request_authenticated_view()
response = self.client.get(reverse('documents:document_list'))
self.assertRedirects(
response=response, expected_url=self.authenticated_url
response,
'http://testserver/authentication/login/?next=/documents/list/'
)
response = self.post(
viewname=settings.LOGIN_URL, data={
'email': TEST_CASE_USER_EMAIL, 'password': TEST_CASE_USER_PASSWORD
response = self.client.post(
reverse(settings.LOGIN_URL), {
'email': TEST_ADMIN_EMAIL, 'password': TEST_ADMIN_PASSWORD
}, follow=True
)
self.assertEqual(response.status_code, 200)
response = self._request_authenticated_view()
response = self.client.get(reverse('documents:document_list'))
# We didn't get redirected to the login URL
self.assertEqual(response.status_code, 200)
@override_settings(AUTHENTICATION_LOGIN_METHOD='username')
def test_username_remember_me(self):
response = self.post(
viewname=settings.LOGIN_URL, data={
'username': TEST_CASE_USER_USERNAME,
'password': TEST_CASE_USER_PASSWORD,
response = self.client.post(
reverse(settings.LOGIN_URL), {
'username': TEST_ADMIN_USERNAME,
'password': TEST_ADMIN_PASSWORD,
'remember_me': True
}, follow=True
)
response = self._request_authenticated_view()
response = self.client.get(reverse('documents:document_list'))
self.assertEqual(response.status_code, 200)
self.assertEqual(
@@ -123,15 +120,15 @@ class UserLoginTestCase(GenericViewTestCase):
@override_settings(AUTHENTICATION_LOGIN_METHOD='username')
def test_username_dont_remember_me(self):
response = self.post(
viewname=settings.LOGIN_URL, data={
'username': TEST_CASE_USER_USERNAME,
'password': TEST_CASE_USER_PASSWORD,
response = self.client.post(
reverse(settings.LOGIN_URL), {
'username': TEST_ADMIN_USERNAME,
'password': TEST_ADMIN_PASSWORD,
'remember_me': False
}, follow=True
)
response = self._request_authenticated_view()
response = self.client.get(reverse('documents:document_list'))
self.assertEqual(response.status_code, 200)
self.assertTrue(self.client.session.get_expire_at_browser_close())
@@ -139,15 +136,15 @@ class UserLoginTestCase(GenericViewTestCase):
@override_settings(AUTHENTICATION_LOGIN_METHOD='email')
def test_email_remember_me(self):
with self.settings(AUTHENTICATION_BACKENDS=(TEST_EMAIL_AUTHENTICATION_BACKEND,)):
response = self.post(
viewname=settings.LOGIN_URL, data={
'email': TEST_CASE_USER_EMAIL,
'password': TEST_CASE_USER_PASSWORD,
response = self.client.post(
reverse(settings.LOGIN_URL), {
'email': TEST_ADMIN_EMAIL,
'password': TEST_ADMIN_PASSWORD,
'remember_me': True
}, follow=True
)
response = self._request_authenticated_view()
response = self.client.get(reverse('documents:document_list'))
self.assertEqual(response.status_code, 200)
self.assertEqual(
@@ -161,13 +158,13 @@ class UserLoginTestCase(GenericViewTestCase):
with self.settings(AUTHENTICATION_BACKENDS=(TEST_EMAIL_AUTHENTICATION_BACKEND,)):
response = self.post(
viewname=settings.LOGIN_URL, data={
'email': TEST_CASE_USER_EMAIL,
'password': TEST_CASE_USER_PASSWORD,
'email': TEST_ADMIN_EMAIL,
'password': TEST_ADMIN_PASSWORD,
'remember_me': False
}
)
response = self._request_authenticated_view()
response = self.get(viewname='documents:document_list')
self.assertEqual(response.status_code, 200)
self.assertTrue(self.client.session.get_expire_at_browser_close())
@@ -176,7 +173,7 @@ class UserLoginTestCase(GenericViewTestCase):
def test_password_reset(self):
response = self.post(
viewname='authentication:password_reset_view', data={
'email': TEST_CASE_USER_EMAIL,
'email': TEST_ADMIN_EMAIL,
}
)
@@ -188,29 +185,29 @@ class UserLoginTestCase(GenericViewTestCase):
response = self.post(
viewname='authentication:password_reset_confirm_view',
args=uid_token[-3:-1], data={
'new_password1': TEST_CASE_USER_PASSWORD,
'new_password2': TEST_CASE_USER_PASSWORD,
'new_password1': TEST_USER_PASSWORD_EDITED,
'new_password2': TEST_USER_PASSWORD_EDITED,
}
)
self.assertEqual(response.status_code, 302)
self.login(
username=TEST_CASE_USER_USERNAME, password=TEST_CASE_USER_PASSWORD
username=TEST_ADMIN_USERNAME, password=TEST_USER_PASSWORD_EDITED
)
response = self._request_authenticated_view()
response = self.get(viewname='documents:document_list')
self.assertEqual(response.status_code, 200)
def test_username_login_redirect(self):
TEST_REDIRECT_URL = reverse(viewname='common:about_view')
TEST_REDIRECT_URL = reverse('common:about_view')
response = self.post(
path='{}?next={}'.format(
response = self.client.post(
'{}?next={}'.format(
reverse(settings.LOGIN_URL), TEST_REDIRECT_URL
), data={
'username': TEST_CASE_USER_USERNAME,
'password': TEST_CASE_USER_PASSWORD,
), {
'username': TEST_ADMIN_USERNAME,
'password': TEST_ADMIN_PASSWORD,
'remember_me': False
}, follow=True
)

View File

@@ -1,44 +1,42 @@
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 (
MayanLoginView, MayanLogoutView, MayanPasswordChangeDoneView,
MayanPasswordChangeView, MayanPasswordResetCompleteView,
MayanPasswordResetConfirmView, MayanPasswordResetDoneView,
MayanPasswordResetView
login_view, password_change_done, password_change_view,
password_reset_complete_view, password_reset_confirm_view,
password_reset_done_view, password_reset_view
)
urlpatterns = [
url(regex=r'^login/$', name='login_view', view=MayanLoginView.as_view()),
url(r'^login/$', login_view, name='login_view'),
url(
regex=r'^logout/$', name='logout_view', view=MayanLogoutView.as_view()
r'^password/change/done/$', password_change_done,
name='password_change_done'
),
url(
regex=r'^password/change/$', name='password_change_view',
view=MayanPasswordChangeView.as_view()
r'^password/change/$', password_change_view,
name='password_change_view'
),
url(
regex=r'^password/change/done/$', name='password_change_done',
view=MayanPasswordChangeDoneView.as_view()
r'^logout/$', logout, {'next_page': settings.LOGIN_REDIRECT_URL},
name='logout_view'
),
url(
regex=r'^password/reset/$', name='password_reset_view',
view=MayanPasswordResetView.as_view()
r'^password/reset/$', password_reset_view, name='password_reset_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=MayanPasswordResetConfirmView.as_view()
r'^password/reset/confirm/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
password_reset_confirm_view, name='password_reset_confirm_view'
),
url(
regex=r'^password/reset/complete/$',
name='password_reset_complete_view',
view=MayanPasswordResetCompleteView.as_view()
r'^password/reset/complete/$', password_reset_complete_view,
name='password_reset_complete_view'
),
url(
regex=r'^password/reset/done/$', name='password_reset_done_view',
view=MayanPasswordResetDoneView.as_view()
r'^password/reset/done/$', password_reset_done_view,
name='password_reset_done_view'
),
]

View File

@@ -1,17 +1,19 @@
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 (
LoginView, LogoutView, PasswordChangeDoneView, PasswordChangeView,
PasswordResetCompleteView, PasswordResetConfirmView, PasswordResetDoneView,
PasswordResetView
login, password_change, password_reset, password_reset_complete,
password_reset_confirm, password_reset_done
)
from django.http import HttpResponseRedirect
from django.shortcuts import redirect
from django.urls import reverse, reverse_lazy
from django.shortcuts import redirect, resolve_url
from django.urls import reverse
from django.utils.http import is_safe_url
from django.utils.translation import ugettext_lazy as _
from stronghold.views import StrongholdPublicMixin
from stronghold.decorators import public
import mayan
from mayan.apps.common.settings import (
@@ -22,108 +24,143 @@ from .forms import EmailAuthenticationForm, UsernameAuthenticationForm
from .settings import setting_login_method, setting_maximum_session_length
class MayanLoginView(StrongholdPublicMixin, LoginView):
extra_context = {
'appearance_type': 'plain'
}
template_name = 'authentication/login.html'
redirect_authenticated_user = True
@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'}
def form_valid(self, form):
result = super(MayanLoginView, self).form_valid(form=form)
remember_me = form.cleaned_data.get('remember_me')
if setting_login_method.value == 'email':
kwargs['authentication_form'] = EmailAuthenticationForm
else:
kwargs['authentication_form'] = UsernameAuthenticationForm
# remember_me values:
# True - long session
# False - short session
# None - Form has no remember_me value and we let the session
# expiration default.
allowed_hosts = {request.get_host()}
allowed_hosts.update(success_url_allowed_hosts)
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)
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
def get_form_class(self):
if setting_login_method.value == 'email':
return EmailAuthenticationForm
else:
return UsernameAuthenticationForm
else:
return HttpResponseRedirect(resolve_url(settings.LOGIN_REDIRECT_URL))
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 redirect(to='common:current_user_details')
class MayanPasswordChangeView(PasswordChangeView):
def password_change_view(request):
"""
Password change wrapper for better control
"""
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)
if request.user.user_options.block_password_change:
messages.error(
request, _(
'Changing the password is not allowed for this account.'
)
)
return HttpResponseRedirect(reverse(setting_home_view.view))
return super(MayanPasswordChangeView, self).dispatch(*args, **kwargs)
class MayanPasswordResetCompleteView(StrongholdPublicMixin, PasswordResetCompleteView):
extra_context = {
'appearance_type': 'plain'
}
template_name = 'authentication/password_reset_complete.html'
class MayanPasswordResetConfirmView(StrongholdPublicMixin, PasswordResetConfirmView):
extra_context = {
'appearance_type': 'plain'
}
success_url = reverse_lazy(
viewname='authentication:password_reset_complete_view'
return password_change(
request, extra_context=extra_context,
template_name='appearance/generic_form.html',
post_change_redirect=reverse('authentication:password_change_done'),
)
template_name = 'authentication/password_reset_confirm.html'
class MayanPasswordResetDoneView(StrongholdPublicMixin, PasswordResetDoneView):
extra_context = {
'appearance_type': 'plain'
}
template_name = 'authentication/password_reset_done.html'
class MayanPasswordResetView(StrongholdPublicMixin, PasswordResetView):
email_template_name = 'authentication/password_reset_email.html'
extra_context = {
'appearance_type': 'plain'
}
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'
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')
@public
def password_reset_complete_view(request):
extra_context = {
'appearance_type': 'plain'
}
return password_reset_complete(
request, extra_context=extra_context,
template_name='authentication/password_reset_complete.html'
)
@public
def password_reset_confirm_view(request, uidb64=None, token=None):
extra_context = {
'appearance_type': 'plain'
}
return password_reset_confirm(
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
)
@public
def password_reset_done_view(request):
extra_context = {
'appearance_type': 'plain'
}
return password_reset_done(
request, extra_context=extra_context,
template_name='authentication/password_reset_done.html'
)
@public
def password_reset_view(request):
extra_context = {
'appearance_type': 'plain'
}
return password_reset(
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'
)
)
template_name = 'authentication/password_reset_form.html'

View File

@@ -40,18 +40,16 @@ class AutoadminAccountAdapter(DefaultAccountAdapter):
Give superuser privileges automagically if the email address of a
user confirming their email is listed in ``settings.ADMINS``.
"""
super(AutoadminAccountAdapter, self).confirm_email(
request=request, email_address=email_address
)
super(AutoadminAccountAdapter,
self).confirm_email(request, email_address)
if email_address.email in ADMIN_EMAIL_ADDRESSES:
user = email_address.user
user.is_staff = user.is_superuser = True
user.save()
messages.info(
request=request, message=_(
'Welcome Admin! You have been given superuser '
'privileges. Use them with caution.'
)
messages.add_message(
request, messages.INFO,
_('Welcome Admin! You have been given superuser privileges. '
'Use them with caution.')
)

View File

@@ -1,7 +1,7 @@
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
from django.db import models, migrations
class Migration(migrations.Migration):

View File

@@ -6,7 +6,7 @@ from mayan.apps.smart_settings import Namespace
from .literals import DEFAULT_EMAIL, DEFAULT_PASSWORD, DEFAULT_USERNAME
namespace = Namespace(label=_('Auto administrator'), name='autoadmin')
namespace = Namespace(name='autoadmin', label=_('Auto administrator'))
setting_email = namespace.add_setting(
global_name='AUTOADMIN_EMAIL', default=DEFAULT_EMAIL,

View File

@@ -1,7 +1,9 @@
from __future__ import unicode_literals
from django.test import TestCase
from django.urls import reverse
from mayan.apps.common.settings import setting_home_view
from mayan.apps.common.tests import GenericViewTestCase
from mayan.apps.common.tests.utils import mute_stdout
from ..models import AutoAdminSingleton
@@ -9,18 +11,15 @@ from ..models import AutoAdminSingleton
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
class AutoAdminViewCase(TestCase):
def setUp(self):
super(AutoAdminViewCase, self).setUp()
with mute_stdout():
AutoAdminSingleton.objects.create_autoadmin()
def _request_home_view(self):
return self.get(viewname=setting_home_view.value, follow=True)
return self.client.get(
reverse(setting_home_view.value), follow=True
)
def test_login_302_view(self):
response = self._request_home_view()
@@ -32,7 +31,7 @@ class AutoAdminViewCase(GenericViewTestCase):
def test_login_ok_view(self):
autoadmin = AutoAdminSingleton.objects.get()
logged_in = self.login(
logged_in = self.client.login(
username=autoadmin.account,
password=autoadmin.password
)

View File

@@ -33,7 +33,7 @@ class APIDocumentCabinetListView(generics.ListAPIView):
mayan_object_permissions = {'GET': (permission_cabinet_view,)}
def get_queryset(self):
document = get_object_or_404(Document, pk=self.kwargs['document_pk'])
document = get_object_or_404(Document, pk=self.kwargs['pk'])
AccessControlList.objects.check_access(
permissions=permission_document_view, user=self.request.user,
obj=document
@@ -135,12 +135,12 @@ class APICabinetDocumentListView(generics.ListCreateAPIView):
return context
def get_cabinet(self):
return get_object_or_404(klass=Cabinet, pk=self.kwargs['cabinet_pk'])
return get_object_or_404(klass=Cabinet, pk=self.kwargs['pk'])
def get_queryset(self):
cabinet = self.get_cabinet()
return AccessControlList.objects.restrict_queryset(
return AccessControlList.objects.filter_by_access(
permission_document_view, self.request.user,
queryset=cabinet.documents.all()
)
@@ -163,7 +163,7 @@ class APICabinetDocumentView(generics.RetrieveDestroyAPIView):
serializer_class = CabinetDocumentSerializer
def get_cabinet(self):
return get_object_or_404(klass=Cabinet, pk=self.kwargs['cabinet_pk'])
return get_object_or_404(klass=Cabinet, pk=self.kwargs['pk'])
def get_queryset(self):
return self.get_cabinet().documents.all()

View File

@@ -9,18 +9,18 @@ from mayan.apps.acls.permissions import (
)
from mayan.apps.common import (
MayanAppConfig, menu_facet, menu_main, menu_multi_item, menu_object,
menu_secondary
menu_sidebar
)
from mayan.apps.common.classes import ModelAttribute
from mayan.apps.documents.search import document_page_search, document_search
from mayan.apps.navigation import SourceColumn
from .links import (
link_cabinet_add_document, link_cabinet_add_multiple_documents,
link_cabinet_child_add, link_cabinet_create, link_cabinet_delete,
link_cabinet_edit, link_cabinet_list, link_cabinet_view,
link_custom_acl_list, link_document_cabinet_add,
link_document_cabinet_list, link_document_cabinet_remove,
link_document_multiple_cabinet_add, link_document_multiple_cabinet_remove
link_custom_acl_list, link_document_cabinet_list,
link_document_cabinet_remove, link_multiple_document_cabinet_remove
)
from .menus import menu_cabinets
from .methods import method_get_document_cabinets
@@ -49,8 +49,8 @@ class CabinetsApp(MayanAppConfig):
app_label='documents', model_name='Document'
)
DocumentCabinet = self.get_model(model_name='DocumentCabinet')
Cabinet = self.get_model(model_name='Cabinet')
DocumentCabinet = self.get_model('DocumentCabinet')
Cabinet = self.get_model('Cabinet')
# Add explicit order_by as DocumentCabinet ordering Meta option has no
# effect.
@@ -75,22 +75,23 @@ class CabinetsApp(MayanAppConfig):
permission_cabinet_remove_document
)
)
#ModelPermission.register_inheritance(
# model=Cabinet, related='get_root',
#)
ModelPermission.register_inheritance(
model=Cabinet, related='get_root',
)
SourceColumn(
source=Document, label=_('Cabinets'),
func=lambda context: widget_document_cabinets(
document=context['object'], user=context['request'].user
), order=1, label=_('Cabinets'), source=Document
), order=1
)
document_page_search.add_model_field(
label=_('Cabinets'),
field='document_version__document__cabinets__label'
field='document_version__document__cabinets__label',
label=_('Cabinets')
)
document_search.add_model_field(
label=_('Cabinets'), field='cabinets__label'
field='cabinets__label', label=_('Cabinets')
)
menu_facet.bind_links(
@@ -107,8 +108,8 @@ class CabinetsApp(MayanAppConfig):
menu_multi_item.bind_links(
links=(
link_document_multiple_cabinet_add,
link_document_multiple_cabinet_remove
link_cabinet_add_multiple_documents,
link_multiple_document_cabinet_remove
), sources=(Document,)
)
menu_object.bind_links(
@@ -123,11 +124,11 @@ class CabinetsApp(MayanAppConfig):
link_cabinet_delete
), sources=(Cabinet,)
)
menu_secondary.bind_links(
links=(link_document_cabinet_add, link_document_cabinet_remove),
menu_sidebar.bind_links(
links=(link_cabinet_add_document, link_document_cabinet_remove),
sources=(
'cabinets:document_cabinet_list',
'cabinets:document_cabinet_add',
'cabinets:cabinet_add_document',
'cabinets:document_cabinet_remove'
)
)

View File

@@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.events import EventTypeNamespace
namespace = EventTypeNamespace(label=_('Cabinets'), name='cabinets')
namespace = EventTypeNamespace(name='cabinets', label=_('Cabinets'))
event_cabinets_add_document = namespace.add_event_type(
label=_('Document added to cabinet'), name='add_document'

View File

@@ -1,14 +1,33 @@
from __future__ import absolute_import, unicode_literals
import logging
from django import forms
from django.utils.translation import ugettext_lazy as _
from mayan.apps.common.forms import FilteredSelectionForm
from mayan.apps.acls.models import AccessControlList
from .models import Cabinet
logger = logging.getLogger(__name__)
class CabinetListForm(FilteredSelectionForm):
class Meta:
allow_multiple = True
field_name = 'cabinets'
label = _('Cabinets')
required = False
widget_attributes = {'class': 'select2'}
class CabinetListForm(forms.Form):
def __init__(self, *args, **kwargs):
help_text = kwargs.pop('help_text', None)
permission = kwargs.pop('permission', None)
queryset = kwargs.pop('queryset', Cabinet.objects.all())
user = kwargs.pop('user', None)
logger.debug('user: %s', user)
super(CabinetListForm, self).__init__(*args, **kwargs)
queryset = AccessControlList.objects.filter_by_access(
permission=permission, user=user, queryset=queryset
)
self.fields['cabinets'] = forms.ModelMultipleChoiceField(
label=_('Cabinets'), help_text=help_text,
queryset=queryset, required=False,
widget=forms.SelectMultiple(attrs={'class': 'select2'})
)

View File

@@ -3,25 +3,7 @@ from __future__ import absolute_import, unicode_literals
from mayan.apps.appearance.classes import Icon
icon_cabinet = Icon(driver_name='fontawesome', symbol='columns')
icon_cabinet_add = Icon(driver_name='fontawesome', symbol='plus')
icon_cabinet_child_add = Icon(driver_name='fontawesome', symbol='plus')
icon_cabinet_create = Icon(driver_name='fontawesome', symbol='plus')
icon_cabinet_delete = Icon(driver_name='fontawesome', symbol='times')
icon_cabinet_edit = Icon(driver_name='fontawesome', symbol='pencil-alt')
icon_cabinet_list = Icon(driver_name='fontawesome', symbol='columns')
icon_cabinet_view = Icon(driver_name='fontawesome', symbol='columns')
icon_document_cabinet_add = Icon(
driver_name='fontawesome-dual', primary_symbol='columns',
secondary_symbol='arrow-right'
)
icon_document_multiple_cabinet_add = Icon(
driver_name='fontawesome-dual', primary_symbol='columns',
secondary_symbol='arrow-right'
)
icon_document_cabinet_remove = Icon(
driver_name='fontawesome-dual', primary_symbol='columns',
secondary_symbol='minus'
)
icon_document_multiple_cabinet_remove = Icon(
driver_name='fontawesome-dual', primary_symbol='columns',
secondary_symbol='minus'
)

View File

@@ -9,10 +9,8 @@ from mayan.apps.documents.permissions import permission_document_view
from mayan.apps.navigation import Link, get_cascade_condition
from .icons import (
icon_cabinet_child_add, icon_cabinet_create, icon_cabinet_delete,
icon_cabinet_edit, icon_cabinet_list, icon_cabinet_view,
icon_document_cabinet_add, icon_document_cabinet_remove,
icon_document_multiple_cabinet_add, icon_document_multiple_cabinet_remove
icon_cabinet_add, icon_cabinet_child_add, icon_cabinet_create,
icon_cabinet_list
)
from .permissions import (
permission_cabinet_add_document, permission_cabinet_create,
@@ -24,27 +22,25 @@ from .permissions import (
link_document_cabinet_list = Link(
args='resolved_object.pk', icon_class=icon_cabinet_list,
permission=permission_document_view, text=_('Cabinets'),
view='cabinets:document_cabinet_list',
permissions=(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,
permission=permission_cabinet_remove_document,
args='resolved_object.pk',
permissions=(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,
permission=permission_cabinet_add_document, text=_('Add to cabinets'),
view='cabinets:document_cabinet_add',
link_cabinet_add_document = Link(
args='object.pk', icon_class=icon_cabinet_add,
permissions=(permission_cabinet_add_document,), text=_('Add to cabinets'),
view='cabinets:cabinet_add_document',
)
link_document_multiple_cabinet_add = Link(
icon_class=icon_document_multiple_cabinet_add, text=_('Add to cabinets'),
view='cabinets:document_multiple_cabinet_add'
link_cabinet_add_multiple_documents = Link(
text=_('Add to cabinets'), view='cabinets:cabinet_add_multiple_documents'
)
link_document_multiple_cabinet_remove = Link(
icon_class=icon_document_multiple_cabinet_remove,
link_multiple_document_cabinet_remove = Link(
text=_('Remove from cabinets'),
view='cabinets:document_multiple_cabinet_remove'
view='cabinets:multiple_document_cabinet_remove'
)
# Cabinet links
@@ -61,21 +57,19 @@ link_custom_acl_list.condition = cabinet_is_root
link_cabinet_child_add = Link(
args='object.pk', icon_class=icon_cabinet_child_add,
permission=permission_cabinet_create, text=_('Add new level'),
permissions=(permission_cabinet_create,), text=_('Add new level'),
view='cabinets:cabinet_child_add'
)
link_cabinet_create = Link(
icon_class=icon_cabinet_create, permission=permission_cabinet_create,
icon_class=icon_cabinet_create, permissions=(permission_cabinet_create,),
text=_('Create cabinet'), view='cabinets:cabinet_create'
)
link_cabinet_delete = Link(
args='object.pk', icon_class=icon_cabinet_delete,
permission=permission_cabinet_delete, tags='dangerous',
text=_('Delete'), view='cabinets:cabinet_delete'
args='object.pk', permissions=(permission_cabinet_delete,),
tags='dangerous', text=_('Delete'), view='cabinets:cabinet_delete'
)
link_cabinet_edit = Link(
args='object.pk', icon_class=icon_cabinet_edit,
permission=permission_cabinet_edit, text=_('Edit'),
args='object.pk', permissions=(permission_cabinet_edit,), text=_('Edit'),
view='cabinets:cabinet_edit'
)
link_cabinet_list = Link(
@@ -86,7 +80,6 @@ link_cabinet_list = Link(
view='cabinets:cabinet_list'
)
link_cabinet_view = Link(
args='object.pk', icon_class=icon_cabinet_view,
permission=permission_cabinet_view, text=_('Details'),
args='object.pk', permissions=(permission_cabinet_view,), text=_('Details'),
view='cabinets:cabinet_view'
)

View File

@@ -53,13 +53,11 @@ class Cabinet(MPTTModel):
"""
self.documents.add(document)
event_cabinets_add_document.commit(
actor=user, action_object=self, target=document
action_object=self, actor=user, target=document
)
def get_absolute_url(self):
return reverse(
viewname='cabinets:cabinet_view', kwargs={'cabinet_pk': self.pk}
)
return reverse('cabinets:cabinet_view', args=(self.pk,))
def get_document_count(self, user):
"""
@@ -73,9 +71,8 @@ class Cabinet(MPTTModel):
Provide a queryset of the documents in a cabinet. The queryset is
filtered by access.
"""
return AccessControlList.objects.restrict_queryset(
permission=permission_document_view, queryset=self.documents,
user=user
return AccessControlList.objects.filter_by_access(
permission_document_view, user, queryset=self.documents
)
def get_full_path(self):
@@ -96,7 +93,7 @@ class Cabinet(MPTTModel):
"""
self.documents.remove(document)
event_cabinets_remove_document.commit(
actor=user, action_object=self, target=document
action_object=self, actor=user, target=document
)
def validate_unique(self, exclude=None):
@@ -108,13 +105,9 @@ class Cabinet(MPTTModel):
"""
with transaction.atomic():
if connection.vendor == 'oracle':
queryset = Cabinet.objects.filter(
parent=self.parent, label=self.label
)
queryset = Cabinet.objects.filter(parent=self.parent, label=self.label)
else:
queryset = Cabinet.objects.select_for_update().filter(
parent=self.parent, label=self.label
)
queryset = Cabinet.objects.select_for_update().filter(parent=self.parent, label=self.label)
if queryset.exists():
params = {

View File

@@ -9,20 +9,20 @@ namespace = PermissionNamespace(label=_('Cabinets'), name='cabinets')
# Translators: this refers to the permission that will allow users to add
# documents to cabinets.
permission_cabinet_add_document = namespace.add_permission(
label=_('Add documents to cabinets'), name='cabinet_add_document'
name='cabinet_add_document', label=_('Add documents to cabinets')
)
permission_cabinet_create = namespace.add_permission(
label=_('Create cabinets'), name='cabinet_create'
name='cabinet_create', label=_('Create cabinets')
)
permission_cabinet_delete = namespace.add_permission(
label=_('Delete cabinets'), name='cabinet_delete'
name='cabinet_delete', label=_('Delete cabinets')
)
permission_cabinet_edit = namespace.add_permission(
label=_('Edit cabinets'), name='cabinet_edit'
name='cabinet_edit', label=_('Edit cabinets')
)
permission_cabinet_remove_document = namespace.add_permission(
label=_('Remove documents from cabinets'), name='cabinet_remove_document'
name='cabinet_remove_document', label=_('Remove documents from cabinets')
)
permission_cabinet_view = namespace.add_permission(
label=_('View cabinets'), name='cabinet_view'
name='cabinet_view', label=_('View cabinets')
)

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_path='mayan.apps.cabinets.serializers.CabinetSerializer'
serializer_string='mayan.apps.cabinets.serializers.CabinetSerializer'
)
cabinet_search.add_model_field(

View File

@@ -54,7 +54,7 @@ class CabinetSerializer(serializers.ModelSerializer):
def get_parent_url(self, obj):
if obj.parent:
return reverse(
viewname='rest_api:cabinet-detail', kwargs={'cabinet_pk': obj.parent.pk},
'rest_api:cabinet-detail', args=(obj.parent.pk,),
format=self.context['format'],
request=self.context.get('request')
)
@@ -167,10 +167,9 @@ class CabinetDocumentSerializer(DocumentSerializer):
def get_cabinet_document_url(self, instance):
return reverse(
viewname='rest_api:cabinet-document', kwargs={
'cabinet_pk': self.context['cabinet'].pk,
'document_pk': instance.pk
}, request=self.context['request'], format=self.context['format']
'rest_api:cabinet-document', args=(
self.context['cabinet'].pk, instance.pk
), request=self.context['request'], format=self.context['format']
)

View File

@@ -1,6 +1,7 @@
from __future__ import unicode_literals
from django.contrib.auth import get_user_model
from django.urls import reverse
from django.utils.encoding import force_text
from rest_framework import status
@@ -38,9 +39,8 @@ class CabinetAPITestCase(DocumentTestMixin, APITestCase):
self.document_2 = self.upload_document()
def test_cabinet_create(self):
response = self.post(
viewname='rest_api:cabinet-list',
data={'label': TEST_CABINET_LABEL}
response = self.client.post(
reverse('rest_api:cabinet-list'), {'label': TEST_CABINET_LABEL}
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
@@ -53,8 +53,8 @@ class CabinetAPITestCase(DocumentTestMixin, APITestCase):
self.assertEqual(cabinet.label, TEST_CABINET_LABEL)
def test_cabinet_create_with_single_document(self):
response = self.post(
viewname='rest_api:cabinet-list', data={
response = self.client.post(
reverse('rest_api:cabinet-list'), {
'label': TEST_CABINET_LABEL, 'documents_pk_list': '{}'.format(
self.document.pk
)
@@ -73,8 +73,8 @@ class CabinetAPITestCase(DocumentTestMixin, APITestCase):
self.assertEqual(cabinet.label, TEST_CABINET_LABEL)
def test_cabinet_create_with_multiple_documents(self):
response = self.post(
viewname='rest_api:cabinet-list', data={
response = self.client.post(
reverse('rest_api:cabinet-list'), {
'label': TEST_CABINET_LABEL,
'documents_pk_list': '{},{}'.format(
self.document.pk, self.document_2.pk
@@ -102,9 +102,11 @@ class CabinetAPITestCase(DocumentTestMixin, APITestCase):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
cabinet.documents.add(self.document)
response = self.delete(
viewname='rest_api:cabinet-document',
kwargs={'cabinet_pk': cabinet.pk, 'document_pk': self.document.pk}
response = self.client.delete(
reverse(
'rest_api:cabinet-document',
args=(cabinet.pk, self.document.pk)
)
)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
@@ -114,9 +116,11 @@ class CabinetAPITestCase(DocumentTestMixin, APITestCase):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
cabinet.documents.add(self.document)
response = self.get(
viewname='rest_api:cabinet-document',
kwargs={'cabinet_pk': cabinet.pk, 'document_pk': self.document.pk}
response = self.client.get(
reverse(
'rest_api:cabinet-document',
args=(cabinet.pk, self.document.pk)
)
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -128,9 +132,8 @@ class CabinetAPITestCase(DocumentTestMixin, APITestCase):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
cabinet.documents.add(self.document)
response = self.get(
viewname='rest_api:cabinet-document-list',
kwargs={'cabinet_pk': cabinet.pk}
response = self.client.get(
reverse('rest_api:cabinet-document-list', args=(cabinet.pk,))
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -141,9 +144,8 @@ class CabinetAPITestCase(DocumentTestMixin, APITestCase):
def test_cabinet_delete(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
response = self.delete(
viewname='rest_api:cabinet-detail',
kwargs={'cabinet_pk': cabinet.pk}
response = self.client.delete(
reverse('rest_api:cabinet-detail', args=(cabinet.pk,))
)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
@@ -152,10 +154,9 @@ class CabinetAPITestCase(DocumentTestMixin, APITestCase):
def test_cabinet_edit_via_patch(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
response = self.patch(
viewname='rest_api:cabinet-detail',
kwargs={'cabinet_pk': cabinet.pk},
data={'label': TEST_CABINET_EDITED_LABEL}
response = self.client.patch(
reverse('rest_api:cabinet-detail', args=(cabinet.pk,)),
{'label': TEST_CABINET_EDITED_LABEL}
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -166,10 +167,9 @@ class CabinetAPITestCase(DocumentTestMixin, APITestCase):
def test_cabinet_edit_via_put(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
response = self.put(
viewname='rest_api:cabinet-detail',
kwargs={'cabinet_pk': cabinet.pk},
data={'label': TEST_CABINET_EDITED_LABEL}
response = self.client.put(
reverse('rest_api:cabinet-detail', args=(cabinet.pk,)),
{'label': TEST_CABINET_EDITED_LABEL}
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -180,9 +180,8 @@ class CabinetAPITestCase(DocumentTestMixin, APITestCase):
def test_cabinet_add_document(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
response = self.post(
viewname='rest_api:cabinet-document-list',
kwargs={'cabinet_pk': cabinet.pk}, data={
response = self.client.post(
reverse('rest_api:cabinet-document-list', args=(cabinet.pk,)), {
'documents_pk_list': '{}'.format(self.document.pk)
}
)
@@ -195,10 +194,8 @@ class CabinetAPITestCase(DocumentTestMixin, APITestCase):
def test_cabinet_add_multiple_documents(self):
cabinet = Cabinet.objects.create(label=TEST_CABINET_LABEL)
response = self.post(
viewname='rest_api:cabinet-document-list',
kwargs={'cabinet_pk': cabinet.pk},
data={
response = self.client.post(
reverse('rest_api:cabinet-document-list', args=(cabinet.pk,)), {
'documents_pk_list': '{},{}'.format(
self.document.pk, self.document_2.pk
)
@@ -218,8 +215,8 @@ class CabinetAPITestCase(DocumentTestMixin, APITestCase):
label=TEST_CABINET_LABEL, parent=cabinet
)
response = self.get(
viewname='rest_api:cabinet-list'
response = self.client.get(
reverse('rest_api:cabinet-list')
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['results'][0]['label'], cabinet.label)
@@ -229,10 +226,12 @@ class CabinetAPITestCase(DocumentTestMixin, APITestCase):
cabinet.documents.add(self.document)
response = self.delete(
viewname='rest_api:cabinet-document', kwargs={
'cabinet_pk': cabinet.pk, 'document_pk': self.document.pk
}
response = self.client.delete(
reverse(
'rest_api:cabinet-document', args=(
cabinet.pk, self.document.pk
)
),
)
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
self.assertEqual(cabinet.documents.count(), 0)

View File

@@ -53,15 +53,14 @@ class CabinetViewTestCase(CabinetTestMixin, GenericDocumentViewTestCase):
def _request_delete_cabinet(self):
return self.post(
viewname='cabinets:cabinet_delete',
kwargs={'cabinet_pk': self.cabinet.pk}
viewname='cabinets:cabinet_delete', args=(self.cabinet.pk,)
)
def test_cabinet_delete_view_no_permission(self):
self._create_cabinet()
response = self._request_delete_cabinet()
self.assertEqual(response.status_code, 404)
self.assertEqual(response.status_code, 403)
self.assertEqual(Cabinet.objects.count(), 1)
def test_cabinet_delete_view_with_access(self):
@@ -75,7 +74,7 @@ class CabinetViewTestCase(CabinetTestMixin, GenericDocumentViewTestCase):
def _request_edit_cabinet(self):
return self.post(
viewname='cabinets:cabinet_edit', kwargs={'cabinet_pk': self.cabinet.pk}, data={
viewname='cabinets:cabinet_edit', args=(self.cabinet.pk,), data={
'label': TEST_CABINET_EDITED_LABEL
}
)
@@ -84,7 +83,7 @@ class CabinetViewTestCase(CabinetTestMixin, GenericDocumentViewTestCase):
self._create_cabinet()
response = self._request_edit_cabinet()
self.assertEqual(response.status_code, 404)
self.assertEqual(response.status_code, 403)
self.cabinet.refresh_from_db()
self.assertEqual(self.cabinet.label, TEST_CABINET_LABEL)
@@ -126,14 +125,16 @@ class DocumentViewsTestCase(CabinetTestMixin, GenericDocumentViewTestCase):
def _add_document_to_cabinet(self):
return self.post(
viewname='cabinets:document_cabinet_add', kwargs={
'document_pk': self.document.pk
}, data={'cabinets': self.cabinet.pk}
viewname='cabinets:cabinet_add_document', args=(
self.document.pk,
), data={'cabinets': self.cabinet.pk}
)
def test_cabinet_add_document_view_no_permission(self):
self._create_cabinet()
self.grant_permission(permission=permission_cabinet_view)
response = self._add_document_to_cabinet()
self.assertContains(
@@ -142,37 +143,10 @@ class DocumentViewsTestCase(CabinetTestMixin, GenericDocumentViewTestCase):
self.cabinet.refresh_from_db()
self.assertEqual(self.cabinet.documents.count(), 0)
def test_cabinet_add_document_view_with_cabinet_access(self):
self._create_cabinet()
self.grant_access(
obj=self.cabinet, permission=permission_cabinet_add_document
)
response = self._add_document_to_cabinet()
self.assertContains(
response, text='Select a valid choice.', status_code=404
)
self.cabinet.refresh_from_db()
self.assertEqual(self.cabinet.documents.count(), 0)
def test_cabinet_add_document_view_with_document_access(self):
self._create_cabinet()
self.grant_access(
obj=self.cabinet, permission=permission_cabinet_add_document
)
response = self._add_document_to_cabinet()
self.assertContains(
response, text='Select a valid choice.', status_code=404
)
self.cabinet.refresh_from_db()
self.assertEqual(self.cabinet.documents.count(), 0)
def test_cabinet_add_document_view_with_full_access(self):
def test_cabinet_add_document_view_with_access(self):
self._create_cabinet()
self.grant_access(obj=self.cabinet, permission=permission_cabinet_view)
self.grant_access(
obj=self.cabinet, permission=permission_cabinet_add_document
)
@@ -192,7 +166,7 @@ class DocumentViewsTestCase(CabinetTestMixin, GenericDocumentViewTestCase):
def _request_add_multiple_documents_to_cabinet(self):
return self.post(
viewname='cabinets:document_multiple_cabinet_add', data={
viewname='cabinets:cabinet_add_multiple_documents', data={
'id_list': (self.document.pk,), 'cabinets': self.cabinet.pk
}
)
@@ -200,6 +174,8 @@ class DocumentViewsTestCase(CabinetTestMixin, GenericDocumentViewTestCase):
def test_cabinet_add_multiple_documents_view_no_permission(self):
self._create_cabinet()
self.grant_permission(permission=permission_cabinet_view)
response = self._request_add_multiple_documents_to_cabinet()
self.assertContains(
@@ -208,7 +184,7 @@ class DocumentViewsTestCase(CabinetTestMixin, GenericDocumentViewTestCase):
self.cabinet.refresh_from_db()
self.assertEqual(self.cabinet.documents.count(), 0)
def test_cabinet_add_multiple_documents_view_with_full_access(self):
def test_cabinet_add_multiple_documents_view_with_access(self):
self._create_cabinet()
self.grant_access(
@@ -230,7 +206,7 @@ class DocumentViewsTestCase(CabinetTestMixin, GenericDocumentViewTestCase):
def _request_remove_document_from_cabinet(self):
return self.post(
viewname='cabinets:document_cabinet_remove',
kwargs={'document_pk': self.document.pk}, data={
args=(self.document.pk,), data={
'cabinets': (self.cabinet.pk,),
}
)
@@ -249,7 +225,7 @@ class DocumentViewsTestCase(CabinetTestMixin, GenericDocumentViewTestCase):
self.cabinet.refresh_from_db()
self.assertEqual(self.cabinet.documents.count(), 1)
def test_cabinet_remove_document_view_with_full_access(self):
def test_cabinet_remove_document_view_with_access(self):
self._create_cabinet()
self.cabinet.documents.add(self.document)
@@ -269,8 +245,7 @@ class DocumentViewsTestCase(CabinetTestMixin, GenericDocumentViewTestCase):
def _request_document_cabinet_list(self):
return self.get(
viewname='cabinets:document_cabinet_list',
kwargs={'document_pk': self.document.pk}
viewname='cabinets:document_cabinet_list', args=(self.document.pk,)
)
def test_document_cabinet_list_view_no_permission(self):
@@ -278,10 +253,10 @@ class DocumentViewsTestCase(CabinetTestMixin, GenericDocumentViewTestCase):
self.cabinet.documents.add(self.document)
response = self._request_document_cabinet_list()
self.assertNotContains(
response=response, text=self.document.label, status_code=404
response=response, text=self.document.label, status_code=403
)
self.assertNotContains(
response=response, text=self.cabinet.label, status_code=404
response=response, text=self.cabinet.label, status_code=403
)
def test_document_cabinet_list_view_with_cabinet_access(self):
@@ -290,10 +265,10 @@ class DocumentViewsTestCase(CabinetTestMixin, GenericDocumentViewTestCase):
self.grant_access(obj=self.cabinet, permission=permission_cabinet_view)
response = self._request_document_cabinet_list()
self.assertNotContains(
response=response, text=self.document.label, status_code=404
response=response, text=self.document.label, status_code=403
)
self.assertNotContains(
response=response, text=self.cabinet.label, status_code=404
response=response, text=self.cabinet.label, status_code=403
)
def test_document_cabinet_list_view_with_document_access(self):

View File

@@ -35,7 +35,7 @@ class CabinetDocumentUploadTestCase(GenericDocumentViewTestCase):
def _request_upload_interactive_document_create_view(self):
with open(TEST_SMALL_DOCUMENT_PATH, mode='rb') as file_object:
return self.post(
viewname='sources:upload_interactive', kwargs={'source_pk': self.source.pk},
viewname='sources:upload_interactive', args=(self.source.pk,),
data={
'document_type_id': self.document_type.pk,
'source-file': file_object,

View File

@@ -14,74 +14,61 @@ from .views import (
)
urlpatterns = [
url(r'^list/$', CabinetListView.as_view(), name='cabinet_list'),
url(
regex=r'^cabinets/$', name='cabinet_list',
view=CabinetListView.as_view()
r'^(?P<pk>\d+)/child/add/$', CabinetChildAddView.as_view(),
name='cabinet_child_add'
),
url(r'^create/$', CabinetCreateView.as_view(), name='cabinet_create'),
url(
r'^(?P<pk>\d+)/edit/$', CabinetEditView.as_view(), name='cabinet_edit'
),
url(
regex=r'^cabinets/create/$', name='cabinet_create',
view=CabinetCreateView.as_view()
r'^(?P<pk>\d+)/delete/$', CabinetDeleteView.as_view(),
name='cabinet_delete'
),
url(r'^(?P<pk>\d+)/$', CabinetDetailView.as_view(), name='cabinet_view'),
url(
r'^document/(?P<pk>\d+)/cabinet/add/$',
DocumentAddToCabinetView.as_view(), name='cabinet_add_document'
),
url(
regex=r'^cabinets/(?P<cabinet_pk>\d+)/$', name='cabinet_view',
view=CabinetDetailView.as_view()
r'^document/multiple/cabinet/add/$',
DocumentAddToCabinetView.as_view(),
name='cabinet_add_multiple_documents'
),
url(
regex=r'^cabinets/(?P<cabinet_pk>\d+)/delete/$', name='cabinet_delete',
view=CabinetDeleteView.as_view()
r'^document/(?P<pk>\d+)/cabinet/remove/$',
DocumentRemoveFromCabinetView.as_view(), name='document_cabinet_remove'
),
url(
regex=r'^cabinets/(?P<cabinet_pk>\d+)/edit/$', name='cabinet_edit',
view=CabinetEditView.as_view()
r'^document/multiple/cabinet/remove/$',
DocumentRemoveFromCabinetView.as_view(),
name='multiple_document_cabinet_remove'
),
url(
regex=r'^cabinets/(?P<cabinet_pk>\d+)/child/add/$',
name='cabinet_child_add', view=CabinetChildAddView.as_view()
),
url(
regex=r'^documents/(?P<document_pk>\d+)/cabinets/add/$',
name='document_cabinet_add', view=DocumentAddToCabinetView.as_view()
),
url(
regex=r'^documents/multiple/cabinets/add/$',
name='document_multiple_cabinet_add',
view=DocumentAddToCabinetView.as_view()
),
url(
regex=r'^documents/(?P<document_pk>\d+)/cabinets/remove/$',
name='document_cabinet_remove',
view=DocumentRemoveFromCabinetView.as_view()
),
url(
regex=r'^documents/multiple/cabinets/remove/$',
name='document_multiple_cabinet_remove',
view=DocumentRemoveFromCabinetView.as_view()
),
url(
regex=r'^documents/(?P<document_pk>\d+)/cabinets/list/$',
name='document_cabinet_list', view=DocumentCabinetListView.as_view()
r'^document/(?P<pk>\d+)/cabinet/list/$',
DocumentCabinetListView.as_view(), name='document_cabinet_list'
),
]
api_urls = [
url(
regex=r'^cabinets/(?P<cabinet_pk>\d+)/documents/(?P<document_pk>\d+)/$',
name='cabinet-document', view=APICabinetDocumentView.as_view()
r'^cabinets/(?P<pk>[0-9]+)/documents/(?P<document_pk>[0-9]+)/$',
APICabinetDocumentView.as_view(), name='cabinet-document'
),
url(
regex=r'^cabinets/(?P<cabinet_pk>\d+)/documents/$',
name='cabinet-document-list', view=APICabinetDocumentListView.as_view()
r'^cabinets/(?P<pk>[0-9]+)/documents/$',
APICabinetDocumentListView.as_view(), name='cabinet-document-list'
),
url(
regex=r'^cabinets/(?P<cabinet_pk>\d+)/$', name='cabinet-detail',
view=APICabinetView.as_view()
r'^cabinets/(?P<pk>[0-9]+)/$', APICabinetView.as_view(),
name='cabinet-detail'
),
url(r'^cabinets/$', APICabinetListView.as_view(), name='cabinet-list'),
url(
regex=r'^cabinets/$', name='cabinet-list',
view=APICabinetListView.as_view()
),
url(
regex=r'^documents/(?P<document_pk>\d+)/cabinets/$',
name='document-cabinet-list', view=APIDocumentCabinetListView.as_view()
r'^documents/(?P<pk>[0-9]+)/cabinets/$',
APIDocumentCabinetListView.as_view(), name='document-cabinet-list'
),
]

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.generics import (
from mayan.apps.common.views import (
MultipleObjectFormActionView, SingleObjectCreateView,
SingleObjectDeleteView, SingleObjectEditView, SingleObjectListView
)
@@ -21,7 +21,7 @@ from mayan.apps.documents.views import DocumentListView
from .forms import CabinetListForm
from .icons import icon_cabinet
from .links import (
link_cabinet_child_add, link_cabinet_create, link_document_cabinet_add
link_cabinet_add_document, link_cabinet_child_add, link_cabinet_create
)
from .models import Cabinet
from .permissions import (
@@ -37,7 +37,7 @@ logger = logging.getLogger(__name__)
class CabinetCreateView(SingleObjectCreateView):
fields = ('label',)
model = Cabinet
post_action_redirect = reverse_lazy(viewname='cabinets:cabinet_list')
post_action_redirect = reverse_lazy('cabinets:cabinet_list')
view_permission = permission_cabinet_create
def get_extra_context(self):
@@ -64,8 +64,8 @@ class CabinetChildAddView(SingleObjectCreateView):
cabinet = super(CabinetChildAddView, self).get_object(*args, **kwargs)
AccessControlList.objects.check_access(
obj=cabinet.get_root(), permission=permission_cabinet_edit,
user=self.request.user, raise_404=True
permissions=permission_cabinet_edit, user=self.request.user,
obj=cabinet.get_root()
)
return cabinet
@@ -81,9 +81,7 @@ class CabinetChildAddView(SingleObjectCreateView):
class CabinetDeleteView(SingleObjectDeleteView):
model = Cabinet
object_permission = permission_cabinet_delete
object_permission_raise_404 = True
pk_url_kwarg = 'cabinet_pk'
post_action_redirect = reverse_lazy(viewname='cabinets:cabinet_list')
post_action_redirect = reverse_lazy('cabinets:cabinet_list')
def get_extra_context(self):
return {
@@ -96,10 +94,9 @@ class CabinetDetailView(DocumentListView):
template_name = 'cabinets/cabinet_details.html'
def get_document_queryset(self):
queryset = AccessControlList.objects.restrict_queryset(
permission=permission_document_view,
queryset=self.get_object().documents.all(),
user=self.request.user
queryset = AccessControlList.objects.filter_by_access(
permission=permission_document_view, user=self.request.user,
queryset=self.get_object().documents.all()
)
return queryset
@@ -136,9 +133,7 @@ class CabinetDetailView(DocumentListView):
return context
def get_object(self):
cabinet = get_object_or_404(
klass=Cabinet, pk=self.kwargs['cabinet_pk']
)
cabinet = get_object_or_404(klass=Cabinet, pk=self.kwargs['pk'])
if cabinet.is_root_node():
permission_object = cabinet
@@ -146,8 +141,8 @@ class CabinetDetailView(DocumentListView):
permission_object = cabinet.get_root()
AccessControlList.objects.check_access(
obj=permission_object, permission=permission_cabinet_view,
user=self.request.user, raise_404=True
permissions=permission_cabinet_view, user=self.request.user,
obj=permission_object
)
return cabinet
@@ -157,9 +152,7 @@ class CabinetEditView(SingleObjectEditView):
fields = ('label',)
model = Cabinet
object_permission = permission_cabinet_edit
object_permission_raise_404 = True
pk_url_kwarg = 'cabinet_pk'
post_action_redirect = reverse_lazy(viewname='cabinets:cabinet_list')
post_action_redirect = reverse_lazy('cabinets:cabinet_list')
def get_extra_context(self):
return {
@@ -187,7 +180,7 @@ class CabinetListView(SingleObjectListView):
'no_results_title': _('No cabinets available'),
}
def get_source_queryset(self):
def get_object_list(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')
@@ -195,13 +188,11 @@ class CabinetListView(SingleObjectListView):
class DocumentCabinetListView(CabinetListView):
def dispatch(self, request, *args, **kwargs):
self.document = get_object_or_404(
klass=Document, pk=self.kwargs['document_pk']
)
self.document = get_object_or_404(klass=Document, pk=self.kwargs['pk'])
AccessControlList.objects.check_access(
obj=self.document, permission=permission_document_view,
user=request.user, raise_404=True
permissions=permission_document_view, user=request.user,
obj=self.document
)
return super(DocumentCabinetListView, self).dispatch(
@@ -212,7 +203,7 @@ class DocumentCabinetListView(CabinetListView):
return {
'hide_link': True,
'no_results_icon': icon_cabinet,
'no_results_main_link': link_document_cabinet_add.resolve(
'no_results_main_link': link_cabinet_add_document.resolve(
context=RequestContext(
request=self.request, dict_={'object': self.document}
)
@@ -227,7 +218,7 @@ class DocumentCabinetListView(CabinetListView):
'title': _('Cabinets containing document: %s') % self.document,
}
def get_source_queryset(self):
def get_object_list(self):
return self.document.get_cabinets().all()
@@ -235,8 +226,7 @@ class DocumentAddToCabinetView(MultipleObjectFormActionView):
form_class = CabinetListForm
model = Document
object_permission = permission_cabinet_add_document
pk_url_kwarg = 'document_pk'
success_message_singular = _(
success_message = _(
'Add to cabinet request performed on %(count)d document'
)
success_message_plural = _(
@@ -295,12 +285,12 @@ class DocumentAddToCabinetView(MultipleObjectFormActionView):
for cabinet in form.cleaned_data['cabinets']:
AccessControlList.objects.check_access(
obj=cabinet, permission=permission_cabinet_add_document,
user=self.request.user, raise_404=True
obj=cabinet, permissions=permission_cabinet_add_document,
user=self.request.user
)
if cabinet in cabinet_membership:
messages.warning(
request=self.request, message=_(
self.request, _(
'Document: %(document)s is already in '
'cabinet: %(cabinet)s.'
) % {
@@ -312,7 +302,7 @@ class DocumentAddToCabinetView(MultipleObjectFormActionView):
document=instance, user=self.request.user
)
messages.success(
request=self.request, message=_(
self.request, _(
'Document: %(document)s added to cabinet: '
'%(cabinet)s successfully.'
) % {
@@ -325,8 +315,7 @@ class DocumentRemoveFromCabinetView(MultipleObjectFormActionView):
form_class = CabinetListForm
model = Document
object_permission = permission_cabinet_remove_document
pk_url_kwarg = 'document_pk'
success_message_singular = _(
success_message = _(
'Remove from cabinet request performed on %(count)d document'
)
success_message_plural = _(
@@ -383,13 +372,13 @@ class DocumentRemoveFromCabinetView(MultipleObjectFormActionView):
for cabinet in form.cleaned_data['cabinets']:
AccessControlList.objects.check_access(
obj=cabinet, permission=permission_cabinet_remove_document,
user=self.request.user, raise_404=True
obj=cabinet, permissions=permission_cabinet_remove_document,
user=self.request.user
)
if cabinet not in cabinet_membership:
messages.warning(
request=self.request, message=_(
self.request, _(
'Document: %(document)s is not in cabinet: '
'%(cabinet)s.'
) % {
@@ -401,7 +390,7 @@ class DocumentRemoveFromCabinetView(MultipleObjectFormActionView):
document=instance, user=self.request.user
)
messages.success(
request=self.request, message=_(
self.request, _(
'Document: %(document)s removed from cabinet: '
'%(cabinet)s.'
) % {

View File

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

View File

@@ -9,7 +9,6 @@ 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
@@ -29,7 +28,7 @@ class WizardStepCabinets(WizardStep):
@classmethod
def done(cls, wizard):
result = {}
cleaned_data = wizard.get_cleaned_data_for_step(step=cls.name)
cleaned_data = wizard.get_cleaned_data_for_step(cls.name)
if cleaned_data:
result['cabinets'] = [
force_text(cabinet.pk) for cabinet in cleaned_data['cabinets']
@@ -42,7 +41,6 @@ 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_check_in, permission_document_check_in_override,
permission_document_checkin, permission_document_checkin_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.restrict_queryset(
filtered_documents = AccessControlList.objects.filter_by_access(
permission=permission_document_view, user=self.request.user,
queryset=DocumentCheckout.objects.checked_out_documents()
)
filtered_documents = AccessControlList.objects.restrict_queryset(
filtered_documents = AccessControlList.objects.filter_by_access(
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.restrict_queryset(
filtered_documents = AccessControlList.objects.filter_by_access(
permission=permission_document_view, user=self.request.user,
queryset=DocumentCheckout.objects.checked_out_documents()
)
filtered_documents = AccessControlList.objects.restrict_queryset(
filtered_documents = AccessControlList.objects.filter_by_access(
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_check_in, user=request.user,
permissions=permission_document_checkin, user=request.user,
obj=document
)
else:
AccessControlList.objects.check_access(
permissions=permission_document_check_in_override,
permissions=permission_document_checkin_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_multi_item, menu_secondary
MayanAppConfig, menu_facet, menu_main, menu_sidebar
)
from mayan.apps.dashboards.dashboards import dashboard_main
from mayan.apps.events import ModelEventType
@@ -22,11 +22,9 @@ 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_document_check_in, link_document_checkout, link_document_checkout_info,
link_document_checkout_list, link_document_multiple_check_in,
link_document_multiple_checkout
link_checkin_document, link_checkout_document, link_checkout_info,
link_checkout_list
)
from .literals import CHECK_EXPIRED_CHECK_OUTS_INTERVAL
from .methods import (
@@ -34,7 +32,7 @@ from .methods import (
method_is_checked_out
)
from .permissions import (
permission_document_check_in, permission_document_check_in_override,
permission_document_checkin, permission_document_checkin_override,
permission_document_checkout, permission_document_checkout_detail_view
)
from .queues import * # NOQA
@@ -71,10 +69,6 @@ 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,
@@ -85,8 +79,8 @@ class CheckoutsApp(MayanAppConfig):
ModelPermission.register(
model=Document, permissions=(
permission_document_checkout,
permission_document_check_in,
permission_document_check_in_override,
permission_document_checkin,
permission_document_checkin_override,
permission_document_checkout_detail_view
)
)
@@ -121,18 +115,13 @@ class CheckoutsApp(MayanAppConfig):
widget=DashboardWidgetTotalCheckouts, order=-1
)
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),
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),
sources=(
'checkouts:document_checkout_info', 'checkouts:document_checkout',
'checkouts:document_check_in'
'checkouts:checkout_info', 'checkouts:checkout_document',
'checkouts:checkin_document'
)
)

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(viewname='checkouts:document_checkout_list')
link = reverse_lazy('checkouts:checkout_list')
def render(self, request):
AccessControlList = apps.get_model(
@@ -23,16 +23,14 @@ class DashboardWidgetTotalCheckouts(DashboardWidgetNumeric):
DocumentCheckout = apps.get_model(
app_label='checkouts', model_name='DocumentCheckout'
)
queryset = AccessControlList.objects.restrict_queryset(
queryset = AccessControlList.objects.filter_by_access(
permission=permission_document_checkout_detail_view,
queryset=DocumentCheckout.objects.checked_out_documents(),
user=request.user,
queryset=DocumentCheckout.objects.checked_out_documents()
)
queryset = AccessControlList.objects.restrict_queryset(
permission=permission_document_view, queryset=queryset,
user=request.user
queryset = AccessControlList.objects.filter_by_access(
permission=permission_document_view, user=request.user,
queryset=queryset
)
self.count = queryset.count()
return super(DashboardWidgetTotalCheckouts, self).render(
request=request
)
return super(DashboardWidgetTotalCheckouts, self).render(request)

View File

@@ -4,19 +4,19 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.events import EventTypeNamespace
namespace = EventTypeNamespace(label=_('Checkouts'), name='checkouts')
namespace = EventTypeNamespace(name='checkouts', label=_('Checkouts'))
event_document_auto_check_in = namespace.add_event_type(
label=_('Document automatically checked in'),
name='document_auto_check_in'
name='document_auto_check_in',
label=_('Document automatically checked in')
)
event_document_check_in = namespace.add_event_type(
label=_('Document checked in'), name='document_check_in'
name='document_check_in', label=_('Document checked in')
)
event_document_check_out = namespace.add_event_type(
label=_('Document checked out'), name='document_check_out'
name='document_check_out', label=_('Document checked out')
)
event_document_forceful_check_in = namespace.add_event_type(
label=_('Document forcefully checked in'),
name='document_forceful_check_in'
name='document_forceful_check_in',
label=_('Document forcefully checked 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(document=instance.document) and not instance.pk:
if NewVersionBlock.objects.is_blocked(instance.document) and not instance.pk:
# Block only new versions (no pk), not existing version being updated.
raise NewDocumentVersionNotAllowed

View File

@@ -1,13 +0,0 @@
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,14 +2,6 @@ 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,12 +4,10 @@ from django.utils.translation import ugettext_lazy as _
from mayan.apps.navigation import Link
from .icons import (
icon_checkin_document, icon_checkout_document, icon_checkout_info
)
from .icons import icon_checkout_info
from .permissions import (
permission_document_check_in, permission_document_checkout,
permission_document_checkout_detail_view
permission_document_checkin, permission_document_checkin_override,
permission_document_checkout
)
@@ -29,32 +27,24 @@ def is_not_checked_out(context):
return True
link_document_checkout_list = Link(
link_checkout_list = Link(
icon_class=icon_checkout_info, text=_('Checkouts'),
view='checkouts:document_checkout_list'
view='checkouts:checkout_list'
)
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_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_multiple_checkout = Link(
icon_class=icon_checkout_document,
permission=permission_document_checkout, text=_('Check out'),
view='checkouts:document_multiple_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_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',
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',
)

View File

@@ -2,54 +2,48 @@ from __future__ import absolute_import, unicode_literals
import logging
from django.core.exceptions import PermissionDenied
from django.db import models, transaction
from django.apps import apps
from django.db import models
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, NewDocumentVersionNotAllowed
from .exceptions import DocumentNotCheckedOut
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(
_('Document not checked out.')
)
raise DocumentNotCheckedOut
else:
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)
if user:
if self.get_document_checkout_info(document).user != user:
event_document_forceful_check_in.commit(
actor=user, target=document
)
else:
event_document_auto_check_in.commit(target=document)
event_document_check_in.commit(actor=user, target=document)
else:
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():
@@ -57,16 +51,21 @@ class DocumentCheckoutManager(models.Manager):
def checkout_document(self, document, expiration_datetime, user, block_new_version=True):
return self.create(
block_new_version=block_new_version, document=document,
expiration_datetime=expiration_datetime, user=user
document=document, expiration_datetime=expiration_datetime,
user=user, block_new_version=block_new_version
)
def checked_out_documents(self):
return Document.objects.filter(
pk__in=self.model.objects.values('document__id')
pk__in=self.model.objects.all().values_list(
'document__pk', flat=True
)
)
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:
@@ -81,29 +80,38 @@ class DocumentCheckoutManager(models.Manager):
raise DocumentNotCheckedOut
def get_document_checkout_state(self, document):
if self.is_document_checked_out(document=document):
if self.is_document_checked_out(document):
return STATE_CHECKED_OUT
else:
return STATE_CHECKED_IN
def get_expired_check_outs(self):
expired_list = Document.objects.filter(
pk__in=self.filter(
pk__in=self.model.objects.filter(
expiration_datetime__lte=now()
).values('document__id')
).values_list('document__pk', flat=True)
)
logger.debug('expired_list: %s', expired_list)
return expired_list
def is_document_checked_out(self, document):
return self.filter(document=document).exists()
if self.model.objects.filter(document=document):
return True
else:
return False
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:
@@ -111,12 +119,5 @@ 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, transaction
from django.db import models
from django.urls import reverse
from django.utils.encoding import python_2_unicode_compatible
from django.utils.timezone import now
@@ -68,42 +68,37 @@ class DocumentCheckout(models.Model):
)
def delete(self, *args, **kwargs):
with transaction.atomic():
NewVersionBlock.objects.unblock(document=self.document)
super(DocumentCheckout, self).delete(*args, **kwargs)
# TODO: enclose in transaction
NewVersionBlock.objects.unblock(self.document)
super(DocumentCheckout, self).delete(*args, **kwargs)
def get_absolute_url(self):
return reverse(
viewname='checkout:checkout_info',
kwargs={'document_id': self.document.pk}
)
return reverse('checkout:checkout_info', args=(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(
_('Document already 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
)
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
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_check_in = namespace.add_permission(
label=_('Check in documents'), name='checkin_document'
permission_document_checkin = namespace.add_permission(
name='checkin_document', label=_('Check in documents')
)
permission_document_check_in_override = namespace.add_permission(
label=_('Forcefully check in documents'), name='checkin_document_override'
permission_document_checkin_override = namespace.add_permission(
name='checkin_document_override', label=_('Forcefully check in documents')
)
permission_document_checkout = namespace.add_permission(
label=_('Check out documents'), name='checkout_document'
name='checkout_document', label=_('Check out documents')
)
permission_document_checkout_detail_view = namespace.add_permission(
label=_('Check out details view'), name='checkout_detail_view'
name='checkout_detail_view', label=_('Check out details 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(
label=_('Checkouts periodic'), name='checkouts_periodic', transient=True
name='checkouts_periodic', label=_('Checkouts periodic'), transient=True
)
queue_checkouts_periodic.add_task_type(
label=_('Check expired checkouts'),
name='mayan.apps.task_check_expired_check_outs'
name='mayan.apps.task_check_expired_check_outs',
label=_('Check expired checkouts')
)

View File

@@ -42,8 +42,8 @@ class NewDocumentCheckoutSerializer(serializers.ModelSerializer):
document = Document.objects.get(pk=validated_data.pop('document_pk'))
AccessControlList.objects.check_access(
obj=document, permissions=permission_document_checkout,
user=self.context['request'].user
permissions=permission_document_checkout,
user=self.context['request'].user, obj=document
)
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',
kwargs={'document_pk': self.checkout.pk}
args=(self.checkout.pk,)
)
def _checkout_document(self):
@@ -44,8 +44,7 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
def test_checkedout_document_view_with_checkout_access(self):
self._checkout_document()
self.grant_access(
obj=self.document,
permission=permission_document_checkout_detail_view
permission=permission_document_checkout_detail_view, obj=self.document
)
response = self._request_checkedout_document_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
@@ -53,7 +52,7 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
def test_checkedout_document_view_with_document_access(self):
self._checkout_document()
self.grant_access(
obj=self.document, permission=permission_document_view
permission=permission_document_view, obj=self.document
)
response = self._request_checkedout_document_view()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
@@ -61,17 +60,14 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
def test_checkedout_document_view_with_access(self):
self._checkout_document()
self.grant_access(
obj=self.document, permission=permission_document_view
permission=permission_document_view, obj=self.document
)
self.grant_access(
obj=self.document,
permission=permission_document_checkout_detail_view
permission=permission_document_checkout_detail_view, obj=self.document
)
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(
@@ -87,9 +83,7 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
self.assertEqual(DocumentCheckout.objects.count(), 0)
def test_document_checkout_with_access(self):
self.grant_access(
obj=self.document, permission=permission_document_checkout
)
self.grant_access(permission=permission_document_checkout, obj=self.document)
response = self._request_document_checkout_view()
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(
@@ -108,7 +102,7 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
def test_checkout_list_view_with_document_access(self):
self._checkout_document()
self.grant_access(
obj=self.document, permission=permission_document_view
permission=permission_document_view, obj=self.document
)
response = self._request_checkout_list_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -117,8 +111,7 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
def test_checkout_list_view_with_checkout_access(self):
self._checkout_document()
self.grant_access(
obj=self.document,
permission=permission_document_checkout_detail_view
permission=permission_document_checkout_detail_view, obj=self.document
)
response = self._request_checkout_list_view()
self.assertEqual(response.status_code, status.HTTP_200_OK)
@@ -127,11 +120,10 @@ class CheckoutsAPITestCase(DocumentTestMixin, BaseAPITestCase):
def test_checkout_list_view_with_access(self):
self._checkout_document()
self.grant_access(
obj=self.document, permission=permission_document_view
permission=permission_document_view, obj=self.document
)
self.grant_access(
obj=self.document,
permission=permission_document_checkout_detail_view
permission=permission_document_checkout_detail_view, obj=self.document
)
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._test_case_user, block_new_version=True
user=self.admin_user, block_new_version=True
)
self.assertTrue(self.document.is_checked_out())
@@ -33,12 +33,29 @@ 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._test_case_user, block_new_version=True
user=self.admin_user, block_new_version=True
)
self.document.check_in()
@@ -55,13 +72,13 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase):
DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime,
user=self._test_case_user, block_new_version=True
user=self.admin_user, block_new_version=True
)
with self.assertRaises(DocumentAlreadyCheckedOut):
DocumentCheckout.objects.checkout_document(
document=self.document,
expiration_datetime=expiration_datetime, user=self._test_case_user,
expiration_datetime=expiration_datetime, user=self.admin_user,
block_new_version=True
)
@@ -74,7 +91,7 @@ class DocumentCheckoutTestCase(DocumentTestMixin, BaseTestCase):
DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime,
user=self._test_case_user, block_new_version=True
user=self.admin_user, block_new_version=True
)
time.sleep(.11)
@@ -83,6 +100,18 @@ 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):
@@ -112,32 +141,3 @@ 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,54 +8,61 @@ 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 ..literals import STATE_CHECKED_OUT, STATE_LABELS
from ..models import DocumentCheckout
from ..permissions import (
permission_document_check_in, permission_document_check_in_override,
permission_document_checkin, permission_document_checkin_override,
permission_document_checkout, permission_document_checkout_detail_view
)
class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
create_test_case_superuser = True
def _request_document_check_in_view(self):
return self.post(
viewname='checkouts:checkin_document', args=(self.document.pk,),
)
def test_checkin_document_view_no_permission(self):
self.login_user()
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
user=self.user, block_new_version=True
)
self.assertTrue(self.document.is_checked_out())
def _request_document_check_in_view(self):
return self.post(
viewname='checkouts:document_check_in',
kwargs={'document_id': self.document.pk}
)
def test_document_check_in_view_no_permission(self):
self._checkout_document()
response = self._request_document_check_in_view()
self.assertEquals(response.status_code, 404)
self.assertEquals(response.status_code, 403)
self.assertTrue(self.document.is_checked_out())
def test_document_check_in_view_with_access(self):
self._checkout_document()
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())
self.grant_access(
obj=self.document, permission=permission_document_check_in
obj=self.document, permission=permission_document_checkin
)
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(
@@ -65,8 +72,7 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
def _request_document_checkout_view(self):
return self.post(
viewname='checkouts:document_checkout',
kwargs={'document_id': self.document.pk},
viewname='checkouts:checkout_document', args=(self.document.pk,),
data={
'expiration_datetime_0': 2,
'expiration_datetime_1': TIME_DELTA_UNIT_DAYS,
@@ -75,11 +81,14 @@ 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, 404)
self.assertEquals(response.status_code, 403)
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
)
@@ -87,37 +96,11 @@ 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 _request_checkout_detail_view(self):
return self.get(
viewname='checkouts:checkout_info', args=(self.document.pk,),
)
def test_checkout_detail_view_no_permission(self):
self._checkout_document()
response = self._request_checkout_detail_view()
self.assertNotContains(
response, text=STATE_LABELS[STATE_CHECKED_OUT], status_code=403
)
def test_checkout_detail_view_with_access(self):
self._checkout_document()
self.grant_access(
obj=self.document,
permission=permission_document_checkout_detail_view
)
response = self._request_checkout_detail_view()
self.assertContains(response, text=STATE_LABELS[STATE_CHECKED_OUT], status_code=200)
def test_document_new_version_after_checkout(self):
"""
Gitlab issue #231
@@ -128,23 +111,31 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
- Link to upload version view should not resolve
- Upload version view should reject request
"""
self.login_superuser()
self.login(
username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD
)
self._checkout_document()
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())
response = self.post(
viewname='sources:upload_version',
kwargs={'document_id': self.document.pk},
'sources:upload_version', args=(self.document.pk,),
follow=True
)
self.assertContains(
response, text='blocked from uploading',
status_code=200
)
response = self.get(
viewname='documents:document_version_list',
kwargs={'document_id': self.document.pk},
'documents:document_version_list', args=(self.document.pk,),
follow=True
)
@@ -168,22 +159,26 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
DocumentCheckout.objects.checkout_document(
document=self.document, expiration_datetime=expiration_datetime,
user=self._test_case_superuser, block_new_version=True
user=self.admin_user, block_new_version=True
)
self.assertTrue(self.document.is_checked_out())
self.grant_access(
obj=self.document, permission=permission_document_check_in
self.login(
username=TEST_USER_USERNAME, password=TEST_USER_PASSWORD
)
self.grant_access(
obj=self.document, permission=permission_document_checkout
self.role.permissions.add(
permission_document_checkin.stored_permission
)
self.role.permissions.add(
permission_document_checkout.stored_permission
)
response = self.post(
viewname='checkouts:document_check_in',
kwargs={'document_id': self.document.pk},
follow=True
'checkouts:checkin_document', args=(self.document.pk,), follow=True
)
self.assertContains(
response, text='Insufficient permissions', status_code=403
)
@@ -191,21 +186,33 @@ class DocumentCheckoutViewTestCase(GenericDocumentViewTestCase):
self.assertTrue(self.document.is_checked_out())
def test_forcefull_check_in_document_view_with_permission(self):
self._checkout_document()
expiration_datetime = now() + datetime.timedelta(days=1)
self.grant_access(
obj=self.document, permission=permission_document_check_in
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_override
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_checkout_detail_view
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
)
response = self.post(
viewname='checkouts:document_check_in',
kwargs={'document_id': self.document.pk},
follow=True
'checkouts:checkin_document', args=(self.document.pk,), follow=True
)
self.assertContains(

View File

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

View File

@@ -1,154 +1,85 @@
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 _, ungettext
from django.utils.translation import ugettext_lazy as _
from mayan.apps.acls.models import AccessControlList
from mayan.apps.common.generics import (
MultipleObjectConfirmActionView, MultipleObjectFormActionView,
SingleObjectDetailView
ConfirmView, SingleObjectCreateView, 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_check_in, permission_document_checkout,
permission_document_checkout_detail_view
permission_document_checkin, permission_document_checkin_override,
permission_document_checkout, permission_document_checkout_detail_view
)
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 get_extra_context(self):
queryset = self.get_object_list()
result = {
'title': ungettext(
singular='Check in %(count)d document',
plural='Check in %(count)d documents',
number=queryset.count()
) % {
'count': queryset.count(),
}
}
if queryset.count() == 1:
result.update(
{
'object': queryset.first(),
'title': _(
'Check in document: %s'
) % queryset.first()
}
)
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(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'
class CheckoutDocumentView(SingleObjectCreateView):
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()
def dispatch(self, request, *args, **kwargs):
self.document = get_object_or_404(klass=Document, pk=self.kwargs['pk'])
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 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,
AccessControlList.objects.check_access(
permissions=permission_document_checkout, user=request.user,
obj=self.document
)
return super(
CheckoutDocumentView, self
).dispatch(request, *args, **kwargs)
class DocumentCheckoutDetailView(SingleObjectDetailView):
form_class = DocumentCheckoutDefailForm
model = Document
object_permission = permission_document_checkout_detail_view
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
)
else:
messages.success(
self.request,
_('Document "%s" checked out successfully.') % self.document
)
return HttpResponseRedirect(self.get_success_url())
def get_extra_context(self):
return {
'object': self.get_object(),
'title': _(
'Check out details for document: %s'
) % self.get_object()
'object': self.document,
'title': _('Check out document: %s') % self.document
}
def get_object(self):
return get_object_or_404(klass=Document, pk=self.kwargs['document_id'])
def get_post_action_redirect(self):
return reverse('checkouts:checkout_info', args=(self.document.pk,))
class DocumentCheckoutListView(DocumentListView):
class CheckoutListView(DocumentListView):
def get_document_queryset(self):
return AccessControlList.objects.restrict_queryset(
return AccessControlList.objects.filter_by_access(
permission=permission_document_checkout_detail_view,
queryset=DocumentCheckout.objects.checked_out_documents(),
user=self.request.user
user=self.request.user,
queryset=DocumentCheckout.objects.checked_out_documents()
)
def get_extra_context(self):
context = super(DocumentCheckoutListView, self).get_extra_context()
context = super(CheckoutListView, self).get_extra_context()
context.update(
{
'extra_columns': (
@@ -182,3 +113,76 @@ class DocumentCheckoutListView(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 viewsets
from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
from .classes import Template
from .serializers import ContentTypeSerializer, TemplateSerializer
class ContentTypeAPIViewSet(viewsets.ReadOnlyModelViewSet):
class APIContentTypeList(generics.ListAPIView):
"""
list:
Return a list of all the available content types.
retrieve:
Return the given content type details.
Returns a list of all the available content types.
"""
lookup_url_kwarg = 'content_type_id'
queryset = ContentType.objects.order_by('app_label', 'model')
serializer_class = ContentTypeSerializer
queryset = ContentType.objects.order_by('app_label', 'model')
class TemplateAPIViewSet(viewsets.ReadOnlyModelViewSet):
class APITemplateListView(generics.ListAPIView):
"""
list:
Return a list of partial templates.
retrieve:
Return the given partial template details.
Returns a list of partial templates.
get: Returns a list of partial templates.
"""
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
)
permission_classes = (IsAuthenticated,)
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,8 +4,6 @@ import logging
import os
import warnings
from datetime import timedelta
import sys
import traceback
from kombu import Exchange, Queue
@@ -43,7 +41,6 @@ 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__)
@@ -77,8 +74,6 @@ 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
@@ -93,9 +88,7 @@ class CommonApp(MayanAppConfig):
def ready(self):
super(CommonApp, self).ready()
if check_for_sqlite():
warnings.warn(
category=DatabaseWarning, message=force_text(MESSAGE_SQLITE_WARNING)
)
warnings.warn(force_text(MESSAGE_SQLITE_WARNING))
Template(
name='menu_main', template_name='appearance/menu_main.html'

View File

@@ -72,6 +72,16 @@ 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 = []
@@ -292,7 +302,7 @@ class Template(object):
def get_absolute_url(self):
return reverse(
viewname='rest_api:template-detail', kwargs={'template_name': self.name}
viewname='rest_api:template-detail', kwargs={'template_pk': self.name}
)
def render(self, request):
@@ -305,10 +315,10 @@ class Template(object):
context=context,
).render()
# Calculate the hash of the bytes version but return the unicode
# version
self.html = result.rendered_content.replace('\n', '')
self.hex_hash = hashlib.sha256(result.content).hexdigest()
content = result.rendered_content.replace('\n', '')
self.html = content
self.hex_hash = hashlib.sha256(content).hexdigest()
return self

View File

@@ -4,7 +4,6 @@ import os
from django import forms
from django.conf import settings
from django.contrib.admin.utils import label_for_field
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.utils.module_loading import import_string
@@ -14,7 +13,7 @@ from mayan.apps.acls.models import AccessControlList
from .classes import Package
from .models import UserLocaleProfile
from .utils import introspect_attribute, resolve_attribute
from .utils import resolve_attribute
from .widgets import DisableableSelectWidget, PlainWidget, TextAreaDiv
@@ -39,94 +38,29 @@ class ChoiceForm(forms.Form):
}
)
selection = forms.MultipleChoiceField(
required=False, widget=DisableableSelectWidget()
)
class FormOptions(object):
def __init__(self, form, kwargs, options=None):
"""
Option definitions will be iterated. The option value will be
determined in the following order: as passed via keyword
arguments during form intialization, as form get_... method or
finally as static Meta options. This is to allow a form with
Meta options or method to be overrided at initialization
and increase the usability of a single class.
"""
for option_definition in self.option_definitions:
name = option_definition.keys()[0]
default_value = option_definition.values()[0]
try:
# Check for a runtime value via kwargs
value = kwargs.pop(name)
except KeyError:
try:
# Check if there is a get_... method
value = getattr(self, 'get_{}'.format(name))()
except AttributeError:
try:
# Check the meta class options
value = getattr(options, name)
except AttributeError:
value = default_value
setattr(self, name, value)
class DetailFormOption(FormOptions):
# Dictionary list of option names and default values
option_definitions = (
{'extra_fields': []},
)
selection = forms.MultipleChoiceField(widget=DisableableSelectWidget())
class DetailForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.opts = DetailFormOption(
form=self, kwargs=kwargs, options=getattr(self, 'Meta', None)
)
self.extra_fields = kwargs.pop('extra_fields', ())
super(DetailForm, self).__init__(*args, **kwargs)
for extra_field in self.opts.extra_fields:
obj = extra_field.get('object', self.instance)
field = extra_field['field']
result = resolve_attribute(
attribute=field, obj=obj
)
label = extra_field.get('label', None)
if not label:
attribute_name, obj = introspect_attribute(
attribute_name=field, obj=obj
)
if not obj:
label = _('None')
else:
try:
label = getattr(
getattr(obj, attribute_name), 'short_description'
)
except AttributeError:
label = label_for_field(
name=attribute_name, model=obj
)
for extra_field in self.extra_fields:
result = resolve_attribute(obj=self.instance, attribute=extra_field['field'])
label = 'label' in extra_field and extra_field['label'] or None
# TODO: Add others result types <=> Field types
if isinstance(result, models.query.QuerySet):
self.fields[field] = forms.ModelMultipleChoiceField(
queryset=result, label=label
)
self.fields[extra_field['field']] = \
forms.ModelMultipleChoiceField(
queryset=result, label=label)
else:
self.fields[field] = forms.CharField(
self.fields[extra_field['field']] = forms.CharField(
label=extra_field['label'],
initial=resolve_attribute(
obj=obj,
attribute=field
), label=label,
obj=self.instance,
attribute=extra_field['field']
),
widget=extra_field.get('widget', PlainWidget)
)
@@ -197,7 +131,7 @@ class FileDisplayForm(forms.Form):
self.fields['text'].initial = file_object.read()
class FilteredSelectionFormOptions(FormOptions):
class FilteredSelectionFormOptions(object):
# Dictionary list of option names and default values
option_definitions = (
{'allow_multiple': False},
@@ -207,12 +141,40 @@ class FilteredSelectionFormOptions(FormOptions):
{'model': None},
{'permission': None},
{'queryset': None},
{'required': True},
{'user': None},
{'widget_class': None},
{'widget_attributes': {'size': '10'}},
)
def __init__(self, form, kwargs, options=None):
"""
Option definitions will be iterated. The option value will be
determined in the following order: as passed via keyword
arguments during form intialization, as form get_... method or
finally as static Meta options. This is to allow a form with
Meta options or method to be overrided at initialization
and increase the usability of a single class.
"""
for option_definition in self.option_definitions:
name = option_definition.keys()[0]
default_value = option_definition.values()[0]
try:
# Check for a runtime value via kwargs
value = kwargs.pop(name)
except KeyError:
try:
# Check if there is a get_... method
value = getattr(self, 'get_{}'.format(name))()
except AttributeError:
try:
# Check the meta class options
value = getattr(options, name)
except AttributeError:
value = default_value
setattr(self, name, value)
class FilteredSelectionForm(forms.Form):
"""
@@ -229,7 +191,7 @@ class FilteredSelectionForm(forms.Form):
raise ImproperlyConfigured(
'{} requires a queryset or a model to be specified as '
'a meta option or passed during initialization.'.format(
self.__class__.__name__
self.__class__
)
)
@@ -249,17 +211,17 @@ class FilteredSelectionForm(forms.Form):
else:
widget_class = opts.widget_class
super(FilteredSelectionForm, self).__init__(*args, **kwargs)
if opts.permission:
queryset = AccessControlList.objects.restrict_queryset(
queryset = AccessControlList.objects.filter_by_access(
permission=opts.permission, queryset=queryset,
user=opts.user
)
super(FilteredSelectionForm, self).__init__(*args, **kwargs)
self.fields[opts.field_name] = field_class(
help_text=opts.help_text, label=opts.label,
queryset=queryset, required=opts.required,
queryset=queryset, required=True,
widget=widget_class(attrs=opts.widget_attributes),
**extra_kwargs
)

File diff suppressed because it is too large Load Diff

View File

@@ -3,12 +3,6 @@ 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')
@@ -49,12 +43,6 @@ 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'),
permission=permission_error_log_view, text=_('Errors'),
permissions=(permission_error_log_view,), text=_('Errors'),
view='common:object_error_list',
)
link_object_error_list_clear = Link(
kwargs=get_kwargs_factory('resolved_object'),
permission=permission_error_log_view, text=_('Clear all'),
permissions=(permission_error_log_view,), text=_('Clear all'),
view='common:object_error_list_clear',
)
link_forum = Link(

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