Compare commits
1 Commits
versions/n
...
feature/er
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78b9df972c |
44
HISTORY.rst
44
HISTORY.rst
@@ -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)
|
||||
|
||||
5
Makefile
5
Makefile
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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:
|
||||
-----------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
====================
|
||||
=====================
|
||||
MERC 2: Test writing
|
||||
====================
|
||||
=====================
|
||||
|
||||
:MERC: 2
|
||||
:Author: Michael Price
|
||||
|
||||
@@ -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()
|
||||
),
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
@@ -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
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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')
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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})
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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}
|
||||
)
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
DEFAULT_MAXIMUM_TITLE_LENGTH = 120
|
||||
DEFAULT_MAXIMUM_TITLE_LENGTH = 80
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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' %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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=_(
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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'
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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.')
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'})
|
||||
)
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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')
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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']
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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.'
|
||||
) % {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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'
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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')
|
||||
)
|
||||
|
||||
@@ -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')
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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'
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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'
|
||||
)
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user