Compare commits
43 Commits
master
...
developmen
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25854a0a1c | ||
|
|
6006fc9b74 | ||
|
|
57baf76a50 | ||
|
|
8385ef13b0 | ||
|
|
dd1e4cb685 | ||
|
|
6dcfd1b861 | ||
|
|
d867614c1c | ||
|
|
6cc574e631 | ||
|
|
5b94d202dc | ||
|
|
3e7d3946f8 | ||
|
|
2cbb1bc9e1 | ||
|
|
f0505db3b7 | ||
|
|
39995764eb | ||
|
|
d0aad4cb35 | ||
|
|
7abf8a7fc0 | ||
|
|
fc0127c7ad | ||
|
|
bbef5fd570 | ||
|
|
7a492b28f3 | ||
|
|
abd720bf10 | ||
|
|
25c1391649 | ||
|
|
95c0be45fb | ||
|
|
944ddd682b | ||
|
|
c6bf3b00cd | ||
|
|
bd419dc943 | ||
|
|
70e2ca3334 | ||
|
|
5dd88ad690 | ||
|
|
ae57b97a1b | ||
|
|
cb15e40f70 | ||
|
|
01420c42dd | ||
|
|
20e3634f5a | ||
|
|
48fc36d54e | ||
|
|
7a29b2496b | ||
|
|
c0407652c0 | ||
|
|
5083a2d261 | ||
|
|
ba1ab5d4f1 | ||
|
|
17b7d6f25e | ||
|
|
ca1f674d78 | ||
|
|
93aeb8cffe | ||
|
|
8eeee5b5a4 | ||
|
|
6221187070 | ||
|
|
acdc7dca48 | ||
|
|
105eab0740 | ||
|
|
b91f7f685a |
@@ -1,3 +1,8 @@
|
|||||||
|
3.0 (2017-XX-XX)
|
||||||
|
================
|
||||||
|
- Add support for notifications. GitLab #262.
|
||||||
|
- Add quota support. GitLab #284.
|
||||||
|
|
||||||
2.7.2 (2017-09-06)
|
2.7.2 (2017-09-06)
|
||||||
==================
|
==================
|
||||||
- Fix new mailer creation view. GitLab issue #431.
|
- Fix new mailer creation view. GitLab issue #431.
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ APP_LIST = (
|
|||||||
'converter', 'django_gpg', 'document_comments', 'document_indexing',
|
'converter', 'django_gpg', 'document_comments', 'document_indexing',
|
||||||
'document_parsing', 'document_signatures', 'document_states', 'documents',
|
'document_parsing', 'document_signatures', 'document_states', 'documents',
|
||||||
'dynamic_search', 'events', 'linking', 'lock_manager', 'mayan_statistics',
|
'dynamic_search', 'events', 'linking', 'lock_manager', 'mayan_statistics',
|
||||||
'mailer', 'metadata', 'mirroring', 'motd', 'navigation', 'ocr', 'permissions',
|
'mailer', 'metadata', 'mirroring', 'motd', 'navigation', 'ocr',
|
||||||
'rest_api', 'smart_settings', 'sources', 'storage', 'tags', 'task_manager',
|
'permissions', 'rest_api', 'smart_settings', 'sources', 'storage', 'tags',
|
||||||
'user_management'
|
'task_manager', 'user_management'
|
||||||
)
|
)
|
||||||
|
|
||||||
LANGUAGE_LIST = (
|
LANGUAGE_LIST = (
|
||||||
|
|||||||
79
docs/releases/3.0.rst
Normal file
79
docs/releases/3.0.rst
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
=============================
|
||||||
|
Mayan EDMS v3.0 release notes
|
||||||
|
=============================
|
||||||
|
|
||||||
|
Released: XX XX, 2017
|
||||||
|
|
||||||
|
What's new
|
||||||
|
==========
|
||||||
|
|
||||||
|
|
||||||
|
Other changes
|
||||||
|
-------------
|
||||||
|
- Resource based API endpoints
|
||||||
|
- Update required versions of Pillow to 4.1.1, django-compressor to 2.1.1, django-suit to 0.2.25, pyocr to 0.4.7, sh to 1.12.13
|
||||||
|
- Add notifications support.
|
||||||
|
|
||||||
|
Removals
|
||||||
|
--------
|
||||||
|
- None
|
||||||
|
|
||||||
|
Upgrading from a previous version
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
If installed via PIP
|
||||||
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Type in the console::
|
||||||
|
|
||||||
|
$ pip install -U mayan-edms
|
||||||
|
|
||||||
|
the requirements will also be updated automatically.
|
||||||
|
|
||||||
|
If installed using Git
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
If you installed Mayan EDMS by cloning the Git repository issue the commands::
|
||||||
|
|
||||||
|
$ git reset --hard HEAD
|
||||||
|
$ git pull
|
||||||
|
|
||||||
|
otherwise download the compressed archived and uncompress it overriding the
|
||||||
|
existing installation.
|
||||||
|
|
||||||
|
Manually upgrade/add the new requirements::
|
||||||
|
|
||||||
|
$ pip install --upgrade -r requirements.txt
|
||||||
|
|
||||||
|
Remove deprecated requirements::
|
||||||
|
|
||||||
|
$ pip uninstall -y -r removals.txt
|
||||||
|
|
||||||
|
Common steps
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Migrate existing database schema with::
|
||||||
|
|
||||||
|
$ mayan-edms.py performupgrade
|
||||||
|
|
||||||
|
Add new static media::
|
||||||
|
|
||||||
|
$ mayan-edms.py collectstatic --noinput
|
||||||
|
|
||||||
|
The upgrade procedure is now complete.
|
||||||
|
|
||||||
|
|
||||||
|
Backward incompatible changes
|
||||||
|
=============================
|
||||||
|
|
||||||
|
* None
|
||||||
|
|
||||||
|
Bugs fixed or issues closed
|
||||||
|
===========================
|
||||||
|
|
||||||
|
* `GitLab issue #366 <https://gitlab.com/mayan-edms/mayan-edms/issues/366>`_ Proofread documentation
|
||||||
|
* `GitLab issue #379 <https://gitlab.com/mayan-edms/mayan-edms/issues/379>`_ Add new document version list view permission.
|
||||||
|
* `GitLab issue #379 <https://gitlab.com/mayan-edms/mayan-edms/issues/379>`_ Add new document version list view permission.
|
||||||
|
|
||||||
|
|
||||||
|
.. _PyPI: https://pypi.python.org/pypi/mayan-edms/
|
||||||
@@ -54,6 +54,12 @@ Whenever possible, but don't obsess over things like line length:
|
|||||||
|
|
||||||
$ flake8 --ignore=E501,E128,E122 |less
|
$ flake8 --ignore=E501,E128,E122 |less
|
||||||
|
|
||||||
|
To perform automatic PEP8 checks, install flake8's git hook using:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ flake8 --install-hook git
|
||||||
|
|
||||||
Imports
|
Imports
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
@@ -245,47 +251,6 @@ Steps to deploy a development version
|
|||||||
$ ./manage.py runserver
|
$ ./manage.py runserver
|
||||||
|
|
||||||
|
|
||||||
Setting up a development version using Vagrant
|
|
||||||
----------------------------------------------
|
|
||||||
Make sure you have Vagrant and a provider properly installed as per
|
|
||||||
https://docs.vagrantup.com/v2/installation/index.html
|
|
||||||
|
|
||||||
Start and provision a machine using:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ vagrant up development
|
|
||||||
|
|
||||||
To launch a standalone development server
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ vagrant ssh
|
|
||||||
$ vagrant@vagrant-ubuntu-trusty-32:~$ cd ~/mayan-edms/
|
|
||||||
$ vagrant@vagrant-ubuntu-trusty-32:~$ source venv/bin/activate
|
|
||||||
$ vagrant@vagrant-ubuntu-trusty-32:~$ ./manage.py runserver 0.0.0.0:8000
|
|
||||||
|
|
||||||
To launch a development server with a celery worker and Redis as broker
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ vagrant ssh
|
|
||||||
$ vagrant@vagrant-ubuntu-trusty-32:~$ cd ~/mayan-edms/
|
|
||||||
$ vagrant@vagrant-ubuntu-trusty-32:~$ source venv/bin/activate
|
|
||||||
$ vagrant@vagrant-ubuntu-trusty-32:~$ ./manage.py runserver 0.0.0.0:8000 --settings=mayan.settings.celery_redis
|
|
||||||
|
|
||||||
Then on a separate console launch a celery worker from the same provisioned Vagrant machine:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ vagrant ssh
|
|
||||||
$ vagrant@vagrant-ubuntu-trusty-32:~$ cd ~/mayan-edms/
|
|
||||||
$ vagrant@vagrant-ubuntu-trusty-32:~$ source venv/bin/activate
|
|
||||||
$ vagrant@vagrant-ubuntu-trusty-32:~$ DJANGO_SETTINGS_MODULE='mayan.settings.celery_redis' celery -A mayan worker -l DEBUG -Q checkouts,mailing,uploads,converter,ocr,tools,indexing,metadata -Ofair -B
|
|
||||||
|
|
||||||
|
|
||||||
Contributing changes
|
Contributing changes
|
||||||
--------------------
|
--------------------
|
||||||
Once your have created and committed some new code or feature, submit a Pull
|
Once your have created and committed some new code or feature, submit a Pull
|
||||||
|
|||||||
@@ -92,8 +92,10 @@ class AccessControlListManager(models.Manager):
|
|||||||
|
|
||||||
def filter_by_access(self, permission, user, queryset):
|
def filter_by_access(self, permission, user, queryset):
|
||||||
if user.is_superuser or user.is_staff:
|
if user.is_superuser or user.is_staff:
|
||||||
logger.debug('Unfiltered queryset returned to user "%s" as superuser or staff',
|
logger.debug(
|
||||||
user)
|
'Unfiltered queryset returned to user "%s" as superuser '
|
||||||
|
'or staff', user
|
||||||
|
)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -28,19 +28,19 @@ urlpatterns = [
|
|||||||
|
|
||||||
api_urls = [
|
api_urls = [
|
||||||
url(
|
url(
|
||||||
r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/$',
|
r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/$',
|
||||||
APIObjectACLListView.as_view(), name='accesscontrollist-list'
|
APIObjectACLListView.as_view(), name='accesscontrollist-list'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/(?P<pk>\d+)/$',
|
r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/(?P<pk>\d+)/$',
|
||||||
APIObjectACLView.as_view(), name='accesscontrollist-detail'
|
APIObjectACLView.as_view(), name='accesscontrollist-detail'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/(?P<pk>\d+)/permissions/$',
|
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'
|
APIObjectACLPermissionListView.as_view(), name='accesscontrollist-permission-list'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/(?P<pk>\d+)/permissions/(?P<permission_pk>\d+)/$',
|
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'
|
APIObjectACLPermissionView.as_view(), name='accesscontrollist-permission-detail'
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import logging
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from common import MayanAppConfig, menu_user
|
from common import MayanAppConfig, menu_user
|
||||||
|
from navigation.classes import Separator, Text
|
||||||
|
|
||||||
from .links import link_logout, link_password_change
|
from .links import link_logout, link_password_change
|
||||||
|
|
||||||
@@ -21,6 +22,6 @@ class AuthenticationApp(MayanAppConfig):
|
|||||||
|
|
||||||
menu_user.bind_links(
|
menu_user.bind_links(
|
||||||
links=(
|
links=(
|
||||||
link_password_change, link_logout
|
Separator(), link_password_change, link_logout
|
||||||
), position=99
|
), position=99
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from events.classes import Event
|
from events.classes import EventTypeNamespace
|
||||||
|
|
||||||
event_cabinets_add_document = Event(
|
namespace = EventTypeNamespace(name='cabinets', label=_('Cabinets'))
|
||||||
name='cabinets_add_document',
|
|
||||||
label=_('Document added to cabinet')
|
event_cabinets_add_document = namespace.add_event_type(
|
||||||
|
label=_('Document added to cabinet'),
|
||||||
|
name='add_document'
|
||||||
)
|
)
|
||||||
event_cabinets_remove_document = Event(
|
event_cabinets_remove_document = namespace.add_event_type(
|
||||||
name='cabinets_remove_document',
|
label=_('Document removed from cabinet'),
|
||||||
label=_('Document removed from cabinet')
|
name='remove_document'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class CabinetsEventsTestCase(GenericDocumentTestCase):
|
|||||||
self.assertEqual(Action.objects.last().target, self.document)
|
self.assertEqual(Action.objects.last().target, self.document)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
Action.objects.last().verb,
|
Action.objects.last().verb,
|
||||||
event_cabinets_add_document.name
|
event_cabinets_add_document.id
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_document_cabinet_remove_event(self):
|
def test_document_cabinet_remove_event(self):
|
||||||
@@ -38,5 +38,5 @@ class CabinetsEventsTestCase(GenericDocumentTestCase):
|
|||||||
self.assertEqual(Action.objects.first().target, self.document)
|
self.assertEqual(Action.objects.first().target, self.document)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
Action.objects.first().verb,
|
Action.objects.first().verb,
|
||||||
event_cabinets_remove_document.name
|
event_cabinets_remove_document.id
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,13 +9,11 @@ from rest_framework import generics, status
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
from acls.models import AccessControlList
|
from acls.models import AccessControlList
|
||||||
from documents.models import Document
|
|
||||||
from documents.permissions import permission_document_view
|
from documents.permissions import permission_document_view
|
||||||
|
|
||||||
from .models import DocumentCheckout
|
from .models import DocumentCheckout
|
||||||
from .permissions import (
|
from .permissions import (
|
||||||
permission_document_checkout, permission_document_checkin,
|
permission_document_checkin, permission_document_checkin_override
|
||||||
permission_document_checkin_override
|
|
||||||
)
|
)
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
DocumentCheckoutSerializer, NewDocumentCheckoutSerializer
|
DocumentCheckoutSerializer, NewDocumentCheckoutSerializer
|
||||||
@@ -48,12 +46,23 @@ class APICheckedoutDocumentListView(generics.ListCreateAPIView):
|
|||||||
APICheckedoutDocumentListView, self
|
APICheckedoutDocumentListView, self
|
||||||
).get(request, *args, **kwargs)
|
).get(request, *args, **kwargs)
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
"""
|
||||||
|
Extra context provided to the serializer class.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'format': self.format_kwarg,
|
||||||
|
'request': self.request,
|
||||||
|
'view': self
|
||||||
|
}
|
||||||
|
|
||||||
|
'''
|
||||||
def post(self, request, *args, **kwargs):
|
def post(self, request, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Checkout a document.
|
Checkout a document.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
serializer = self.get_serializer(data=request.DATA, files=request.FILES)
|
serializer = self.get_serializer(data=request.data, files=request.file)
|
||||||
|
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
document = get_object_or_404(
|
document = get_object_or_404(
|
||||||
@@ -84,6 +93,7 @@ class APICheckedoutDocumentListView(generics.ListCreateAPIView):
|
|||||||
return Response(status=status.HTTP_201_CREATED)
|
return Response(status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
class APICheckedoutDocumentView(generics.RetrieveDestroyAPIView):
|
class APICheckedoutDocumentView(generics.RetrieveDestroyAPIView):
|
||||||
|
|||||||
@@ -11,10 +11,15 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
from acls import ModelPermission
|
from acls import ModelPermission
|
||||||
from common import MayanAppConfig, menu_facet, menu_main, menu_sidebar
|
from common import MayanAppConfig, menu_facet, menu_main, menu_sidebar
|
||||||
from common.dashboards import dashboard_main
|
from common.dashboards import dashboard_main
|
||||||
|
from events import ModelEventType
|
||||||
from mayan.celery import app
|
from mayan.celery import app
|
||||||
from rest_api.classes import APIEndPoint
|
from rest_api.classes import APIEndPoint
|
||||||
|
|
||||||
from .dashboard_widgets import widget_checkouts
|
from .dashboard_widgets import widget_checkouts
|
||||||
|
from .events import (
|
||||||
|
event_document_auto_check_in, event_document_check_in,
|
||||||
|
event_document_check_out, event_document_forceful_check_in
|
||||||
|
)
|
||||||
from .handlers import check_new_version_creation
|
from .handlers import check_new_version_creation
|
||||||
from .links import (
|
from .links import (
|
||||||
link_checkin_document, link_checkout_document, link_checkout_info,
|
link_checkin_document, link_checkout_document, link_checkout_info,
|
||||||
@@ -51,7 +56,10 @@ class CheckoutsApp(MayanAppConfig):
|
|||||||
|
|
||||||
Document.add_to_class(
|
Document.add_to_class(
|
||||||
'check_in',
|
'check_in',
|
||||||
lambda document, user=None: DocumentCheckout.objects.check_in_document(document, user)
|
lambda document,
|
||||||
|
user=None: DocumentCheckout.objects.check_in_document(
|
||||||
|
document, user
|
||||||
|
)
|
||||||
)
|
)
|
||||||
Document.add_to_class(
|
Document.add_to_class(
|
||||||
'checkout_info',
|
'checkout_info',
|
||||||
@@ -72,6 +80,13 @@ class CheckoutsApp(MayanAppConfig):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ModelEventType.register(
|
||||||
|
model=Document, event_types=(
|
||||||
|
event_document_auto_check_in, event_document_check_in,
|
||||||
|
event_document_check_out, event_document_forceful_check_in
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
ModelPermission.register(
|
ModelPermission.register(
|
||||||
model=Document, permissions=(
|
model=Document, permissions=(
|
||||||
permission_document_checkout,
|
permission_document_checkout,
|
||||||
|
|||||||
@@ -2,19 +2,21 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from events.classes import Event
|
from events import EventTypeNamespace
|
||||||
|
|
||||||
event_document_auto_check_in = Event(
|
namespace = EventTypeNamespace(name='checkouts', label=_('Checkouts'))
|
||||||
name='checkouts_document_auto_check_in',
|
|
||||||
|
event_document_auto_check_in = namespace.add_event_type(
|
||||||
|
name='document_auto_check_in',
|
||||||
label=_('Document automatically checked in')
|
label=_('Document automatically checked in')
|
||||||
)
|
)
|
||||||
event_document_check_in = Event(
|
event_document_check_in = namespace.add_event_type(
|
||||||
name='checkouts_document_check_in', label=_('Document checked in')
|
name='document_check_in', label=_('Document checked in')
|
||||||
)
|
)
|
||||||
event_document_check_out = Event(
|
event_document_check_out = namespace.add_event_type(
|
||||||
name='checkouts_document_check_out', label=_('Document checked out')
|
name='document_check_out', label=_('Document checked out')
|
||||||
)
|
)
|
||||||
event_document_forceful_check_in = Event(
|
event_document_forceful_check_in = namespace.add_event_type(
|
||||||
name='checkouts_document_forceful_check_in',
|
name='document_forceful_check_in',
|
||||||
label=_('Document forcefully checked in')
|
label=_('Document forcefully checked in')
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class DocumentCheckoutManager(models.Manager):
|
class DocumentCheckoutManager(models.Manager):
|
||||||
def checkout_document(self, document, expiration_datetime, user, block_new_version=True):
|
def checkout_document(self, document, expiration_datetime, user, block_new_version=True):
|
||||||
self.create(
|
return self.create(
|
||||||
document=document, expiration_datetime=expiration_datetime,
|
document=document, expiration_datetime=expiration_datetime,
|
||||||
user=user, block_new_version=block_new_version
|
user=user, block_new_version=block_new_version
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from acls.models import AccessControlList
|
||||||
|
from documents.models import Document
|
||||||
|
from documents.serializers import DocumentSerializer
|
||||||
|
|
||||||
from .models import DocumentCheckout
|
from .models import DocumentCheckout
|
||||||
|
from .permissions import permission_document_checkout
|
||||||
|
|
||||||
|
|
||||||
class DocumentCheckoutSerializer(serializers.ModelSerializer):
|
class DocumentCheckoutSerializer(serializers.ModelSerializer):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
# Hide this import otherwise strange circular import error occur
|
|
||||||
from documents.serializers import DocumentSerializer
|
|
||||||
|
|
||||||
super(DocumentCheckoutSerializer, self).__init__(*args, **kwargs)
|
super(DocumentCheckoutSerializer, self).__init__(*args, **kwargs)
|
||||||
self.fields['document'] = DocumentSerializer()
|
self.fields['document'] = DocumentSerializer()
|
||||||
|
|
||||||
@@ -17,7 +21,33 @@ class DocumentCheckoutSerializer(serializers.ModelSerializer):
|
|||||||
model = DocumentCheckout
|
model = DocumentCheckout
|
||||||
|
|
||||||
|
|
||||||
class NewDocumentCheckoutSerializer(serializers.Serializer):
|
class NewDocumentCheckoutSerializer(serializers.ModelSerializer):
|
||||||
document = serializers.IntegerField()
|
|
||||||
expiration_datetime = serializers.DateTimeField()
|
|
||||||
block_new_version = serializers.BooleanField()
|
block_new_version = serializers.BooleanField()
|
||||||
|
document_pk = serializers.IntegerField(
|
||||||
|
help_text=_('Primary key of the document to be checked out.'),
|
||||||
|
write_only=True
|
||||||
|
)
|
||||||
|
expiration_datetime = serializers.DateTimeField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
fields = (
|
||||||
|
'block_new_version', 'document', 'document_pk',
|
||||||
|
'expiration_datetime', 'id'
|
||||||
|
)
|
||||||
|
model = DocumentCheckout
|
||||||
|
read_only_fields = ('document',)
|
||||||
|
write_only_fields = ('document_pk',)
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
document = Document.objects.get(pk=validated_data.pop('document_pk'))
|
||||||
|
|
||||||
|
AccessControlList.objects.check_access(
|
||||||
|
permissions=permission_document_checkout,
|
||||||
|
user=self.context['request'].user, obj=document
|
||||||
|
)
|
||||||
|
|
||||||
|
validated_data['document'] = document
|
||||||
|
validated_data['user'] = self.context['request'].user
|
||||||
|
return super(NewDocumentCheckoutSerializer, self).create(
|
||||||
|
validated_data
|
||||||
|
)
|
||||||
|
|||||||
75
mayan/apps/checkouts/tests/test_api.py
Normal file
75
mayan/apps/checkouts/tests/test_api.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.test import override_settings
|
||||||
|
from django.utils.encoding import force_text
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from documents.models import DocumentType
|
||||||
|
from documents.tests import TEST_DOCUMENT_TYPE_LABEL, TEST_SMALL_DOCUMENT_PATH
|
||||||
|
from user_management.tests.literals import (
|
||||||
|
TEST_ADMIN_EMAIL, TEST_ADMIN_PASSWORD, TEST_ADMIN_USERNAME
|
||||||
|
)
|
||||||
|
|
||||||
|
from ..models import DocumentCheckout
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(OCR_AUTO_OCR=False)
|
||||||
|
class CheckoutAPITestCase(APITestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(CheckoutAPITestCase, self).setUp()
|
||||||
|
|
||||||
|
self.admin_user = get_user_model().objects.create_superuser(
|
||||||
|
username=TEST_ADMIN_USERNAME, email=TEST_ADMIN_EMAIL,
|
||||||
|
password=TEST_ADMIN_PASSWORD
|
||||||
|
)
|
||||||
|
|
||||||
|
self.client.login(
|
||||||
|
username=TEST_ADMIN_USERNAME, password=TEST_ADMIN_PASSWORD
|
||||||
|
)
|
||||||
|
|
||||||
|
self.document_type = DocumentType.objects.create(
|
||||||
|
label=TEST_DOCUMENT_TYPE_LABEL
|
||||||
|
)
|
||||||
|
|
||||||
|
with open(TEST_SMALL_DOCUMENT_PATH) as file_object:
|
||||||
|
self.document = self.document_type.new_document(
|
||||||
|
file_object=file_object,
|
||||||
|
)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.document_type.delete()
|
||||||
|
super(CheckoutAPITestCase, self).tearDown()
|
||||||
|
|
||||||
|
def test_document_checkout_get_view(self):
|
||||||
|
expiration_datetime = now() + datetime.timedelta(days=1)
|
||||||
|
|
||||||
|
DocumentCheckout.objects.checkout_document(
|
||||||
|
document=self.document, expiration_datetime=expiration_datetime,
|
||||||
|
user=self.admin_user, block_new_version=True
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(reverse('rest_api:checkout-document-list'))
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
response.data['results'][0]['document']['uuid'],
|
||||||
|
force_text(self.document.uuid)
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_document_checkout_post_view(self):
|
||||||
|
response = self.client.post(
|
||||||
|
reverse('rest_api:checkout-document-list'), data={
|
||||||
|
'document_pk': self.document.pk,
|
||||||
|
'expiration_datetime': '2099-01-01T12:00'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 201)
|
||||||
|
self.assertEqual(
|
||||||
|
DocumentCheckout.objects.first().document, self.document
|
||||||
|
)
|
||||||
@@ -26,11 +26,11 @@ urlpatterns = [
|
|||||||
|
|
||||||
api_urls = [
|
api_urls = [
|
||||||
url(
|
url(
|
||||||
r'^documents/$', APICheckedoutDocumentListView.as_view(),
|
r'^checkouts/$', APICheckedoutDocumentListView.as_view(),
|
||||||
name='checkout-document-list'
|
name='checkout-document-list'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r'^documents/(?P<pk>[0-9]+)/$', APICheckedoutDocumentView.as_view(),
|
r'^checkouts/(?P<pk>[0-9]+)/$', APICheckedoutDocumentView.as_view(),
|
||||||
name='checkedout-document-view'
|
name='checkedout-document-view'
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ class MayanAppConfig(apps.AppConfig):
|
|||||||
except ImportError as exception:
|
except ImportError as exception:
|
||||||
if force_text(exception) not in ('No module named urls', 'No module named \'{}.urls\''.format(self.name)):
|
if force_text(exception) not in ('No module named urls', 'No module named \'{}.urls\''.format(self.name)):
|
||||||
logger.error(
|
logger.error(
|
||||||
'Import time error when running AppConfig.ready(). Check '
|
'Import time error when running AppConfig.ready() of app '
|
||||||
'apps.py, urls.py, views.py, etc.'
|
'"%s".', self.name
|
||||||
)
|
)
|
||||||
raise exception
|
raise exception
|
||||||
|
|
||||||
@@ -127,7 +127,6 @@ class CommonApp(MayanAppConfig):
|
|||||||
Text(text=CommonApp.get_user_label_text), Separator(),
|
Text(text=CommonApp.get_user_label_text), Separator(),
|
||||||
link_current_user_details, link_current_user_edit,
|
link_current_user_details, link_current_user_edit,
|
||||||
link_current_user_locale_profile_edit,
|
link_current_user_locale_profile_edit,
|
||||||
Separator()
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -83,10 +83,9 @@ def render_subtemplate(context, template_name, template_context):
|
|||||||
Renders the specified template with the mixed parent and
|
Renders the specified template with the mixed parent and
|
||||||
subtemplate contexts
|
subtemplate contexts
|
||||||
"""
|
"""
|
||||||
|
new_context = Context(context.flatten())
|
||||||
new_context = Context(context)
|
|
||||||
new_context.update(Context(template_context))
|
new_context.update(Context(template_context))
|
||||||
return get_template(template_name).render(new_context)
|
return get_template(template_name).render(new_context.flatten())
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
@@ -6,9 +6,13 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
from acls import ModelPermission
|
from acls import ModelPermission
|
||||||
from common import MayanAppConfig, menu_facet, menu_object, menu_sidebar
|
from common import MayanAppConfig, menu_facet, menu_object, menu_sidebar
|
||||||
from documents.search import document_page_search, document_search
|
from documents.search import document_page_search, document_search
|
||||||
|
from events import ModelEventType
|
||||||
from navigation import SourceColumn
|
from navigation import SourceColumn
|
||||||
from rest_api.classes import APIEndPoint
|
from rest_api.classes import APIEndPoint
|
||||||
|
|
||||||
|
from .events import (
|
||||||
|
event_document_comment_create, event_document_comment_delete
|
||||||
|
)
|
||||||
from .links import (
|
from .links import (
|
||||||
link_comment_add, link_comment_delete, link_comments_for_document
|
link_comment_add, link_comment_delete, link_comments_for_document
|
||||||
)
|
)
|
||||||
@@ -36,6 +40,12 @@ class DocumentCommentsApp(MayanAppConfig):
|
|||||||
|
|
||||||
Comment = self.get_model('Comment')
|
Comment = self.get_model('Comment')
|
||||||
|
|
||||||
|
ModelEventType.register(
|
||||||
|
model=Document, event_types=(
|
||||||
|
event_document_comment_create, event_document_comment_delete
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
ModelPermission.register(
|
ModelPermission.register(
|
||||||
model=Document, permissions=(
|
model=Document, permissions=(
|
||||||
permission_comment_create, permission_comment_delete,
|
permission_comment_create, permission_comment_delete,
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from events.classes import Event
|
from events import EventTypeNamespace
|
||||||
|
|
||||||
event_document_comment_create = Event(
|
namespace = EventTypeNamespace(
|
||||||
name='document_comment_create',
|
name='document_comments', label=_('Document comments')
|
||||||
label=_('Document comment created')
|
|
||||||
)
|
)
|
||||||
event_document_comment_delete = Event(
|
|
||||||
name='document_comment_delete',
|
event_document_comment_create = namespace.add_event_type(
|
||||||
label=_('Document comment deleted')
|
name='create', label=_('Document comment created')
|
||||||
|
)
|
||||||
|
event_document_comment_delete = namespace.add_event_type(
|
||||||
|
name='delete', label=_('Document comment deleted')
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ urlpatterns = [
|
|||||||
|
|
||||||
api_urls = [
|
api_urls = [
|
||||||
url(
|
url(
|
||||||
r'^document/(?P<document_pk>[0-9]+)/comments/$',
|
r'^documents/(?P<document_pk>[0-9]+)/comments/$',
|
||||||
APICommentListView.as_view(), name='comment-list'
|
APICommentListView.as_view(), name='comment-list'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r'^document/(?P<document_pk>[0-9]+)/comments/(?P<comment_pk>[0-9]+)/$',
|
r'^documents/(?P<document_pk>[0-9]+)/comments/(?P<comment_pk>[0-9]+)/$',
|
||||||
APICommentView.as_view(), name='comment-detail'
|
APICommentView.as_view(), name='comment-detail'
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -72,12 +72,12 @@ urlpatterns = [
|
|||||||
|
|
||||||
api_urls = [
|
api_urls = [
|
||||||
url(
|
url(
|
||||||
r'^index/node/(?P<pk>[0-9]+)/documents/$',
|
r'^indexes/node/(?P<pk>[0-9]+)/documents/$',
|
||||||
APIIndexNodeInstanceDocumentListView.as_view(),
|
APIIndexNodeInstanceDocumentListView.as_view(),
|
||||||
name='index-node-documents'
|
name='index-node-documents'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r'^index/template/(?P<pk>[0-9]+)/$', APIIndexTemplateView.as_view(),
|
r'^indexes/template/(?P<pk>[0-9]+)/$', APIIndexTemplateView.as_view(),
|
||||||
name='index-template-detail'
|
name='index-template-detail'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
@@ -85,12 +85,12 @@ api_urls = [
|
|||||||
name='index-detail'
|
name='index-detail'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r'^index/(?P<pk>[0-9]+)/template/$',
|
r'^indexes/(?P<pk>[0-9]+)/template/$',
|
||||||
APIIndexTemplateListView.as_view(), name='index-template-detail'
|
APIIndexTemplateListView.as_view(), name='index-template-detail'
|
||||||
),
|
),
|
||||||
url(r'^indexes/$', APIIndexListView.as_view(), name='index-list'),
|
url(r'^indexes/$', APIIndexListView.as_view(), name='index-list'),
|
||||||
url(
|
url(
|
||||||
r'^document/(?P<pk>[0-9]+)/indexes/$',
|
r'^documents/(?P<pk>[0-9]+)/indexes/$',
|
||||||
APIDocumentIndexListView.as_view(), name='document-index-list'
|
APIDocumentIndexListView.as_view(), name='document-index-list'
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -2,13 +2,17 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from events.classes import Event
|
from events import EventTypeNamespace
|
||||||
|
|
||||||
event_parsing_document_version_submit = Event(
|
namespace = EventTypeNamespace(
|
||||||
name='parsing_document_version_submit',
|
name='parsing', label=_('Document parsing')
|
||||||
label=_('Document version submitted for parsing')
|
|
||||||
)
|
)
|
||||||
event_parsing_document_version_finish = Event(
|
|
||||||
name='parsing_document_version_finish',
|
event_parsing_document_version_submit = namespace.add_event_type(
|
||||||
label=_('Document version parsing finished')
|
label=_('Document version submitted for parsing'),
|
||||||
|
name='document_version_submit'
|
||||||
|
)
|
||||||
|
event_parsing_document_version_finish = namespace.add_event_type(
|
||||||
|
label=_('Document version parsing finished'),
|
||||||
|
name='document_version_finish'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class DocumentParsingEventsTestCase(GenericDocumentTestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
Action.objects.last().verb,
|
Action.objects.last().verb,
|
||||||
event_parsing_document_version_submit.name
|
event_parsing_document_version_submit.id
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_document_version_finish_event(self):
|
def test_document_version_finish_event(self):
|
||||||
@@ -35,5 +35,5 @@ class DocumentParsingEventsTestCase(GenericDocumentTestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
Action.objects.first().verb,
|
Action.objects.first().verb,
|
||||||
event_parsing_document_version_finish.name
|
event_parsing_document_version_finish.id
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from django.apps import apps
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from document_indexing.tasks import task_index_document
|
from document_indexing.tasks import task_index_document
|
||||||
from events.classes import Event
|
from events.classes import EventType
|
||||||
|
|
||||||
|
|
||||||
def handler_index_document(sender, **kwargs):
|
def handler_index_document(sender, **kwargs):
|
||||||
@@ -42,8 +42,9 @@ def handler_trigger_transition(sender, **kwargs):
|
|||||||
transition = list(set(trigger_transitions) & set(workflow_instance.get_transition_choices()))[0]
|
transition = list(set(trigger_transitions) & set(workflow_instance.get_transition_choices()))[0]
|
||||||
|
|
||||||
workflow_instance.do_transition(
|
workflow_instance.do_transition(
|
||||||
comment=_('Event trigger: %s') % Event.get(name=action.verb).label,
|
comment=_('Event trigger: %s') % EventType.get(
|
||||||
transition=transition
|
name=action.verb
|
||||||
|
).label, transition=transition
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import django.db.models.deletion
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('events', '0001_initial'),
|
('events', '0005_auto_20170731_0452'),
|
||||||
('document_states', '0004_workflow_internal_name'),
|
('document_states', '0004_workflow_internal_name'),
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -17,8 +17,19 @@ class Migration(migrations.Migration):
|
|||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='WorkflowTransitionTriggerEvent',
|
name='WorkflowTransitionTriggerEvent',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
(
|
||||||
('stored_event_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trigger_events', to='events.EventType', verbose_name='Event type')),
|
'id', models.AutoField(
|
||||||
|
auto_created=True, primary_key=True, serialize=False,
|
||||||
|
verbose_name='ID'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'stored_event_type', models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='trigger_events',
|
||||||
|
to='events.StoredEventType', verbose_name='Event type'
|
||||||
|
)
|
||||||
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'Workflow transition trigger event',
|
'verbose_name': 'Workflow transition trigger event',
|
||||||
@@ -28,16 +39,28 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='workflowtransition',
|
model_name='workflowtransition',
|
||||||
name='trigger_time_period',
|
name='trigger_time_period',
|
||||||
field=models.PositiveIntegerField(blank=True, help_text='Amount of time after which this transition will trigger on its own.', null=True, verbose_name='Trigger time period'),
|
field=models.PositiveIntegerField(
|
||||||
|
blank=True, help_text='Amount of time after which this '
|
||||||
|
'transition will trigger on its own.', null=True,
|
||||||
|
verbose_name='Trigger time period'
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='workflowtransition',
|
model_name='workflowtransition',
|
||||||
name='trigger_time_unit',
|
name='trigger_time_unit',
|
||||||
field=models.CharField(blank=True, choices=[('days', 'Days'), ('hours', 'Hours'), ('minutes', 'Minutes')], max_length=8, null=True, verbose_name='Trigger time unit'),
|
field=models.CharField(
|
||||||
|
blank=True, choices=[
|
||||||
|
('days', 'Days'), ('hours', 'Hours'),
|
||||||
|
('minutes', 'Minutes')
|
||||||
|
], max_length=8, null=True, verbose_name='Trigger time unit'),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='workflowtransitiontriggerevent',
|
model_name='workflowtransitiontriggerevent',
|
||||||
name='transition',
|
name='transition',
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='document_states.WorkflowTransition', verbose_name='Transition'),
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to='document_states.WorkflowTransition',
|
||||||
|
verbose_name='Transition'
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -17,16 +17,28 @@ class Migration(migrations.Migration):
|
|||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='workflowinstancelogentry',
|
model_name='workflowinstancelogentry',
|
||||||
name='user',
|
name='user',
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User'),
|
field=models.ForeignKey(
|
||||||
|
blank=True, null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.AUTH_USER_MODEL, verbose_name='User'
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='workflowtransitiontriggerevent',
|
model_name='workflowtransitiontriggerevent',
|
||||||
name='event_type',
|
name='event_type',
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.EventType', verbose_name='Event type'),
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to='events.StoredEventType', verbose_name='Event type'
|
||||||
|
),
|
||||||
),
|
),
|
||||||
migrations.AlterField(
|
migrations.AlterField(
|
||||||
model_name='workflowtransitiontriggerevent',
|
model_name='workflowtransitiontriggerevent',
|
||||||
name='transition',
|
name='transition',
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trigger_events', to='document_states.WorkflowTransition', verbose_name='Transition'),
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name='trigger_events',
|
||||||
|
to='document_states.WorkflowTransition',
|
||||||
|
verbose_name='Transition'
|
||||||
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
from acls.models import AccessControlList
|
from acls.models import AccessControlList
|
||||||
from common.validators import validate_internal_name
|
from common.validators import validate_internal_name
|
||||||
from documents.models import Document, DocumentType
|
from documents.models import Document, DocumentType
|
||||||
from events.models import EventType
|
from events.models import StoredEventType
|
||||||
from permissions import Permission
|
from permissions import Permission
|
||||||
|
|
||||||
from .error_logs import error_log_state_actions
|
from .error_logs import error_log_state_actions
|
||||||
@@ -306,7 +306,8 @@ class WorkflowTransitionTriggerEvent(models.Model):
|
|||||||
related_name='trigger_events', verbose_name=_('Transition')
|
related_name='trigger_events', verbose_name=_('Transition')
|
||||||
)
|
)
|
||||||
event_type = models.ForeignKey(
|
event_type = models.ForeignKey(
|
||||||
EventType, on_delete=models.CASCADE, verbose_name=_('Event type')
|
StoredEventType, on_delete=models.CASCADE,
|
||||||
|
verbose_name=_('Event type')
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -215,20 +215,20 @@ api_urls = [
|
|||||||
APIWorkflowTransitionView.as_view(), name='workflowtransition-detail'
|
APIWorkflowTransitionView.as_view(), name='workflowtransition-detail'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r'^document/(?P<pk>[0-9]+)/workflows/$',
|
r'^documents/(?P<pk>[0-9]+)/workflows/$',
|
||||||
APIWorkflowInstanceListView.as_view(), name='workflowinstance-list'
|
APIWorkflowInstanceListView.as_view(), name='workflowinstance-list'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r'^document/(?P<pk>[0-9]+)/workflows/(?P<workflow_pk>[0-9]+)/$',
|
r'^documents/(?P<pk>[0-9]+)/workflows/(?P<workflow_pk>[0-9]+)/$',
|
||||||
APIWorkflowInstanceView.as_view(), name='workflowinstance-detail'
|
APIWorkflowInstanceView.as_view(), name='workflowinstance-detail'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r'^document/(?P<pk>[0-9]+)/workflows/(?P<workflow_pk>[0-9]+)/log_entries/$',
|
r'^documents/(?P<pk>[0-9]+)/workflows/(?P<workflow_pk>[0-9]+)/log_entries/$',
|
||||||
APIWorkflowInstanceLogEntryListView.as_view(),
|
APIWorkflowInstanceLogEntryListView.as_view(),
|
||||||
name='workflowinstancelogentry-list'
|
name='workflowinstancelogentry-list'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r'^document_type/(?P<pk>[0-9]+)/workflows/$',
|
r'^document_types/(?P<pk>[0-9]+)/workflows/$',
|
||||||
APIDocumentTypeWorkflowListView.as_view(),
|
APIDocumentTypeWorkflowListView.as_view(),
|
||||||
name='documenttype-workflow-list'
|
name='documenttype-workflow-list'
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ from common.views import (
|
|||||||
)
|
)
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.views import DocumentListView
|
from documents.views import DocumentListView
|
||||||
from events.classes import Event
|
from events.classes import EventType
|
||||||
from events.models import EventType
|
from events.models import StoredEventType
|
||||||
|
|
||||||
from .classes import WorkflowAction
|
from .classes import WorkflowAction
|
||||||
from .forms import (
|
from .forms import (
|
||||||
@@ -675,7 +675,7 @@ class WorkflowStateListView(SingleObjectListView):
|
|||||||
|
|
||||||
class SetupWorkflowTransitionTriggerEventListView(FormView):
|
class SetupWorkflowTransitionTriggerEventListView(FormView):
|
||||||
form_class = WorkflowTransitionTriggerEventRelationshipFormSet
|
form_class = WorkflowTransitionTriggerEventRelationshipFormSet
|
||||||
submodel = EventType
|
submodel = StoredEventType
|
||||||
|
|
||||||
def dispatch(self, *args, **kwargs):
|
def dispatch(self, *args, **kwargs):
|
||||||
messages.warning(
|
messages.warning(
|
||||||
@@ -689,7 +689,7 @@ class SetupWorkflowTransitionTriggerEventListView(FormView):
|
|||||||
user=self.request.user, obj=self.get_object().workflow
|
user=self.request.user, obj=self.get_object().workflow
|
||||||
)
|
)
|
||||||
|
|
||||||
Event.refresh()
|
EventType.refresh()
|
||||||
return super(
|
return super(
|
||||||
SetupWorkflowTransitionTriggerEventListView, self
|
SetupWorkflowTransitionTriggerEventListView, self
|
||||||
).dispatch(*args, **kwargs)
|
).dispatch(*args, **kwargs)
|
||||||
@@ -735,8 +735,10 @@ class SetupWorkflowTransitionTriggerEventListView(FormView):
|
|||||||
initial = []
|
initial = []
|
||||||
|
|
||||||
# Return the queryset by name from the sorted list of the class
|
# Return the queryset by name from the sorted list of the class
|
||||||
event_type_ids = [event_type.name for event_type in Event.all()]
|
event_type_ids = [event_type.name for event_type in EventType.all()]
|
||||||
event_type_queryset = EventType.objects.filter(name__in=event_type_ids)
|
event_type_queryset = StoredEventType.objects.filter(
|
||||||
|
name__in=event_type_ids
|
||||||
|
)
|
||||||
|
|
||||||
for event_type in event_type_queryset:
|
for event_type in event_type_queryset:
|
||||||
initial.append({
|
initial.append({
|
||||||
|
|||||||
@@ -23,12 +23,16 @@ from converter.permissions import (
|
|||||||
permission_transformation_delete, permission_transformation_edit,
|
permission_transformation_delete, permission_transformation_edit,
|
||||||
permission_transformation_view,
|
permission_transformation_view,
|
||||||
)
|
)
|
||||||
from events.links import link_events_for_object
|
from events import ModelEventType
|
||||||
|
from events.links import (
|
||||||
|
link_events_for_object, link_object_event_types_user_subcriptions_list,
|
||||||
|
link_object_event_types_user_subcriptions_list_with_icon
|
||||||
|
)
|
||||||
from events.permissions import permission_events_view
|
from events.permissions import permission_events_view
|
||||||
from mayan.celery import app
|
from mayan.celery import app
|
||||||
from mayan_statistics.classes import StatisticNamespace, CharJSLine
|
from mayan_statistics.classes import StatisticNamespace, CharJSLine
|
||||||
from navigation import SourceColumn
|
from navigation import SourceColumn
|
||||||
from rest_api.classes import APIEndPoint
|
from rest_api.classes import APIEndPoint, APIResource
|
||||||
from rest_api.fields import DynamicSerializerField
|
from rest_api.fields import DynamicSerializerField
|
||||||
|
|
||||||
from .dashboard_widgets import (
|
from .dashboard_widgets import (
|
||||||
@@ -36,6 +40,12 @@ from .dashboard_widgets import (
|
|||||||
widget_new_documents_this_month, widget_pages_per_month,
|
widget_new_documents_this_month, widget_pages_per_month,
|
||||||
widget_total_documents
|
widget_total_documents
|
||||||
)
|
)
|
||||||
|
from .events import (
|
||||||
|
event_document_create, event_document_download,
|
||||||
|
event_document_properties_edit, event_document_type_change,
|
||||||
|
event_document_new_version, event_document_version_revert,
|
||||||
|
event_document_view
|
||||||
|
)
|
||||||
from .handlers import (
|
from .handlers import (
|
||||||
create_default_document_type, handler_scan_duplicates_for
|
create_default_document_type, handler_scan_duplicates_for
|
||||||
)
|
)
|
||||||
@@ -107,6 +117,9 @@ class DocumentsApp(MayanAppConfig):
|
|||||||
from actstream import registry
|
from actstream import registry
|
||||||
|
|
||||||
APIEndPoint(app=self, version_string='1')
|
APIEndPoint(app=self, version_string='1')
|
||||||
|
APIResource(label=_('Document types'), name='document_types')
|
||||||
|
APIResource(label=_('Documents'), name='documents')
|
||||||
|
APIResource(label=_('Trashed documents'), name='trashed_documents')
|
||||||
|
|
||||||
DeletedDocument = self.get_model('DeletedDocument')
|
DeletedDocument = self.get_model('DeletedDocument')
|
||||||
Document = self.get_model('Document')
|
Document = self.get_model('Document')
|
||||||
@@ -141,6 +154,19 @@ class DocumentsApp(MayanAppConfig):
|
|||||||
label=_('MIME type'), name='versions__mimetype', type_name='field'
|
label=_('MIME type'), name='versions__mimetype', type_name='field'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ModelEventType.register(
|
||||||
|
model=DocumentType, event_types=(
|
||||||
|
event_document_create,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ModelEventType.register(
|
||||||
|
model=Document, event_types=(
|
||||||
|
event_document_download, event_document_properties_edit,
|
||||||
|
event_document_type_change, event_document_new_version,
|
||||||
|
event_document_version_revert, event_document_view
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
ModelPermission.register(
|
ModelPermission.register(
|
||||||
model=Document, permissions=(
|
model=Document, permissions=(
|
||||||
permission_acl_edit, permission_acl_view,
|
permission_acl_edit, permission_acl_view,
|
||||||
@@ -376,7 +402,8 @@ class DocumentsApp(MayanAppConfig):
|
|||||||
menu_object.bind_links(
|
menu_object.bind_links(
|
||||||
links=(
|
links=(
|
||||||
link_document_type_edit, link_document_type_filename_list,
|
link_document_type_edit, link_document_type_filename_list,
|
||||||
link_acl_list, link_document_type_delete
|
link_acl_list, link_object_event_types_user_subcriptions_list,
|
||||||
|
link_document_type_delete
|
||||||
), sources=(DocumentType,)
|
), sources=(DocumentType,)
|
||||||
)
|
)
|
||||||
menu_object.bind_links(
|
menu_object.bind_links(
|
||||||
@@ -433,8 +460,11 @@ class DocumentsApp(MayanAppConfig):
|
|||||||
links=(link_document_properties,), sources=(Document,), position=2
|
links=(link_document_properties,), sources=(Document,), position=2
|
||||||
)
|
)
|
||||||
menu_facet.bind_links(
|
menu_facet.bind_links(
|
||||||
links=(link_events_for_object, link_document_version_list,),
|
links=(
|
||||||
sources=(Document,), position=2
|
link_events_for_object,
|
||||||
|
link_object_event_types_user_subcriptions_list_with_icon,
|
||||||
|
link_document_version_list,
|
||||||
|
), sources=(Document,), position=2
|
||||||
)
|
)
|
||||||
menu_facet.bind_links(links=(link_document_pages,), sources=(Document,))
|
menu_facet.bind_links(links=(link_document_pages,), sources=(Document,))
|
||||||
menu_facet.bind_links(
|
menu_facet.bind_links(
|
||||||
@@ -444,7 +474,8 @@ class DocumentsApp(MayanAppConfig):
|
|||||||
# Document actions
|
# Document actions
|
||||||
menu_object.bind_links(
|
menu_object.bind_links(
|
||||||
links=(
|
links=(
|
||||||
link_document_version_revert, link_document_version_download
|
link_document_version_view, link_document_version_revert,
|
||||||
|
link_document_version_download
|
||||||
),
|
),
|
||||||
sources=(DocumentVersion,)
|
sources=(DocumentVersion,)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,29 +2,28 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from events.classes import Event
|
from events import EventTypeNamespace
|
||||||
|
|
||||||
event_document_create = Event(
|
namespace = EventTypeNamespace(name='documents', label=_('Documents'))
|
||||||
name='documents_document_create', label=_('Document created')
|
|
||||||
|
event_document_create = namespace.add_event_type(
|
||||||
|
name='document_create', label=_('Document created')
|
||||||
)
|
)
|
||||||
event_document_download = Event(
|
event_document_download = namespace.add_event_type(
|
||||||
name='documents_document_download',
|
name='document_download', label=_('Document downloaded')
|
||||||
label=_('Document downloaded')
|
|
||||||
)
|
)
|
||||||
event_document_properties_edit = Event(
|
event_document_properties_edit = namespace.add_event_type(
|
||||||
name='documents_document_edit', label=_('Document properties edited')
|
name='document_edit', label=_('Document properties edited')
|
||||||
)
|
)
|
||||||
event_document_type_change = Event(
|
event_document_type_change = namespace.add_event_type(
|
||||||
name='documents_document_type_change', label=_('Document type changed')
|
name='document_type_change', label=_('Document type changed')
|
||||||
)
|
)
|
||||||
event_document_new_version = Event(
|
event_document_new_version = namespace.add_event_type(
|
||||||
name='documents_document_new_version', label=_('New version uploaded')
|
name='document_new_version', label=_('New version uploaded')
|
||||||
)
|
)
|
||||||
event_document_version_revert = Event(
|
event_document_version_revert = namespace.add_event_type(
|
||||||
name='documents_document_version_revert',
|
name='document_version_revert', label=_('Document version reverted')
|
||||||
label=_('Document version reverted')
|
|
||||||
)
|
)
|
||||||
event_document_view = Event(
|
event_document_view = namespace.add_event_type(
|
||||||
name='documents_document_view',
|
name='document_view', label=_('Document viewed')
|
||||||
label=_('Document viewed')
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -100,6 +100,11 @@ class DocumentType(models.Model):
|
|||||||
|
|
||||||
return super(DocumentType, self).delete(*args, **kwargs)
|
return super(DocumentType, self).delete(*args, **kwargs)
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse(
|
||||||
|
'documents:document_type_document_list', args=(self.pk,)
|
||||||
|
)
|
||||||
|
|
||||||
def natural_key(self):
|
def natural_key(self):
|
||||||
return (self.label,)
|
return (self.label,)
|
||||||
|
|
||||||
@@ -222,9 +227,13 @@ class Document(models.Model):
|
|||||||
if new_document:
|
if new_document:
|
||||||
if user:
|
if user:
|
||||||
self.add_as_recent_document_for_user(user)
|
self.add_as_recent_document_for_user(user)
|
||||||
event_document_create.commit(actor=user, target=self)
|
event_document_create.commit(
|
||||||
|
actor=user, target=self, action_object=self.document_type
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
event_document_create.commit(target=self)
|
event_document_create.commit(
|
||||||
|
target=self, action_object=self.document_type
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if _commit_events:
|
if _commit_events:
|
||||||
event_document_properties_edit.commit(actor=user, target=self)
|
event_document_properties_edit.commit(actor=user, target=self)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
from smart_settings import Namespace
|
from smart_settings import Namespace
|
||||||
|
|
||||||
LANGUAGE_CHOICES = [
|
LANGUAGE_CHOICES = [
|
||||||
(i.iso639_3_code, i.name) for i in list(pycountry.languages)
|
(i.alpha_3, i.name) for i in list(pycountry.languages)
|
||||||
]
|
]
|
||||||
|
|
||||||
namespace = Namespace(name='documents', label=_('Documents'))
|
namespace = Namespace(name='documents', label=_('Documents'))
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class DocumentEventsTestCase(GenericDocumentViewTestCase):
|
|||||||
|
|
||||||
event = Action.objects.any(obj=self.document).first()
|
event = Action.objects.any(obj=self.document).first()
|
||||||
|
|
||||||
self.assertEqual(event.verb, event_document_download.name)
|
self.assertEqual(event.verb, event_document_download.id)
|
||||||
self.assertEqual(event.target, self.document)
|
self.assertEqual(event.target, self.document)
|
||||||
self.assertEqual(event.actor, self.user)
|
self.assertEqual(event.actor, self.user)
|
||||||
|
|
||||||
@@ -98,6 +98,6 @@ class DocumentEventsTestCase(GenericDocumentViewTestCase):
|
|||||||
|
|
||||||
event = Action.objects.any(obj=self.document).first()
|
event = Action.objects.any(obj=self.document).first()
|
||||||
|
|
||||||
self.assertEqual(event.verb, event_document_view.name)
|
self.assertEqual(event.verb, event_document_view.id)
|
||||||
self.assertEqual(event.target, self.document)
|
self.assertEqual(event.target, self.document)
|
||||||
self.assertEqual(event.actor, self.user)
|
self.assertEqual(event.actor, self.user)
|
||||||
|
|||||||
@@ -916,6 +916,73 @@ class DocumentVersionTestCase(GenericDocumentViewTestCase):
|
|||||||
self.assertEqual(self.document.versions.count(), 1)
|
self.assertEqual(self.document.versions.count(), 1)
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentVersionTestCase(GenericDocumentViewTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(DocumentVersionTestCase, self).setUp()
|
||||||
|
self.login_user()
|
||||||
|
|
||||||
|
def test_document_version_list_no_permission(self):
|
||||||
|
with open(TEST_SMALL_DOCUMENT_PATH) as file_object:
|
||||||
|
self.document.new_version(
|
||||||
|
comment=TEST_VERSION_COMMENT, file_object=file_object
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.get(
|
||||||
|
'documents:document_version_list', args=(self.document.pk,)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
def test_document_version_list_with_permission(self):
|
||||||
|
self.grant_permission(permission=permission_document_version_view)
|
||||||
|
|
||||||
|
with open(TEST_SMALL_DOCUMENT_PATH) as file_object:
|
||||||
|
self.document.new_version(
|
||||||
|
comment=TEST_VERSION_COMMENT, file_object=file_object
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.get(
|
||||||
|
'documents:document_version_list', args=(self.document.pk,)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertContains(response, TEST_VERSION_COMMENT, status_code=200)
|
||||||
|
|
||||||
|
def test_document_version_revert_no_permission(self):
|
||||||
|
first_version = self.document.latest_version
|
||||||
|
|
||||||
|
with open(TEST_SMALL_DOCUMENT_PATH) as file_object:
|
||||||
|
self.document.new_version(
|
||||||
|
file_object=file_object
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.post(
|
||||||
|
'documents:document_version_revert', args=(first_version.pk,)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
self.assertEqual(self.document.versions.count(), 2)
|
||||||
|
|
||||||
|
def test_document_version_revert_with_permission(self):
|
||||||
|
first_version = self.document.latest_version
|
||||||
|
|
||||||
|
with open(TEST_SMALL_DOCUMENT_PATH) as file_object:
|
||||||
|
self.document.new_version(
|
||||||
|
file_object=file_object
|
||||||
|
)
|
||||||
|
|
||||||
|
self.grant_permission(permission=permission_document_version_revert)
|
||||||
|
|
||||||
|
response = self.post(
|
||||||
|
'documents:document_version_revert', args=(first_version.pk,),
|
||||||
|
follow=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertContains(response, 'reverted', status_code=200)
|
||||||
|
self.assertEqual(self.document.versions.count(), 1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class DeletedDocumentTestCase(GenericDocumentViewTestCase):
|
class DeletedDocumentTestCase(GenericDocumentViewTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(DeletedDocumentTestCase, self).setUp()
|
super(DeletedDocumentTestCase, self).setUp()
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ api_urls = [
|
|||||||
name='search-view'
|
name='search-view'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r'^advanced/(?P<search_model>[\.\w]+)/$', APIAdvancedSearchView.as_view(),
|
r'^search/advanced/(?P<search_model>[\.\w]+)/$', APIAdvancedSearchView.as_view(),
|
||||||
name='advanced-search-view'
|
name='advanced-search-view'
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from .classes import Event # NOQA
|
from .classes import EventTypeNamespace, ModelEventType # NOQA
|
||||||
|
|
||||||
default_app_config = 'events.apps.EventsApp'
|
default_app_config = 'events.apps.EventsApp'
|
||||||
|
|||||||
@@ -2,9 +2,19 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import EventType
|
from .models import EventSubscription, Notification, StoredEventType
|
||||||
|
|
||||||
|
|
||||||
@admin.register(EventType)
|
@admin.register(EventSubscription)
|
||||||
class EventTypeAdmin(admin.ModelAdmin):
|
class EventSubscriptionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('user', 'stored_event_type')
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(StoredEventType)
|
||||||
|
class StoredEventTypeAdmin(admin.ModelAdmin):
|
||||||
readonly_fields = ('name', '__str__')
|
readonly_fields = ('name', '__str__')
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Notification)
|
||||||
|
class NotificationAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('user', 'action', 'read')
|
||||||
|
|||||||
@@ -10,9 +10,13 @@ from rest_framework import generics
|
|||||||
from acls.models import AccessControlList
|
from acls.models import AccessControlList
|
||||||
from rest_api.permissions import MayanPermission
|
from rest_api.permissions import MayanPermission
|
||||||
|
|
||||||
from .classes import Event
|
from .classes import EventType, EventTypeNamespace
|
||||||
|
from .models import Notification
|
||||||
from .permissions import permission_events_view
|
from .permissions import permission_events_view
|
||||||
from .serializers import EventSerializer, EventTypeSerializer
|
from .serializers import (
|
||||||
|
EventSerializer, EventTypeSerializer, EventTypeNamespaceSerializer,
|
||||||
|
NotificationSerializer
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class APIObjectEventListView(generics.ListAPIView):
|
class APIObjectEventListView(generics.ListAPIView):
|
||||||
@@ -46,13 +50,72 @@ class APIObjectEventListView(generics.ListAPIView):
|
|||||||
return any_stream(obj)
|
return any_stream(obj)
|
||||||
|
|
||||||
|
|
||||||
|
class APIEventTypeNamespaceDetailView(generics.RetrieveAPIView):
|
||||||
|
"""
|
||||||
|
Returns the details of an event type namespace.
|
||||||
|
"""
|
||||||
|
serializer_class = EventTypeNamespaceSerializer
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
try:
|
||||||
|
return EventTypeNamespace.get(name=self.kwargs['name'])
|
||||||
|
except KeyError:
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
|
||||||
|
class APIEventTypeNamespaceListView(generics.ListAPIView):
|
||||||
|
"""
|
||||||
|
Returns a list of all the available event type namespaces.
|
||||||
|
"""
|
||||||
|
|
||||||
|
serializer_class = EventTypeNamespaceSerializer
|
||||||
|
queryset = EventTypeNamespace.all()
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
return {
|
||||||
|
'format': self.format_kwarg,
|
||||||
|
'request': self.request,
|
||||||
|
'view': self
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class APIEventTypeNamespaceEventTypeListView(generics.ListAPIView):
|
||||||
|
"""
|
||||||
|
Returns a list of all the available event types from a namespaces.
|
||||||
|
"""
|
||||||
|
|
||||||
|
serializer_class = EventTypeSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
try:
|
||||||
|
return EventTypeNamespace.get(
|
||||||
|
name=self.kwargs['name']
|
||||||
|
).get_event_types()
|
||||||
|
except KeyError:
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
return {
|
||||||
|
'format': self.format_kwarg,
|
||||||
|
'request': self.request,
|
||||||
|
'view': self
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class APIEventTypeListView(generics.ListAPIView):
|
class APIEventTypeListView(generics.ListAPIView):
|
||||||
"""
|
"""
|
||||||
Returns a list of all the available event types.
|
Returns a list of all the available event types.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
serializer_class = EventTypeSerializer
|
serializer_class = EventTypeSerializer
|
||||||
queryset = sorted(Event.all(), key=lambda event: event.name)
|
queryset = EventType.all()
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
return {
|
||||||
|
'format': self.format_kwarg,
|
||||||
|
'request': self.request,
|
||||||
|
'view': self
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class APIEventListView(generics.ListAPIView):
|
class APIEventListView(generics.ListAPIView):
|
||||||
@@ -64,3 +127,20 @@ class APIEventListView(generics.ListAPIView):
|
|||||||
permission_classes = (MayanPermission,)
|
permission_classes = (MayanPermission,)
|
||||||
queryset = Action.objects.all()
|
queryset = Action.objects.all()
|
||||||
serializer_class = EventSerializer
|
serializer_class = EventSerializer
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
return {
|
||||||
|
'format': self.format_kwarg,
|
||||||
|
'request': self.request,
|
||||||
|
'view': self
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class APINotificationListView(generics.ListAPIView):
|
||||||
|
"""
|
||||||
|
Return a list of notifications for the current user.
|
||||||
|
"""
|
||||||
|
serializer_class = NotificationSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Notification.objects.filter(user=self.request.user)
|
||||||
|
|||||||
@@ -3,11 +3,19 @@ from __future__ import unicode_literals
|
|||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from common import MayanAppConfig, menu_tools
|
from common import (
|
||||||
|
MayanAppConfig, menu_main, menu_object, menu_secondary, menu_tools,
|
||||||
|
menu_user
|
||||||
|
)
|
||||||
|
from common.widgets import two_state_template
|
||||||
from navigation import SourceColumn
|
from navigation import SourceColumn
|
||||||
from rest_api.classes import APIEndPoint
|
from rest_api.classes import APIEndPoint
|
||||||
|
|
||||||
from .links import link_events_list
|
from .links import (
|
||||||
|
link_events_list, link_event_types_subscriptions_list,
|
||||||
|
link_notification_mark_read, link_notification_mark_read_all,
|
||||||
|
link_user_notifications_list,
|
||||||
|
)
|
||||||
from .licenses import * # NOQA
|
from .licenses import * # NOQA
|
||||||
from .widgets import event_object_link, event_type_link
|
from .widgets import event_object_link, event_type_link
|
||||||
|
|
||||||
@@ -28,6 +36,8 @@ class EventsApp(MayanAppConfig):
|
|||||||
def ready(self):
|
def ready(self):
|
||||||
super(EventsApp, self).ready()
|
super(EventsApp, self).ready()
|
||||||
Action = apps.get_model(app_label='actstream', model_name='Action')
|
Action = apps.get_model(app_label='actstream', model_name='Action')
|
||||||
|
Notification = self.get_model(model_name='Notification')
|
||||||
|
StoredEventType = self.get_model(model_name='StoredEventType')
|
||||||
|
|
||||||
APIEndPoint(app=self, version_string='1')
|
APIEndPoint(app=self, version_string='1')
|
||||||
|
|
||||||
@@ -39,7 +49,7 @@ class EventsApp(MayanAppConfig):
|
|||||||
func=lambda context: event_actor(context['object'])
|
func=lambda context: event_actor(context['object'])
|
||||||
)
|
)
|
||||||
SourceColumn(
|
SourceColumn(
|
||||||
source=Action, label=_('Verb'),
|
source=Action, label=_('Event'),
|
||||||
func=lambda context: event_type_link(context['object'])
|
func=lambda context: event_type_link(context['object'])
|
||||||
)
|
)
|
||||||
SourceColumn(
|
SourceColumn(
|
||||||
@@ -49,4 +59,44 @@ class EventsApp(MayanAppConfig):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SourceColumn(
|
||||||
|
source=StoredEventType, label=_('Namespace'), attribute='namespace'
|
||||||
|
)
|
||||||
|
SourceColumn(
|
||||||
|
source=StoredEventType, label=_('Label'), attribute='label'
|
||||||
|
)
|
||||||
|
|
||||||
|
SourceColumn(
|
||||||
|
source=Notification, label=_('Timestamp'),
|
||||||
|
attribute='action.timestamp'
|
||||||
|
)
|
||||||
|
SourceColumn(
|
||||||
|
source=Notification, label=_('Actor'), attribute='action.actor'
|
||||||
|
)
|
||||||
|
SourceColumn(
|
||||||
|
source=Notification, label=_('Event'),
|
||||||
|
func=lambda context: event_type_link(context['object'].action)
|
||||||
|
)
|
||||||
|
SourceColumn(
|
||||||
|
source=Notification, label=_('Target'),
|
||||||
|
func=lambda context: event_object_link(context['object'].action)
|
||||||
|
)
|
||||||
|
SourceColumn(
|
||||||
|
source=Notification, label=_('Seen'),
|
||||||
|
func=lambda context: two_state_template(
|
||||||
|
state=context['object'].read
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
menu_main.bind_links(
|
||||||
|
links=(link_user_notifications_list,), position=99
|
||||||
|
)
|
||||||
|
menu_object.bind_links(
|
||||||
|
links=(link_notification_mark_read,), sources=(Notification,)
|
||||||
|
)
|
||||||
|
menu_secondary.bind_links(
|
||||||
|
links=(link_notification_mark_read_all,),
|
||||||
|
sources=('events:user_notifications_list',)
|
||||||
|
)
|
||||||
menu_tools.bind_links(links=(link_events_list,))
|
menu_tools.bind_links(links=(link_events_list,))
|
||||||
|
menu_user.bind_links(links=(link_event_types_subscriptions_list,))
|
||||||
|
|||||||
@@ -1,18 +1,57 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.utils.encoding import force_text
|
from django.contrib.auth import get_user_model
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.utils.encoding import force_text, python_2_unicode_compatible
|
||||||
|
|
||||||
from actstream import action
|
from actstream import action
|
||||||
|
|
||||||
|
from .permissions import permission_events_view
|
||||||
|
|
||||||
class Event(object):
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class EventTypeNamespace(object):
|
||||||
_registry = {}
|
_registry = {}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def all(cls):
|
def all(cls):
|
||||||
return Event.sort(event_type_list=cls._registry.values())
|
return sorted(cls._registry.values())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, name):
|
||||||
|
return cls._registry[name]
|
||||||
|
|
||||||
|
def __init__(self, name, label):
|
||||||
|
self.name = name
|
||||||
|
self.label = label
|
||||||
|
self.event_types = []
|
||||||
|
self.__class__._registry[name] = self
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return force_text(self.label)
|
||||||
|
|
||||||
|
def add_event_type(self, name, label):
|
||||||
|
event_type = EventType(namespace=self, name=name, label=label)
|
||||||
|
self.event_types.append(event_type)
|
||||||
|
return event_type
|
||||||
|
|
||||||
|
def get_event_types(self):
|
||||||
|
return EventType.sort(event_type_list=self.event_types)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class EventType(object):
|
||||||
|
_registry = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def all(cls):
|
||||||
|
# Return sorted permisions by namespace.name
|
||||||
|
return EventType.sort(event_type_list=cls._registry.values())
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls, name):
|
def get(cls, name):
|
||||||
@@ -20,51 +59,174 @@ class Event(object):
|
|||||||
return cls._registry[name]
|
return cls._registry[name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise KeyError(
|
raise KeyError(
|
||||||
_('Unknown or obsolete event type: {0}'.format(name))
|
'Unknown or obsolete event type: {0}'.format(name)
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
def __init__(self, namespace, name, label):
|
||||||
def get_label(cls, name):
|
self.namespace = namespace
|
||||||
try:
|
self.name = name
|
||||||
return cls.get(name=name).label
|
self.label = label
|
||||||
except KeyError as exception:
|
self.stored_event_type = None
|
||||||
return force_text(exception)
|
self.__class__._registry[self.id] = self
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return force_text('{}: {}'.format(self.namespace.label, self.label))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self):
|
||||||
|
return '%s.%s' % (self.namespace.name, self.name)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def refresh(cls):
|
def refresh(cls):
|
||||||
for event_type in cls.all():
|
for event_type in cls.all():
|
||||||
event_type.get_type()
|
event_type.get_stored_event_type()
|
||||||
|
|
||||||
|
def get_stored_event_type(self):
|
||||||
|
if not self.stored_event_type:
|
||||||
|
StoredEventType = apps.get_model('events', 'StoredEventType')
|
||||||
|
|
||||||
|
self.stored_event_type, created = StoredEventType.objects.get_or_create(
|
||||||
|
name=self.id
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.stored_event_type
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def sort(event_type_list):
|
def sort(event_type_list):
|
||||||
return sorted(
|
return sorted(
|
||||||
event_type_list, key=lambda x: x.label
|
event_type_list, key=lambda x: (x.namespace.label, x.label)
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, name, label):
|
|
||||||
self.name = name
|
|
||||||
self.label = label
|
|
||||||
self.event_type = None
|
|
||||||
self.__class__._registry[name] = self
|
|
||||||
|
|
||||||
def get_type(self):
|
|
||||||
if not self.event_type:
|
|
||||||
EventType = apps.get_model('events', 'EventType')
|
|
||||||
|
|
||||||
self.event_type, created = EventType.objects.get_or_create(
|
|
||||||
name=self.name
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.event_type
|
|
||||||
|
|
||||||
def commit(self, actor=None, action_object=None, target=None):
|
def commit(self, actor=None, action_object=None, target=None):
|
||||||
if not self.event_type:
|
AccessControlList = apps.get_model(
|
||||||
EventType = apps.get_model('events', 'EventType')
|
app_label='acls', model_name='AccessControlList'
|
||||||
self.event_type, created = EventType.objects.get_or_create(
|
)
|
||||||
name=self.name
|
Action = apps.get_model(
|
||||||
)
|
app_label='actstream', model_name='Action'
|
||||||
|
)
|
||||||
|
ContentType = apps.get_model(
|
||||||
|
app_label='contenttypes', model_name='ContentType'
|
||||||
|
)
|
||||||
|
Notification = apps.get_model(
|
||||||
|
app_label='events', model_name='Notification'
|
||||||
|
)
|
||||||
|
|
||||||
action.send(
|
results = action.send(
|
||||||
actor or target, actor=actor, verb=self.name,
|
actor or target, actor=actor, verb=self.id,
|
||||||
action_object=action_object, target=target
|
action_object=action_object, target=target
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for handler, result in results:
|
||||||
|
if isinstance(result, Action):
|
||||||
|
for user in get_user_model().objects.all():
|
||||||
|
notification = None
|
||||||
|
|
||||||
|
if user.event_subscriptions.filter(stored_event_type__name=result.verb).exists():
|
||||||
|
if result.target:
|
||||||
|
try:
|
||||||
|
AccessControlList.objects.check_access(
|
||||||
|
permissions=permission_events_view,
|
||||||
|
user=user, obj=result.target
|
||||||
|
)
|
||||||
|
except PermissionDenied:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
notification = Notification.objects.create(action=result, user=user)
|
||||||
|
else:
|
||||||
|
notification = Notification.objects.create(action=result, user=user)
|
||||||
|
|
||||||
|
if result.target:
|
||||||
|
content_type = ContentType.objects.get_for_model(model=result.target)
|
||||||
|
|
||||||
|
relationship = user.object_subscriptions.filter(
|
||||||
|
content_type=content_type,
|
||||||
|
object_id=result.target.pk,
|
||||||
|
stored_event_type__name=result.verb
|
||||||
|
)
|
||||||
|
|
||||||
|
if relationship.exists():
|
||||||
|
try:
|
||||||
|
AccessControlList.objects.check_access(
|
||||||
|
permissions=permission_events_view,
|
||||||
|
user=user, obj=result.target
|
||||||
|
)
|
||||||
|
except PermissionDenied:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
notification = Notification.objects.create(action=result, user=user)
|
||||||
|
if not notification and result.action_object:
|
||||||
|
content_type = ContentType.objects.get_for_model(model=result.action_object)
|
||||||
|
|
||||||
|
relationship = user.object_subscriptions.filter(
|
||||||
|
content_type=content_type,
|
||||||
|
object_id=result.action_object.pk,
|
||||||
|
stored_event_type__name=result.verb
|
||||||
|
)
|
||||||
|
|
||||||
|
if relationship.exists():
|
||||||
|
try:
|
||||||
|
AccessControlList.objects.check_access(
|
||||||
|
permissions=permission_events_view,
|
||||||
|
user=user, obj=result.action_object
|
||||||
|
)
|
||||||
|
except PermissionDenied:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
notification = Notification.objects.create(action=result, user=user)
|
||||||
|
|
||||||
|
|
||||||
|
class ModelEventType(object):
|
||||||
|
"""
|
||||||
|
Class to allow matching a model to a specific set of events.
|
||||||
|
"""
|
||||||
|
_registry = {}
|
||||||
|
_proxies = {}
|
||||||
|
_inheritances = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register(cls, model, event_types):
|
||||||
|
cls._registry.setdefault(model, [])
|
||||||
|
for event_type in event_types:
|
||||||
|
cls._registry[model].append(event_type)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_for_class(cls, klass):
|
||||||
|
return cls._registry.get(klass, ())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_for_instance(cls, instance):
|
||||||
|
StoredEventType = apps.get_model(
|
||||||
|
app_label='events', model_name='StoredEventType'
|
||||||
|
)
|
||||||
|
|
||||||
|
events = []
|
||||||
|
|
||||||
|
class_events = cls._registry.get(type(instance))
|
||||||
|
|
||||||
|
if class_events:
|
||||||
|
events.extend(class_events)
|
||||||
|
|
||||||
|
proxy = cls._proxies.get(type(instance))
|
||||||
|
|
||||||
|
if proxy:
|
||||||
|
events.extend(cls._registry.get(proxy))
|
||||||
|
|
||||||
|
pks = [
|
||||||
|
event.id for event in set(events)
|
||||||
|
]
|
||||||
|
|
||||||
|
return EventType.sort(
|
||||||
|
event_type_list=StoredEventType.objects.filter(name__in=pks)
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_inheritance(cls, model):
|
||||||
|
return cls._inheritances[model]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register_proxy(cls, source, model):
|
||||||
|
cls._proxies[model] = source
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register_inheritance(cls, model, related):
|
||||||
|
cls._inheritances[model] = related
|
||||||
|
|||||||
122
mayan/apps/events/forms.py
Normal file
122
mayan/apps/events/forms.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.forms.formsets import formset_factory
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from .models import EventSubscription, ObjectEventSubscription
|
||||||
|
|
||||||
|
|
||||||
|
class EventTypeUserRelationshipForm(forms.Form):
|
||||||
|
namespace = forms.CharField(
|
||||||
|
label=_('Namespace'), required=False,
|
||||||
|
widget=forms.TextInput(attrs={'readonly': 'readonly'})
|
||||||
|
)
|
||||||
|
label = forms.CharField(
|
||||||
|
label=_('Label'), required=False,
|
||||||
|
widget=forms.TextInput(attrs={'readonly': 'readonly'})
|
||||||
|
)
|
||||||
|
subscription = forms.ChoiceField(
|
||||||
|
label=_('Subscription'),
|
||||||
|
widget=forms.RadioSelect(), choices=(
|
||||||
|
('none', _('No')),
|
||||||
|
('subscribed', _('Subscribed')),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(EventTypeUserRelationshipForm, self).__init__(
|
||||||
|
*args, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
self.fields['namespace'].initial = self.initial['stored_event_type'].namespace
|
||||||
|
self.fields['label'].initial = self.initial['stored_event_type'].label
|
||||||
|
|
||||||
|
subscription = EventSubscription.objects.get_for(
|
||||||
|
stored_event_type=self.initial['stored_event_type'],
|
||||||
|
user=self.initial['user'],
|
||||||
|
)
|
||||||
|
|
||||||
|
if subscription.exists():
|
||||||
|
self.fields['subscription'].initial = 'subscribed'
|
||||||
|
else:
|
||||||
|
self.fields['subscription'].initial = 'none'
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
subscription = EventSubscription.objects.get_for(
|
||||||
|
stored_event_type=self.initial['stored_event_type'],
|
||||||
|
user=self.initial['user'],
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.cleaned_data['subscription'] == 'none':
|
||||||
|
subscription.delete()
|
||||||
|
elif self.cleaned_data['subscription'] == 'subscribed':
|
||||||
|
if not subscription.exists():
|
||||||
|
EventSubscription.objects.create_for(
|
||||||
|
stored_event_type=self.initial['stored_event_type'],
|
||||||
|
user=self.initial['user']
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
EventTypeUserRelationshipFormSet = formset_factory(
|
||||||
|
EventTypeUserRelationshipForm, extra=0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectEventTypeUserRelationshipForm(forms.Form):
|
||||||
|
namespace = forms.CharField(
|
||||||
|
label=_('Namespace'), required=False,
|
||||||
|
widget=forms.TextInput(attrs={'readonly': 'readonly'})
|
||||||
|
)
|
||||||
|
label = forms.CharField(
|
||||||
|
label=_('Label'), required=False,
|
||||||
|
widget=forms.TextInput(attrs={'readonly': 'readonly'})
|
||||||
|
)
|
||||||
|
subscription = forms.ChoiceField(
|
||||||
|
label=_('Subscription'),
|
||||||
|
widget=forms.RadioSelect(), choices=(
|
||||||
|
('none', _('No')),
|
||||||
|
('subscribed', _('Subscribed')),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(ObjectEventTypeUserRelationshipForm, self).__init__(
|
||||||
|
*args, **kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
self.fields['namespace'].initial = self.initial['stored_event_type'].namespace
|
||||||
|
self.fields['label'].initial = self.initial['stored_event_type'].label
|
||||||
|
|
||||||
|
subscription = ObjectEventSubscription.objects.get_for(
|
||||||
|
obj=self.initial['object'],
|
||||||
|
stored_event_type=self.initial['stored_event_type'],
|
||||||
|
user=self.initial['user'],
|
||||||
|
)
|
||||||
|
|
||||||
|
if subscription.exists():
|
||||||
|
self.fields['subscription'].initial = 'subscribed'
|
||||||
|
else:
|
||||||
|
self.fields['subscription'].initial = 'none'
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
subscription = ObjectEventSubscription.objects.get_for(
|
||||||
|
obj=self.initial['object'],
|
||||||
|
stored_event_type=self.initial['stored_event_type'],
|
||||||
|
user=self.initial['user'],
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.cleaned_data['subscription'] == 'none':
|
||||||
|
subscription.delete()
|
||||||
|
elif self.cleaned_data['subscription'] == 'subscribed':
|
||||||
|
if not subscription.exists():
|
||||||
|
ObjectEventSubscription.objects.create_for(
|
||||||
|
obj=self.initial['object'],
|
||||||
|
stored_event_type=self.initial['stored_event_type'],
|
||||||
|
user=self.initial['user']
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
ObjectEventTypeUserRelationshipFormSet = formset_factory(
|
||||||
|
ObjectEventTypeUserRelationshipForm, extra=0
|
||||||
|
)
|
||||||
@@ -26,12 +26,44 @@ def get_kwargs_factory(variable_name):
|
|||||||
return get_kwargs
|
return get_kwargs
|
||||||
|
|
||||||
|
|
||||||
|
def get_notification_count(context):
|
||||||
|
return context['request'].user.notifications.filter(read=False).count()
|
||||||
|
|
||||||
|
|
||||||
link_events_list = Link(
|
link_events_list = Link(
|
||||||
icon='fa fa-list-ol', permissions=(permission_events_view,),
|
icon='fa fa-list-ol', permissions=(permission_events_view,),
|
||||||
text=_('Events'), view='events:events_list'
|
text=_('Events'), view='events:events_list'
|
||||||
)
|
)
|
||||||
|
link_events_details = Link(
|
||||||
|
text=_('Events'), view='events:events_list'
|
||||||
|
)
|
||||||
link_events_for_object = Link(
|
link_events_for_object = Link(
|
||||||
icon='fa fa-list-ol', permissions=(permission_events_view,),
|
icon='fa fa-list-ol', permissions=(permission_events_view,),
|
||||||
text=_('Events'), view='events:events_for_object',
|
text=_('Events'), view='events:events_for_object',
|
||||||
kwargs=get_kwargs_factory('resolved_object')
|
kwargs=get_kwargs_factory('resolved_object')
|
||||||
)
|
)
|
||||||
|
link_event_types_subscriptions_list = Link(
|
||||||
|
icon='fa fa-list-ol', text=_('Event subscriptions'),
|
||||||
|
view='events:event_types_user_subcriptions_list'
|
||||||
|
)
|
||||||
|
link_notification_mark_read = Link(
|
||||||
|
args='object.pk', text=_('Mark as seen'),
|
||||||
|
view='events:notification_mark_read'
|
||||||
|
)
|
||||||
|
link_notification_mark_read_all = Link(
|
||||||
|
text=_('Mark all as seen'), view='events:notification_mark_read_all'
|
||||||
|
)
|
||||||
|
link_object_event_types_user_subcriptions_list = Link(
|
||||||
|
kwargs=get_kwargs_factory('resolved_object'),
|
||||||
|
permissions=(permission_events_view,), text=_('Subscriptions'),
|
||||||
|
view='events:object_event_types_user_subcriptions_list',
|
||||||
|
)
|
||||||
|
link_object_event_types_user_subcriptions_list_with_icon = Link(
|
||||||
|
kwargs=get_kwargs_factory('resolved_object'), icon='fa fa-rss',
|
||||||
|
permissions=(permission_events_view,), text=_('Subscriptions'),
|
||||||
|
view='events:object_event_types_user_subcriptions_list',
|
||||||
|
)
|
||||||
|
link_user_notifications_list = Link(
|
||||||
|
icon='fa fa-bell', text=get_notification_count,
|
||||||
|
view='events:user_notifications_list'
|
||||||
|
)
|
||||||
|
|||||||
34
mayan/apps/events/managers.py
Normal file
34
mayan/apps/events/managers.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class EventSubscriptionManager(models.Manager):
|
||||||
|
def create_for(self, stored_event_type, user):
|
||||||
|
return self.create(
|
||||||
|
stored_event_type=stored_event_type, user=user
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_for(self, stored_event_type, user):
|
||||||
|
return self.filter(
|
||||||
|
stored_event_type=stored_event_type, user=user
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectEventSubscriptionManager(models.Manager):
|
||||||
|
def create_for(self, obj, stored_event_type, user):
|
||||||
|
content_type = ContentType.objects.get_for_model(model=obj)
|
||||||
|
|
||||||
|
return self.create(
|
||||||
|
content_type=content_type, object_id=obj.pk,
|
||||||
|
stored_event_type=stored_event_type, user=user
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_for(self, obj, stored_event_type, user):
|
||||||
|
content_type = ContentType.objects.get_for_model(model=obj)
|
||||||
|
|
||||||
|
return self.filter(
|
||||||
|
content_type=content_type, object_id=obj.pk,
|
||||||
|
stored_event_type=stored_event_type, user=user
|
||||||
|
)
|
||||||
30
mayan/apps/events/migrations/0002_eventsubscription.py
Normal file
30
mayan/apps/events/migrations/0002_eventsubscription.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2017-07-29 07:04
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('events', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='EventSubscription',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('event_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subscriptions', to='events.EventType', verbose_name='Event type')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Event subscription',
|
||||||
|
'verbose_name_plural': 'Event subscriptions',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
32
mayan/apps/events/migrations/0003_notification.py
Normal file
32
mayan/apps/events/migrations/0003_notification.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2017-07-29 07:23
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('actstream', '0002_remove_action_data'),
|
||||||
|
('events', '0002_eventsubscription'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Notification',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('read', models.BooleanField(default=False, verbose_name='Read')),
|
||||||
|
('action', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to='actstream.Action', verbose_name='Action')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Notification',
|
||||||
|
'verbose_name_plural': 'Notifications',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
40
mayan/apps/events/migrations/0004_auto_20170731_0423.py
Normal file
40
mayan/apps/events/migrations/0004_auto_20170731_0423.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2017-07-31 04:23
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('events', '0003_notification'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameModel(
|
||||||
|
old_name='EventType',
|
||||||
|
new_name='StoredEventType',
|
||||||
|
),
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='storedeventtype',
|
||||||
|
options={'verbose_name': 'Stored event type', 'verbose_name_plural': 'Stored event types'},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='eventsubscription',
|
||||||
|
name='event_type',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='eventsubscription',
|
||||||
|
name='stored_event_type',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='event_subscriptions', to='events.StoredEventType', verbose_name='Event type'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='eventsubscription',
|
||||||
|
name='user',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='event_subscriptions', to=settings.AUTH_USER_MODEL, verbose_name='User'),
|
||||||
|
),
|
||||||
|
]
|
||||||
44
mayan/apps/events/migrations/0005_auto_20170731_0452.py
Normal file
44
mayan/apps/events/migrations/0005_auto_20170731_0452.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2017-07-31 04:52
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def update_event_types_names(apps, schema_editor):
|
||||||
|
Action = apps.get_model('actstream', 'Action')
|
||||||
|
StoredEventType = apps.get_model('events', 'StoredEventType')
|
||||||
|
|
||||||
|
known_namespaces = {
|
||||||
|
'documents_': 'documents.',
|
||||||
|
'checkouts_': 'checkouts.',
|
||||||
|
'document_comment_': 'document_comments.',
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern = re.compile('|'.join(known_namespaces.keys()))
|
||||||
|
|
||||||
|
for event_type in StoredEventType.objects.all():
|
||||||
|
event_type.name = pattern.sub(
|
||||||
|
lambda x: known_namespaces[x.group()], event_type.name
|
||||||
|
)
|
||||||
|
event_type.save()
|
||||||
|
|
||||||
|
for action in Action.objects.all():
|
||||||
|
action.verb = pattern.sub(
|
||||||
|
lambda x: known_namespaces[x.group()], action.verb
|
||||||
|
)
|
||||||
|
action.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('events', '0004_auto_20170731_0423'),
|
||||||
|
('actstream', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(update_event_types_names),
|
||||||
|
]
|
||||||
33
mayan/apps/events/migrations/0006_objecteventsubscription.py
Normal file
33
mayan/apps/events/migrations/0006_objecteventsubscription.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2017-07-31 06:40
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
('events', '0005_auto_20170731_0452'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ObjectEventSubscription',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('object_id', models.PositiveIntegerField()),
|
||||||
|
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
|
||||||
|
('stored_event_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='object_subscriptions', to='events.StoredEventType', verbose_name='Event type')),
|
||||||
|
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='object_subscriptions', to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Object event subscription',
|
||||||
|
'verbose_name_plural': 'Object event subscriptions',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
19
mayan/apps/events/migrations/0007_auto_20170802_0823.py
Normal file
19
mayan/apps/events/migrations/0007_auto_20170802_0823.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2017-08-02 08:23
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('events', '0006_objecteventsubscription'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='notification',
|
||||||
|
options={'ordering': ('-action__timestamp',), 'verbose_name': 'Notification', 'verbose_name_plural': 'Notifications'},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,28 +1,111 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.encoding import python_2_unicode_compatible
|
from django.utils.encoding import force_text, python_2_unicode_compatible
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from .classes import Event
|
from actstream.models import Action
|
||||||
|
|
||||||
|
from .classes import EventType
|
||||||
|
from .managers import (
|
||||||
|
EventSubscriptionManager, ObjectEventSubscriptionManager
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class EventType(models.Model):
|
class StoredEventType(models.Model):
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=64, unique=True, verbose_name=_('Name')
|
max_length=64, unique=True, verbose_name=_('Name')
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _('Event type')
|
verbose_name = _('Stored event type')
|
||||||
verbose_name_plural = _('Event types')
|
verbose_name_plural = _('Stored event types')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.get_class().label
|
return force_text(self.get_class())
|
||||||
|
|
||||||
def get_class(self):
|
def get_class(self):
|
||||||
return Event.get(name=self.name)
|
return EventType.get(name=self.name)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def label(self):
|
def label(self):
|
||||||
return self.get_class().label
|
return self.get_class().label
|
||||||
|
|
||||||
|
@property
|
||||||
|
def namespace(self):
|
||||||
|
return self.get_class().namespace
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class EventSubscription(models.Model):
|
||||||
|
user = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL, db_index=True, on_delete=models.CASCADE,
|
||||||
|
related_name='event_subscriptions', verbose_name=_('User')
|
||||||
|
)
|
||||||
|
stored_event_type = models.ForeignKey(
|
||||||
|
StoredEventType, on_delete=models.CASCADE,
|
||||||
|
related_name='event_subscriptions', verbose_name=_('Event type')
|
||||||
|
)
|
||||||
|
|
||||||
|
objects = EventSubscriptionManager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Event subscription')
|
||||||
|
verbose_name_plural = _('Event subscriptions')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return force_text(self.stored_event_type)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class Notification(models.Model):
|
||||||
|
user = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL, db_index=True, on_delete=models.CASCADE,
|
||||||
|
related_name='notifications', verbose_name=_('User')
|
||||||
|
)
|
||||||
|
action = models.ForeignKey(
|
||||||
|
Action, on_delete=models.CASCADE, related_name='notifications',
|
||||||
|
verbose_name=_('Action')
|
||||||
|
)
|
||||||
|
read = models.BooleanField(default=False, verbose_name=_('Read'))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('-action__timestamp',)
|
||||||
|
verbose_name = _('Notification')
|
||||||
|
verbose_name_plural = _('Notifications')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return force_text(self.action)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class ObjectEventSubscription(models.Model):
|
||||||
|
content_type = models.ForeignKey(
|
||||||
|
ContentType, on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
object_id = models.PositiveIntegerField()
|
||||||
|
content_object = GenericForeignKey(
|
||||||
|
ct_field='content_type',
|
||||||
|
fk_field='object_id',
|
||||||
|
)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL, db_index=True, on_delete=models.CASCADE,
|
||||||
|
related_name='object_subscriptions', verbose_name=_('User')
|
||||||
|
)
|
||||||
|
stored_event_type = models.ForeignKey(
|
||||||
|
StoredEventType, on_delete=models.CASCADE,
|
||||||
|
related_name='object_subscriptions', verbose_name=_('Event type')
|
||||||
|
)
|
||||||
|
|
||||||
|
objects = ObjectEventSubscriptionManager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Object event subscription')
|
||||||
|
verbose_name_plural = _('Object event subscriptions')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return force_text(self.stored_event_type)
|
||||||
|
|||||||
@@ -4,30 +4,59 @@ from django.utils.six import string_types
|
|||||||
|
|
||||||
from actstream.models import Action
|
from actstream.models import Action
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
from common.serializers import ContentTypeSerializer
|
from common.serializers import ContentTypeSerializer
|
||||||
from rest_api.fields import DynamicSerializerField
|
from rest_api.fields import DynamicSerializerField
|
||||||
|
from user_management.serializers import UserSerializer
|
||||||
|
|
||||||
from .classes import Event
|
from .classes import EventType
|
||||||
from .models import EventType
|
from .models import Notification, StoredEventType
|
||||||
|
|
||||||
|
|
||||||
|
class EventTypeNamespaceSerializer(serializers.Serializer):
|
||||||
|
label = serializers.CharField()
|
||||||
|
name = serializers.CharField()
|
||||||
|
url = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
event_types_url = serializers.HyperlinkedIdentityField(
|
||||||
|
lookup_field='name',
|
||||||
|
view_name='rest_api:event-type-namespace-event-type-list',
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_url(self, instance):
|
||||||
|
return reverse(
|
||||||
|
'rest_api:event-type-namespace-detail', args=(
|
||||||
|
instance.name,
|
||||||
|
), request=self.context['request'], format=self.context['format']
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class EventTypeSerializer(serializers.Serializer):
|
class EventTypeSerializer(serializers.Serializer):
|
||||||
label = serializers.CharField()
|
label = serializers.CharField()
|
||||||
name = serializers.CharField()
|
name = serializers.CharField()
|
||||||
|
id = serializers.CharField()
|
||||||
|
event_type_namespace_url = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def get_event_type_namespace_url(self, instance):
|
||||||
|
return reverse(
|
||||||
|
'rest_api:event-type-namespace-detail', args=(
|
||||||
|
instance.namespace.name,
|
||||||
|
), request=self.context['request'], format=self.context['format']
|
||||||
|
)
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
if isinstance(instance, Event):
|
if isinstance(instance, EventType):
|
||||||
return super(EventTypeSerializer, self).to_representation(
|
return super(EventTypeSerializer, self).to_representation(
|
||||||
instance
|
instance
|
||||||
)
|
)
|
||||||
elif isinstance(instance, EventType):
|
elif isinstance(instance, StoredEventType):
|
||||||
return super(EventTypeSerializer, self).to_representation(
|
return super(EventTypeSerializer, self).to_representation(
|
||||||
instance.get_class()
|
instance.get_class()
|
||||||
)
|
)
|
||||||
elif isinstance(instance, string_types):
|
elif isinstance(instance, string_types):
|
||||||
return super(EventTypeSerializer, self).to_representation(
|
return super(EventTypeSerializer, self).to_representation(
|
||||||
Event.get(name=instance)
|
EventType.get(name=instance)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -43,3 +72,12 @@ class EventSerializer(serializers.ModelSerializer):
|
|||||||
'action_object_content_type', 'action_object_object_id'
|
'action_object_content_type', 'action_object_object_id'
|
||||||
)
|
)
|
||||||
model = Action
|
model = Action
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationSerializer(serializers.ModelSerializer):
|
||||||
|
user = UserSerializer(read_only=True)
|
||||||
|
action = EventSerializer(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
fields = ('action', 'read', 'user')
|
||||||
|
model = Notification
|
||||||
|
|||||||
@@ -3,9 +3,15 @@ from __future__ import unicode_literals
|
|||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from .api_views import (
|
from .api_views import (
|
||||||
APIEventListView, APIEventTypeListView, APIObjectEventListView
|
APIEventListView, APIEventTypeListView, APIEventTypeNamespaceDetailView,
|
||||||
|
APIEventTypeNamespaceEventTypeListView, APIEventTypeNamespaceListView,
|
||||||
|
APINotificationListView, APIObjectEventListView
|
||||||
|
)
|
||||||
|
from .views import (
|
||||||
|
EventListView, EventTypeSubscriptionListView, NotificationListView,
|
||||||
|
NotificationMarkRead, NotificationMarkReadAll, ObjectEventListView,
|
||||||
|
ObjectEventTypeSubscriptionListView, VerbEventListView
|
||||||
)
|
)
|
||||||
from .views import EventListView, ObjectEventListView, VerbEventListView
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^all/$', EventListView.as_view(), name='events_list'),
|
url(r'^all/$', EventListView.as_view(), name='events_list'),
|
||||||
@@ -14,16 +20,60 @@ urlpatterns = [
|
|||||||
ObjectEventListView.as_view(), name='events_for_object'
|
ObjectEventListView.as_view(), name='events_for_object'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r'^by_verb/(?P<verb>[\w\-]+)/$', VerbEventListView.as_view(),
|
r'^by_verb/(?P<verb>[\w\-\.]+)/$', VerbEventListView.as_view(),
|
||||||
name='events_by_verb'
|
name='events_by_verb'
|
||||||
),
|
),
|
||||||
|
url(
|
||||||
|
r'^notifications/(?P<pk>\d+)/mark_read/$',
|
||||||
|
NotificationMarkRead.as_view(), name='notification_mark_read'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^notifications/all/mark_read/$',
|
||||||
|
NotificationMarkReadAll.as_view(), name='notification_mark_read_all'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^user/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/subscriptions/$',
|
||||||
|
ObjectEventTypeSubscriptionListView.as_view(),
|
||||||
|
name='object_event_types_user_subcriptions_list'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^user/event_types/subscriptions/$',
|
||||||
|
EventTypeSubscriptionListView.as_view(),
|
||||||
|
name='event_types_user_subcriptions_list'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^user/notifications/$',
|
||||||
|
NotificationListView.as_view(),
|
||||||
|
name='user_notifications_list'
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
api_urls = [
|
api_urls = [
|
||||||
url(r'^types/$', APIEventTypeListView.as_view(), name='event-type-list'),
|
url(
|
||||||
|
r'^event_type_namespaces/(?P<name>[-\w]+)/$',
|
||||||
|
APIEventTypeNamespaceDetailView.as_view(),
|
||||||
|
name='event-type-namespace-detail'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^event_type_namespaces/(?P<name>[-\w]+)/event_types/$',
|
||||||
|
APIEventTypeNamespaceEventTypeListView.as_view(),
|
||||||
|
name='event-type-namespace-event-type-list'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^event_type_namespaces/$', APIEventTypeNamespaceListView.as_view(),
|
||||||
|
name='event-type-namespace-list'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^event_types/$', APIEventTypeListView.as_view(),
|
||||||
|
name='event-type-list'
|
||||||
|
),
|
||||||
url(r'^events/$', APIEventListView.as_view(), name='event-list'),
|
url(r'^events/$', APIEventListView.as_view(), name='event-list'),
|
||||||
url(
|
url(
|
||||||
r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/events/$',
|
r'^notifications/$', APINotificationListView.as_view(),
|
||||||
|
name='notification-list'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/events/$',
|
||||||
APIObjectEventListView.as_view(), name='object-event-list'
|
APIObjectEventListView.as_view(), name='object-event-list'
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from django.contrib import messages
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.http import Http404
|
from django.http import Http404, HttpResponseRedirect
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from actstream.models import Action, any_stream
|
from actstream.models import Action, any_stream
|
||||||
|
|
||||||
from acls.models import AccessControlList
|
from acls.models import AccessControlList
|
||||||
|
from common.generics import FormView, SimpleView
|
||||||
from common.utils import encapsulate
|
from common.utils import encapsulate
|
||||||
from common.views import SingleObjectListView
|
from common.views import SingleObjectListView
|
||||||
|
|
||||||
from .classes import Event
|
from .classes import EventType, ModelEventType
|
||||||
|
from .forms import (
|
||||||
|
EventTypeUserRelationshipFormSet, ObjectEventTypeUserRelationshipFormSet
|
||||||
|
)
|
||||||
|
from .models import StoredEventType
|
||||||
from .permissions import permission_events_view
|
from .permissions import permission_events_view
|
||||||
from .widgets import event_object_link
|
from .widgets import event_object_link
|
||||||
|
|
||||||
@@ -37,6 +44,96 @@ class EventListView(SingleObjectListView):
|
|||||||
return Action.objects.all()
|
return Action.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class EventTypeSubscriptionListView(FormView):
|
||||||
|
form_class = EventTypeUserRelationshipFormSet
|
||||||
|
main_model = 'user'
|
||||||
|
submodel = StoredEventType
|
||||||
|
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
EventType.refresh()
|
||||||
|
return super(EventTypeSubscriptionListView, self).dispatch(*args, **kwargs)
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
try:
|
||||||
|
for instance in form:
|
||||||
|
instance.save()
|
||||||
|
except Exception as exception:
|
||||||
|
messages.error(
|
||||||
|
self.request,
|
||||||
|
_('Error updating event subscription; %s') % exception
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
messages.success(
|
||||||
|
self.request, _('Event subscriptions updated successfully')
|
||||||
|
)
|
||||||
|
|
||||||
|
return super(
|
||||||
|
EventTypeSubscriptionListView, self
|
||||||
|
).form_valid(form=form)
|
||||||
|
|
||||||
|
def get_extra_context(self):
|
||||||
|
return {
|
||||||
|
'form_display_mode_table': True,
|
||||||
|
'object': self.get_object(),
|
||||||
|
'title': _(
|
||||||
|
'Event subscriptions'
|
||||||
|
) % self.get_object()
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
obj = self.get_object()
|
||||||
|
initial = []
|
||||||
|
|
||||||
|
for element in self.get_queryset():
|
||||||
|
initial.append({
|
||||||
|
'user': obj,
|
||||||
|
'main_model': self.main_model,
|
||||||
|
'stored_event_type': element,
|
||||||
|
})
|
||||||
|
return initial
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
return self.request.user
|
||||||
|
|
||||||
|
def get_object_list(self):
|
||||||
|
# Return the queryset by name from the sorted list of the class
|
||||||
|
event_type_ids = [event_type.id for event_type in EventType.all()]
|
||||||
|
return self.submodel.objects.filter(name__in=event_type_ids)
|
||||||
|
|
||||||
|
def get_post_action_redirect(self):
|
||||||
|
return reverse('common:current_user_details')
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationListView(SingleObjectListView):
|
||||||
|
def get_extra_context(self):
|
||||||
|
return {
|
||||||
|
'hide_object': True,
|
||||||
|
'object': self.request.user,
|
||||||
|
'title': _('Notifications'),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_object_list(self):
|
||||||
|
return self.request.user.notifications.all()
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationMarkRead(SimpleView):
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
self.get_queryset().filter(pk=self.kwargs['pk']).update(read=True)
|
||||||
|
return HttpResponseRedirect(reverse('events:user_notifications_list'))
|
||||||
|
|
||||||
|
def get_object_list(self):
|
||||||
|
return self.request.user.notifications.all()
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationMarkReadAll(SimpleView):
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
self.get_queryset().update(read=True)
|
||||||
|
return HttpResponseRedirect(reverse('events:user_notifications_list'))
|
||||||
|
|
||||||
|
def get_object_list(self):
|
||||||
|
return self.request.user.notifications.all()
|
||||||
|
|
||||||
|
|
||||||
class ObjectEventListView(EventListView):
|
class ObjectEventListView(EventListView):
|
||||||
view_permissions = None
|
view_permissions = None
|
||||||
|
|
||||||
@@ -73,6 +170,76 @@ class ObjectEventListView(EventListView):
|
|||||||
return any_stream(self.content_object)
|
return any_stream(self.content_object)
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectEventTypeSubscriptionListView(FormView):
|
||||||
|
form_class = ObjectEventTypeUserRelationshipFormSet
|
||||||
|
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
EventType.refresh()
|
||||||
|
return super(ObjectEventTypeSubscriptionListView, self).dispatch(*args, **kwargs)
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
try:
|
||||||
|
for instance in form:
|
||||||
|
instance.save()
|
||||||
|
except Exception as exception:
|
||||||
|
messages.error(
|
||||||
|
self.request,
|
||||||
|
_('Error updating object event subscription; %s') % exception
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
messages.success(
|
||||||
|
self.request, _('Object event subscriptions updated successfully')
|
||||||
|
)
|
||||||
|
|
||||||
|
return super(
|
||||||
|
ObjectEventTypeSubscriptionListView, self
|
||||||
|
).form_valid(form=form)
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
object_content_type = get_object_or_404(
|
||||||
|
ContentType, app_label=self.kwargs['app_label'],
|
||||||
|
model=self.kwargs['model']
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
content_object = object_content_type.get_object_for_this_type(
|
||||||
|
pk=self.kwargs['object_id']
|
||||||
|
)
|
||||||
|
except object_content_type.model_class().DoesNotExist:
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
AccessControlList.objects.check_access(
|
||||||
|
permissions=permission_events_view, user=self.request.user,
|
||||||
|
obj=content_object
|
||||||
|
)
|
||||||
|
|
||||||
|
return content_object
|
||||||
|
|
||||||
|
def get_extra_context(self):
|
||||||
|
return {
|
||||||
|
'form_display_mode_table': True,
|
||||||
|
'object': self.get_object(),
|
||||||
|
'title': _(
|
||||||
|
'Event subscriptions for: %s'
|
||||||
|
) % self.get_object()
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
obj = self.get_object()
|
||||||
|
initial = []
|
||||||
|
|
||||||
|
for element in self.get_object_list():
|
||||||
|
initial.append({
|
||||||
|
'user': self.request.user,
|
||||||
|
'object': obj,
|
||||||
|
'stored_event_type': element,
|
||||||
|
})
|
||||||
|
return initial
|
||||||
|
|
||||||
|
def get_object_list(self):
|
||||||
|
return ModelEventType.get_for_instance(instance=self.get_object())
|
||||||
|
|
||||||
|
|
||||||
class VerbEventListView(SingleObjectListView):
|
class VerbEventListView(SingleObjectListView):
|
||||||
def get_extra_context(self):
|
def get_extra_context(self):
|
||||||
return {
|
return {
|
||||||
@@ -87,7 +254,7 @@ class VerbEventListView(SingleObjectListView):
|
|||||||
'hide_object': True,
|
'hide_object': True,
|
||||||
'title': _(
|
'title': _(
|
||||||
'Events of type: %s'
|
'Events of type: %s'
|
||||||
) % Event.get_label(self.kwargs['verb']),
|
) % EventType.get(name=self.kwargs['verb']),
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_object_list(self):
|
def get_object_list(self):
|
||||||
|
|||||||
@@ -1,23 +1,28 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.encoding import force_text
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from .classes import Event
|
from .classes import EventType
|
||||||
|
|
||||||
|
|
||||||
def event_object_link(entry, attribute='target'):
|
def event_object_link(entry, attribute='target'):
|
||||||
|
label = ''
|
||||||
|
url = '#'
|
||||||
|
obj_type = ''
|
||||||
|
|
||||||
obj = getattr(entry, attribute)
|
obj = getattr(entry, attribute)
|
||||||
|
|
||||||
if obj:
|
if obj:
|
||||||
obj_type = '{}: '.format(obj._meta.verbose_name)
|
obj_type = '{}: '.format(obj._meta.verbose_name)
|
||||||
else:
|
if hasattr(obj, 'get_absolute_url'):
|
||||||
obj_type = ''
|
url = obj.get_absolute_url()
|
||||||
|
label = force_text(obj)
|
||||||
|
|
||||||
return mark_safe(
|
return mark_safe(
|
||||||
'<a href="%(url)s">%(obj_type)s%(label)s</a>' % {
|
'<a href="%(url)s">%(obj_type)s%(label)s</a>' % {
|
||||||
'url': obj.get_absolute_url() if obj else '#',
|
'url': url, 'label': label, 'obj_type': obj_type
|
||||||
'label': obj or '', 'obj_type': obj_type
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,6 +31,6 @@ def event_type_link(entry):
|
|||||||
return mark_safe(
|
return mark_safe(
|
||||||
'<a href="%(url)s">%(label)s</a>' % {
|
'<a href="%(url)s">%(label)s</a>' % {
|
||||||
'url': reverse('events:events_by_verb', kwargs={'verb': entry.verb}),
|
'url': reverse('events:events_by_verb', kwargs={'verb': entry.verb}),
|
||||||
'label': Event.get_label(entry.verb)
|
'label': EventType.get(name=entry.verb)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
20
mayan/apps/metadata/migrations/0008_auto_20170904_1730.py
Normal file
20
mayan/apps/metadata/migrations/0008_auto_20170904_1730.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2017-09-04 17:30
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('metadata', '0007_auto_20150918_0800'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='documentmetadata',
|
||||||
|
name='value',
|
||||||
|
field=models.TextField(blank=True, db_index=True, null=True, verbose_name='Value'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -157,8 +157,8 @@ class DocumentMetadata(models.Model):
|
|||||||
metadata_type = models.ForeignKey(
|
metadata_type = models.ForeignKey(
|
||||||
MetadataType, on_delete=models.CASCADE, verbose_name=_('Type')
|
MetadataType, on_delete=models.CASCADE, verbose_name=_('Type')
|
||||||
)
|
)
|
||||||
value = models.CharField(
|
value = models.TextField(
|
||||||
blank=True, db_index=True, max_length=255, null=True,
|
blank=True, db_index=True, null=True,
|
||||||
verbose_name=_('Value')
|
verbose_name=_('Value')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -190,6 +190,25 @@ class MetadataTestCase(MetadataTypeMixin, BaseTestCase):
|
|||||||
self.metadata_type.save()
|
self.metadata_type.save()
|
||||||
self.metadata_type.validate_value(document_type=None, value='test1')
|
self.metadata_type.validate_value(document_type=None, value='test1')
|
||||||
|
|
||||||
|
def test_long_value_for_metadata(self):
|
||||||
|
"""
|
||||||
|
asserts that DocumentMetadata.value can store more than 255 chars
|
||||||
|
"""
|
||||||
|
very_long_string = (
|
||||||
|
'Mayan EDMS is a Free Open Source Electronic '
|
||||||
|
'Document Management System, coded in the Python language '
|
||||||
|
'using the Django web application framework and released '
|
||||||
|
'under the Apache 2.0 License. It provides an electronic '
|
||||||
|
'vault or repository for electronic documents.'
|
||||||
|
) # 258 chars
|
||||||
|
document_metadata = DocumentMetadata(
|
||||||
|
document=self.document, metadata_type=self.metadata_type,
|
||||||
|
value=very_long_string
|
||||||
|
)
|
||||||
|
document_metadata.full_clean()
|
||||||
|
document_metadata.save()
|
||||||
|
self.assertEqual(very_long_string, document_metadata.value)
|
||||||
|
|
||||||
def test_add_new_metadata_type_on_document_type_change(self):
|
def test_add_new_metadata_type_on_document_type_change(self):
|
||||||
"""
|
"""
|
||||||
When switching document types, add the required metadata of the new
|
When switching document types, add the required metadata of the new
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
from django.utils.html import format_html_join
|
||||||
|
|
||||||
from django.utils.html import format_html_join
|
from django.utils.html import format_html_join
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
from rest_framework import generics, status
|
from rest_framework import generics, status
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
from documents.models import Document, DocumentPage, DocumentVersion
|
from documents.models import Document, DocumentPage, DocumentVersion
|
||||||
from rest_api.permissions import MayanPermission
|
from rest_api.permissions import MayanPermission
|
||||||
|
|
||||||
@@ -26,10 +28,6 @@ class APIDocumentOCRView(generics.GenericAPIView):
|
|||||||
Submit a document for OCR.
|
Submit a document for OCR.
|
||||||
---
|
---
|
||||||
omit_serializer: true
|
omit_serializer: true
|
||||||
parameters:
|
|
||||||
- name: pk
|
|
||||||
paramType: path
|
|
||||||
type: number
|
|
||||||
responseMessages:
|
responseMessages:
|
||||||
- code: 202
|
- code: 202
|
||||||
message: Accepted
|
message: Accepted
|
||||||
@@ -40,12 +38,19 @@ class APIDocumentOCRView(generics.GenericAPIView):
|
|||||||
|
|
||||||
|
|
||||||
class APIDocumentVersionOCRView(generics.GenericAPIView):
|
class APIDocumentVersionOCRView(generics.GenericAPIView):
|
||||||
|
lookup_url_kwarg = 'version_pk'
|
||||||
mayan_object_permissions = {
|
mayan_object_permissions = {
|
||||||
'POST': (permission_ocr_document,)
|
'POST': (permission_ocr_document,)
|
||||||
}
|
}
|
||||||
permission_classes = (MayanPermission,)
|
permission_classes = (MayanPermission,)
|
||||||
queryset = DocumentVersion.objects.all()
|
queryset = DocumentVersion.objects.all()
|
||||||
|
|
||||||
|
def get_document(self):
|
||||||
|
return get_object_or_404(Document, pk=self.kwargs['document_pk'])
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.get_document().versions.all()
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -54,10 +59,6 @@ class APIDocumentVersionOCRView(generics.GenericAPIView):
|
|||||||
Submit a document version for OCR.
|
Submit a document version for OCR.
|
||||||
---
|
---
|
||||||
omit_serializer: true
|
omit_serializer: true
|
||||||
parameters:
|
|
||||||
- name: pk
|
|
||||||
paramType: path
|
|
||||||
type: number
|
|
||||||
responseMessages:
|
responseMessages:
|
||||||
- code: 202
|
- code: 202
|
||||||
message: Accepted
|
message: Accepted
|
||||||
@@ -70,20 +71,25 @@ class APIDocumentVersionOCRView(generics.GenericAPIView):
|
|||||||
class APIDocumentPageOCRContentView(generics.RetrieveAPIView):
|
class APIDocumentPageOCRContentView(generics.RetrieveAPIView):
|
||||||
"""
|
"""
|
||||||
Returns the OCR content of the selected document page.
|
Returns the OCR content of the selected document page.
|
||||||
---
|
|
||||||
GET:
|
|
||||||
parameters:
|
|
||||||
- name: pk
|
|
||||||
paramType: path
|
|
||||||
type: number
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
lookup_url_kwarg = 'page_pk'
|
||||||
mayan_object_permissions = {
|
mayan_object_permissions = {
|
||||||
'GET': (permission_ocr_content_view,),
|
'GET': (permission_ocr_content_view,),
|
||||||
}
|
}
|
||||||
permission_classes = (MayanPermission,)
|
permission_classes = (MayanPermission,)
|
||||||
serializer_class = DocumentPageOCRContentSerializer
|
serializer_class = DocumentPageOCRContentSerializer
|
||||||
queryset = DocumentPage.objects.all()
|
|
||||||
|
def get_document(self):
|
||||||
|
return get_object_or_404(Document, pk=self.kwargs['document_pk'])
|
||||||
|
|
||||||
|
def get_document_version(self):
|
||||||
|
return get_object_or_404(
|
||||||
|
self.get_document().versions.all(), pk=self.kwargs['version_pk']
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.get_document_version().pages.all()
|
||||||
|
|
||||||
def retrieve(self, request, *args, **kwargs):
|
def retrieve(self, request, *args, **kwargs):
|
||||||
instance = self.get_object()
|
instance = self.get_object()
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from events.classes import Event
|
from events.classes import EventTypeNamespace
|
||||||
|
|
||||||
event_ocr_document_version_submit = Event(
|
namespace = EventTypeNamespace(name='ocr', label=_('OCR'))
|
||||||
name='ocr_document_version_submit',
|
|
||||||
label=_('Document version submitted for OCR')
|
event_ocr_document_version_submit = namespace.add_event_type(
|
||||||
|
label=_('Document version submitted for OCR'),
|
||||||
|
name='document_version_submit'
|
||||||
)
|
)
|
||||||
event_ocr_document_version_finish = Event(
|
event_ocr_document_version_finish = namespace.add_event_type(
|
||||||
name='ocr_document_version_finish',
|
label=_('Document version OCR finished'),
|
||||||
label=_('Document version OCR finished')
|
name='document_version_finish'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -63,7 +63,9 @@ class OCRAPITestCase(BaseAPITestCase):
|
|||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
reverse(
|
reverse(
|
||||||
'rest_api:document-version-ocr-submit-view',
|
'rest_api:document-version-ocr-submit-view',
|
||||||
args=(self.document.latest_version.pk,)
|
args=(
|
||||||
|
self.document.pk, self.document.latest_version.pk,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -77,7 +79,10 @@ class OCRAPITestCase(BaseAPITestCase):
|
|||||||
response = self.client.get(
|
response = self.client.get(
|
||||||
reverse(
|
reverse(
|
||||||
'rest_api:document-page-content-view',
|
'rest_api:document-page-content-view',
|
||||||
args=(self.document.latest_version.pages.first().pk,)
|
args=(
|
||||||
|
self.document.pk, self.document.latest_version.pk,
|
||||||
|
self.document.latest_version.pages.first().pk,
|
||||||
|
)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class OCREventsTestCase(GenericDocumentTestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
Action.objects.last().verb,
|
Action.objects.last().verb,
|
||||||
event_ocr_document_version_submit.name
|
event_ocr_document_version_submit.id
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_document_version_finish_event(self):
|
def test_document_version_finish_event(self):
|
||||||
@@ -31,5 +31,5 @@ class OCREventsTestCase(GenericDocumentTestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
Action.objects.first().verb,
|
Action.objects.first().verb,
|
||||||
event_ocr_document_version_finish.name
|
event_ocr_document_version_finish.id
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -47,16 +47,16 @@ urlpatterns = [
|
|||||||
|
|
||||||
api_urls = [
|
api_urls = [
|
||||||
url(
|
url(
|
||||||
r'^document/(?P<pk>\d+)/submit/$', APIDocumentOCRView.as_view(),
|
r'^documents/(?P<pk>\d+)/ocr/$', APIDocumentOCRView.as_view(),
|
||||||
name='document-ocr-submit-view'
|
name='document-ocr-submit-view'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r'^document_version/(?P<pk>\d+)/submit/$',
|
r'^documents/(?P<document_pk>\d+)/versions/(?P<version_pk>\d+)/ocr/$',
|
||||||
APIDocumentVersionOCRView.as_view(),
|
APIDocumentVersionOCRView.as_view(),
|
||||||
name='document-version-ocr-submit-view'
|
name='document-version-ocr-submit-view'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r'^page/(?P<pk>\d+)/content/$',
|
r'^documents/(?P<document_pk>\d+)/versions/(?P<version_pk>\d+)/pages/(?P<page_pk>\d+)/ocr/$',
|
||||||
APIDocumentPageOCRContentView.as_view(),
|
APIDocumentPageOCRContentView.as_view(),
|
||||||
name='document-page-content-view'
|
name='document-page-content-view'
|
||||||
),
|
),
|
||||||
|
|||||||
3
mayan/apps/quotas/__init__.py
Normal file
3
mayan/apps/quotas/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
default_app_config = 'quotas.apps.QuotasApp'
|
||||||
24
mayan/apps/quotas/admin.py
Normal file
24
mayan/apps/quotas/admin.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
from .models import Quota
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Quota)
|
||||||
|
class QuotaAdmin(admin.ModelAdmin):
|
||||||
|
list_display = (
|
||||||
|
'backend_path', 'backend_data', 'enabled', 'editable',
|
||||||
|
)
|
||||||
|
|
||||||
|
def has_change_permission(self, request, obj=None):
|
||||||
|
if obj:
|
||||||
|
return obj.editable
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def has_delete_permission(self, request, obj=None):
|
||||||
|
if obj:
|
||||||
|
return obj.editable
|
||||||
|
else:
|
||||||
|
return False
|
||||||
85
mayan/apps/quotas/apps.py
Normal file
85
mayan/apps/quotas/apps.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db.utils import OperationalError, ProgrammingError
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from acls import ModelPermission
|
||||||
|
from acls.links import link_acl_list
|
||||||
|
from acls.permissions import permission_acl_edit, permission_acl_view
|
||||||
|
from common import MayanAppConfig, menu_object, menu_secondary, menu_setup
|
||||||
|
from common.widgets import two_state_template
|
||||||
|
from navigation import SourceColumn
|
||||||
|
|
||||||
|
from .classes import QuotaBackend
|
||||||
|
from .links import (
|
||||||
|
link_quota_create, link_quota_delete, link_quota_edit, link_quota_list,
|
||||||
|
link_quota_setup
|
||||||
|
)
|
||||||
|
from .permissions import (
|
||||||
|
permission_quota_delete, permission_quota_edit, permission_quota_view
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class QuotasApp(MayanAppConfig):
|
||||||
|
name = 'quotas'
|
||||||
|
verbose_name = _('Quotas')
|
||||||
|
|
||||||
|
def ready(self, *args, **kwargs):
|
||||||
|
super(QuotasApp, self).ready(*args, **kwargs)
|
||||||
|
Quota = self.get_model('Quota')
|
||||||
|
|
||||||
|
QuotaBackend.initialize()
|
||||||
|
|
||||||
|
try:
|
||||||
|
for quota in Quota.objects.all():
|
||||||
|
quota.update_receiver()
|
||||||
|
except (OperationalError, ProgrammingError):
|
||||||
|
# Ignore errors during migration
|
||||||
|
pass
|
||||||
|
|
||||||
|
ModelPermission.register(
|
||||||
|
model=Quota, permissions=(
|
||||||
|
permission_acl_edit, permission_acl_view,
|
||||||
|
permission_quota_delete, permission_quota_edit,
|
||||||
|
permission_quota_view
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
SourceColumn(
|
||||||
|
source=Quota, label=_('Backend'), attribute='backend_label'
|
||||||
|
)
|
||||||
|
SourceColumn(
|
||||||
|
source=Quota, label=_('Display'), attribute='backend_display'
|
||||||
|
)
|
||||||
|
SourceColumn(
|
||||||
|
source=Quota, label=_('Usage'), attribute='backend_usage'
|
||||||
|
)
|
||||||
|
SourceColumn(
|
||||||
|
source=Quota, label=_('Enabled?'),
|
||||||
|
func=lambda context: two_state_template(
|
||||||
|
context['object'].enabled
|
||||||
|
)
|
||||||
|
)
|
||||||
|
SourceColumn(
|
||||||
|
source=Quota, label=_('Editable?'),
|
||||||
|
func=lambda context: two_state_template(
|
||||||
|
context['object'].editable
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
menu_object.bind_links(
|
||||||
|
links=(
|
||||||
|
link_quota_edit, link_acl_list, link_quota_delete,
|
||||||
|
), sources=(Quota,)
|
||||||
|
)
|
||||||
|
|
||||||
|
menu_secondary.bind_links(
|
||||||
|
links=(
|
||||||
|
link_quota_list, link_quota_create,
|
||||||
|
), sources=(
|
||||||
|
Quota, 'quotas:quota_backend_selection', 'quotas:quota_create',
|
||||||
|
'quotas:quota_list',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
menu_setup.bind_links(links=(link_quota_setup,))
|
||||||
81
mayan/apps/quotas/classes.py
Normal file
81
mayan/apps/quotas/classes.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from importlib import import_module
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
|
from django.utils import six
|
||||||
|
from django.utils.encoding import force_text
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
__ALL__ = ('QuotaBackend',)
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaBackendMetaclass(type):
|
||||||
|
_registry = {}
|
||||||
|
|
||||||
|
def __new__(mcs, name, bases, attrs):
|
||||||
|
new_class = super(QuotaBackendMetaclass, mcs).__new__(
|
||||||
|
mcs, name, bases, attrs
|
||||||
|
)
|
||||||
|
if not new_class.__module__ == 'quotas.classes':
|
||||||
|
mcs._registry[
|
||||||
|
'{}.{}'.format(new_class.__module__, name)
|
||||||
|
] = new_class
|
||||||
|
new_class.id = '{}.{}'.format(new_class.__module__, name)
|
||||||
|
|
||||||
|
return new_class
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaBackendBase(object):
|
||||||
|
"""
|
||||||
|
Base class for the mailing backends. This class is mainly a wrapper
|
||||||
|
for other Django backends that adds a few metadata to specify the
|
||||||
|
fields it needs to be instanciated at runtime.
|
||||||
|
|
||||||
|
The fields attribute is a list of dictionaries with the format:
|
||||||
|
{
|
||||||
|
'name': '' # Field internal name
|
||||||
|
'label': '' # Label to show to users
|
||||||
|
'class': '' # Field class to use. Field classes are Python dot
|
||||||
|
paths to Django's form fields.
|
||||||
|
'initial': '' # Field initial value
|
||||||
|
'default': '' # Default value.
|
||||||
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
fields = ()
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaBackend(six.with_metaclass(QuotaBackendMetaclass, QuotaBackendBase)):
|
||||||
|
@classmethod
|
||||||
|
def get(cls, name):
|
||||||
|
return cls._registry[name]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_all(cls):
|
||||||
|
return sorted(
|
||||||
|
cls._registry.values(), key=lambda x: x.label
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def as_choices(cls):
|
||||||
|
return [
|
||||||
|
(
|
||||||
|
backend.id, backend.label
|
||||||
|
) for backend in QuotaBackend.get_all()
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def initialize():
|
||||||
|
for app in apps.get_app_configs():
|
||||||
|
try:
|
||||||
|
import_module('{}.quota_backends'.format(app.name))
|
||||||
|
except ImportError as exception:
|
||||||
|
if force_text(exception) != 'No module named quota_backends':
|
||||||
|
logger.error(
|
||||||
|
'Error importing %s quota_backends.py file; %s',
|
||||||
|
app.name, exception
|
||||||
|
)
|
||||||
12
mayan/apps/quotas/exceptions.py
Normal file
12
mayan/apps/quotas/exceptions.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaBaseException(Exception):
|
||||||
|
"""
|
||||||
|
Base exception for the quota app
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaExceeded(QuotaBaseException):
|
||||||
|
pass
|
||||||
50
mayan/apps/quotas/forms.py
Normal file
50
mayan/apps/quotas/forms.py
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from common.forms import DynamicModelForm
|
||||||
|
|
||||||
|
from .classes import QuotaBackend
|
||||||
|
from .models import Quota
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaBackendSelectionForm(forms.Form):
|
||||||
|
backend = forms.ChoiceField(choices=(), label=_('Backend'))
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(QuotaBackendSelectionForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.fields['backend'].choices = QuotaBackend.as_choices()
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaDynamicForm(DynamicModelForm):
|
||||||
|
class Meta:
|
||||||
|
fields = ('enabled', 'backend_data')
|
||||||
|
model = Quota
|
||||||
|
widgets = {'backend_data': forms.widgets.HiddenInput}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
result = super(QuotaDynamicForm, self).__init__(*args, **kwargs)
|
||||||
|
if self.instance.backend_data:
|
||||||
|
for key, value in json.loads(self.instance.backend_data).items():
|
||||||
|
self.fields[key].initial = value
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
data = super(QuotaDynamicForm, self).clean()
|
||||||
|
|
||||||
|
# Consolidate the dynamic fields into a single JSON field called
|
||||||
|
# 'backend_data'.
|
||||||
|
backend_data = {}
|
||||||
|
|
||||||
|
for field in self.schema['fields']:
|
||||||
|
backend_data[field['name']] = data.pop(
|
||||||
|
field['name'], field.get('default', None)
|
||||||
|
)
|
||||||
|
|
||||||
|
data['backend_data'] = json.dumps(backend_data)
|
||||||
|
return data
|
||||||
13
mayan/apps/quotas/handlers.py
Normal file
13
mayan/apps/quotas/handlers.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.apps import apps
|
||||||
|
|
||||||
|
|
||||||
|
def handler_process_signal(sender, **kwargs):
|
||||||
|
Quota = apps.get_model(app_label='quotas', model_name='Quota')
|
||||||
|
|
||||||
|
for quota in Quota.objects.filter(enabled=True):
|
||||||
|
backend_instance = quota.get_backend_instance()
|
||||||
|
|
||||||
|
if backend_instance.sender == sender and kwargs['signal'].__class__ == backend_instance.signal.__class__:
|
||||||
|
backend_instance.process(**kwargs)
|
||||||
38
mayan/apps/quotas/links.py
Normal file
38
mayan/apps/quotas/links.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from navigation import Link
|
||||||
|
|
||||||
|
from .permissions import (
|
||||||
|
permission_quota_create, permission_quota_delete,
|
||||||
|
permission_quota_edit, permission_quota_view,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_not_editable(context):
|
||||||
|
return not context['object'].editable
|
||||||
|
|
||||||
|
|
||||||
|
link_quota_create = Link(
|
||||||
|
icon='fa fa-envelope', permissions=(permission_quota_create,),
|
||||||
|
text=_('Quota create'), view='quotas:quota_backend_selection',
|
||||||
|
)
|
||||||
|
link_quota_delete = Link(
|
||||||
|
args='resolved_object.pk', conditional_disable=is_not_editable,
|
||||||
|
permissions=(permission_quota_delete,), tags='dangerous', text=_('Delete'),
|
||||||
|
view='quotas:quota_delete',
|
||||||
|
)
|
||||||
|
link_quota_edit = Link(
|
||||||
|
args='object.pk', conditional_disable=is_not_editable,
|
||||||
|
permissions=(permission_quota_edit,), text=_('Edit'),
|
||||||
|
view='quotas:quota_edit',
|
||||||
|
)
|
||||||
|
link_quota_list = Link(
|
||||||
|
icon='fa fa-envelope', permissions=(permission_quota_view,),
|
||||||
|
text=_('Quotas list'), view='quotas:quota_list',
|
||||||
|
)
|
||||||
|
link_quota_setup = Link(
|
||||||
|
icon='fa fa-dashboard', permissions=(permission_quota_view,),
|
||||||
|
text=_('Quotas'), view='quotas:quota_list',
|
||||||
|
)
|
||||||
29
mayan/apps/quotas/migrations/0001_initial.py
Normal file
29
mayan/apps/quotas/migrations/0001_initial.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2017-08-01 06:26
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Quota',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('backend_path', models.CharField(help_text='The dotted Python path to the backend class.', max_length=255, verbose_name='Backend path')),
|
||||||
|
('backend_data', models.TextField(blank=True, verbose_name='Backend data')),
|
||||||
|
('enabled', models.BooleanField(default=True, verbose_name='Enabled')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Quota',
|
||||||
|
'verbose_name_plural': 'Quotas',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
20
mayan/apps/quotas/migrations/0002_quota_editable.py
Normal file
20
mayan/apps/quotas/migrations/0002_quota_editable.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2017-08-01 07:57
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('quotas', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='quota',
|
||||||
|
name='editable',
|
||||||
|
field=models.BooleanField(default=True, editable=False, verbose_name='Editable'),
|
||||||
|
),
|
||||||
|
]
|
||||||
0
mayan/apps/quotas/migrations/__init__.py
Normal file
0
mayan/apps/quotas/migrations/__init__.py
Normal file
84
mayan/apps/quotas/models.py
Normal file
84
mayan/apps/quotas/models.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.db import models
|
||||||
|
from django.utils.encoding import force_text
|
||||||
|
from django.utils.module_loading import import_string
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from .handlers import handler_process_signal
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Quota(models.Model):
|
||||||
|
backend_path = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
help_text=_('The dotted Python path to the backend class.'),
|
||||||
|
verbose_name=_('Backend path')
|
||||||
|
)
|
||||||
|
backend_data = models.TextField(
|
||||||
|
blank=True, verbose_name=_('Backend data')
|
||||||
|
)
|
||||||
|
enabled = models.BooleanField(default=True, verbose_name=_('Enabled'))
|
||||||
|
editable = models.BooleanField(
|
||||||
|
default=True, editable=False, verbose_name=_('Editable')
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Quota')
|
||||||
|
verbose_name_plural = _('Quotas')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return force_text(self.backend_label())
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
result = super(Quota, self).save(*args, **kwargs)
|
||||||
|
self.update_receiver()
|
||||||
|
return result
|
||||||
|
|
||||||
|
def backend_display(self):
|
||||||
|
return self.get_backend_instance().display()
|
||||||
|
|
||||||
|
def backend_label(self):
|
||||||
|
return self.get_backend_instance().label
|
||||||
|
|
||||||
|
def backend_usage(self):
|
||||||
|
return self.get_backend_instance().usage()
|
||||||
|
|
||||||
|
def dispatch_uid(self):
|
||||||
|
return 'quote_{}'.format(self.pk)
|
||||||
|
|
||||||
|
def dumps(self, data):
|
||||||
|
self.backend_data = json.dumps(data)
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def get_backend_class(self):
|
||||||
|
return import_string(self.backend_path)
|
||||||
|
|
||||||
|
def get_backend_instance(self):
|
||||||
|
return self.get_backend_class()(**self.loads())
|
||||||
|
|
||||||
|
def loads(self):
|
||||||
|
return json.loads(self.backend_data)
|
||||||
|
|
||||||
|
def update_receiver(self):
|
||||||
|
backend_instance = self.get_backend_instance()
|
||||||
|
|
||||||
|
if self.enabled:
|
||||||
|
backend_instance.signal.disconnect(
|
||||||
|
dispatch_uid=self.dispatch_uid(),
|
||||||
|
sender=backend_instance.sender
|
||||||
|
)
|
||||||
|
backend_instance.signal.connect(
|
||||||
|
handler_process_signal,
|
||||||
|
dispatch_uid=self.dispatch_uid(),
|
||||||
|
sender=backend_instance.sender
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
backend_instance.signal.disconnect(
|
||||||
|
dispatch_uid=self.dispatch_uid(),
|
||||||
|
sender=backend_instance.sender
|
||||||
|
)
|
||||||
20
mayan/apps/quotas/permissions.py
Normal file
20
mayan/apps/quotas/permissions.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from permissions import PermissionNamespace
|
||||||
|
|
||||||
|
namespace = PermissionNamespace('quotas', _('Quotas'))
|
||||||
|
|
||||||
|
permission_quota_create = namespace.add_permission(
|
||||||
|
name='quota_create', label=_('Create a quota')
|
||||||
|
)
|
||||||
|
permission_quota_delete = namespace.add_permission(
|
||||||
|
name='quota_delete', label=_('Delete a quota')
|
||||||
|
)
|
||||||
|
permission_quota_edit = namespace.add_permission(
|
||||||
|
name='quota_edit', label=_('Edit a quota')
|
||||||
|
)
|
||||||
|
permission_quota_view = namespace.add_permission(
|
||||||
|
name='quota_view', label=_('View a quota')
|
||||||
|
)
|
||||||
211
mayan/apps/quotas/quota_backends.py
Normal file
211
mayan/apps/quotas/quota_backends.py
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db.models.signals import pre_save
|
||||||
|
from django.template.defaultfilters import filesizeformat
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from actstream.models import actor_stream
|
||||||
|
|
||||||
|
from documents.events import event_document_create, event_document_new_version
|
||||||
|
from documents.models import Document, DocumentVersion
|
||||||
|
|
||||||
|
from .classes import QuotaBackend
|
||||||
|
from .exceptions import QuotaExceeded
|
||||||
|
|
||||||
|
__all__ = ('DocumentStorageQuota', 'DocumentCountQuota',)
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentCountQuota(QuotaBackend):
|
||||||
|
fields = (
|
||||||
|
{
|
||||||
|
'name': 'documents_limit', 'label': _('Documents limit'),
|
||||||
|
'class': 'django.forms.IntegerField',
|
||||||
|
'help_text': _('Maximum number of documents')
|
||||||
|
},
|
||||||
|
)
|
||||||
|
label = _('Document count')
|
||||||
|
sender = Document
|
||||||
|
signal = pre_save
|
||||||
|
|
||||||
|
def __init__(self, documents_limit):
|
||||||
|
self.documents_limit = documents_limit
|
||||||
|
|
||||||
|
def _allowed(self):
|
||||||
|
return self.documents_limit
|
||||||
|
|
||||||
|
def _usage(self, **kwargs):
|
||||||
|
return Document.passthrough.all().count()
|
||||||
|
|
||||||
|
def display(self):
|
||||||
|
return _(
|
||||||
|
'Maximum document count: %(total_documents)s'
|
||||||
|
) % {
|
||||||
|
'total_documents': self._allowed(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def process(self, **kwargs):
|
||||||
|
if self._usage() > self._allowed():
|
||||||
|
raise QuotaExceeded('Document count exceeded')
|
||||||
|
|
||||||
|
def usage(self):
|
||||||
|
return _('%(usage)s out of %(allowed)s') % {
|
||||||
|
'usage': self._usage(),
|
||||||
|
'allowed': self._allowed()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentStorageQuota(QuotaBackend):
|
||||||
|
fields = (
|
||||||
|
{
|
||||||
|
'name': 'storage_size', 'label': _('Storage size'),
|
||||||
|
'class': 'django.forms.FloatField',
|
||||||
|
'help_text': _('Total storage usage in megabytes (MB)')
|
||||||
|
},
|
||||||
|
)
|
||||||
|
label = _('Document storage')
|
||||||
|
sender = Document
|
||||||
|
signal = pre_save
|
||||||
|
|
||||||
|
def __init__(self, storage_size):
|
||||||
|
self.storage_size = storage_size
|
||||||
|
|
||||||
|
def _allowed(self):
|
||||||
|
return self.storage_size * 1024 * 1024
|
||||||
|
|
||||||
|
def _usage(self, **kwargs):
|
||||||
|
total_usage = 0
|
||||||
|
for document_version in DocumentVersion.objects.all():
|
||||||
|
if document_version.exists():
|
||||||
|
total_usage += document_version.file.size
|
||||||
|
|
||||||
|
return total_usage
|
||||||
|
|
||||||
|
def display(self):
|
||||||
|
return _(
|
||||||
|
'Maximum storage usage: %(formatted_file_size)s (%(raw_file_size)s MB)'
|
||||||
|
) % {
|
||||||
|
'formatted_file_size': filesizeformat(self._allowed()),
|
||||||
|
'raw_file_size': self.storage_size
|
||||||
|
}
|
||||||
|
|
||||||
|
def process(self, **kwargs):
|
||||||
|
if self._usage() > self.storage_size * 1024 * 1024:
|
||||||
|
raise QuotaExceeded('Storage usage exceeded')
|
||||||
|
|
||||||
|
def usage(self):
|
||||||
|
return _('%(usage)s out of %(allowed)s') % {
|
||||||
|
'usage': filesizeformat(self._usage()),
|
||||||
|
'allowed': filesizeformat(self._allowed())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class UserDocumentCountQuota(QuotaBackend):
|
||||||
|
fields = (
|
||||||
|
{
|
||||||
|
'name': 'username', 'label': _('Username'),
|
||||||
|
'class': 'django.forms.CharField', 'kwargs': {
|
||||||
|
'max_length': 255
|
||||||
|
}, 'help_text': _(
|
||||||
|
'Username of the user to which the quota will be applied'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'documents_limit', 'label': _('Documents limit'),
|
||||||
|
'class': 'django.forms.IntegerField',
|
||||||
|
'help_text': _('Maximum number of documents')
|
||||||
|
},
|
||||||
|
)
|
||||||
|
label = _('User document count')
|
||||||
|
sender = Document
|
||||||
|
signal = pre_save
|
||||||
|
|
||||||
|
def __init__(self, documents_limit, username):
|
||||||
|
self.documents_limit = documents_limit
|
||||||
|
self.username = username
|
||||||
|
|
||||||
|
def _allowed(self):
|
||||||
|
return self.documents_limit
|
||||||
|
|
||||||
|
def _usage(self, **kwargs):
|
||||||
|
user = get_user_model().objects.get(username=self.username)
|
||||||
|
return actor_stream(user).filter(verb=event_document_create.id).count()
|
||||||
|
|
||||||
|
def display(self):
|
||||||
|
user = get_user_model().objects.get(username=self.username)
|
||||||
|
return _(
|
||||||
|
'Maximum document count: %(total_documents)s, for user: %(user)s'
|
||||||
|
) % {
|
||||||
|
'total_documents': self._allowed(),
|
||||||
|
'user': user.get_full_name() or user
|
||||||
|
}
|
||||||
|
|
||||||
|
def process(self, **kwargs):
|
||||||
|
if self._usage() > self._allowed():
|
||||||
|
raise QuotaExceeded('Document count exceeded')
|
||||||
|
|
||||||
|
def usage(self):
|
||||||
|
return _('%(usage)s out of %(allowed)s') % {
|
||||||
|
'usage': self._usage(),
|
||||||
|
'allowed': self._allowed()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
###
|
||||||
|
class UserDocumentStorageQuota(QuotaBackend):
|
||||||
|
fields = (
|
||||||
|
{
|
||||||
|
'name': 'username', 'label': _('Username'),
|
||||||
|
'class': 'django.forms.CharField', 'kwargs': {
|
||||||
|
'max_length': 255
|
||||||
|
}, 'help_text': _(
|
||||||
|
'Username of the user to which the quota will be applied'
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'storage_size', 'label': _('Storage size'),
|
||||||
|
'class': 'django.forms.FloatField',
|
||||||
|
'help_text': _('Total storage usage in megabytes (MB)')
|
||||||
|
},
|
||||||
|
)
|
||||||
|
label = _('User document storage')
|
||||||
|
sender = Document
|
||||||
|
signal = pre_save
|
||||||
|
|
||||||
|
def __init__(self, storage_size, username):
|
||||||
|
self.storage_size = storage_size
|
||||||
|
self.username = username
|
||||||
|
|
||||||
|
def _allowed(self):
|
||||||
|
return self.storage_size * 1024 * 1024
|
||||||
|
|
||||||
|
def _usage(self, **kwargs):
|
||||||
|
total_usage = 0
|
||||||
|
user = get_user_model().objects.get(username=self.username)
|
||||||
|
content_type = ContentType.objects.get_for_model(model=user)
|
||||||
|
for document_version in DocumentVersion.objects.filter(target_actions__actor_object_id=1, target_actions__actor_content_type=content_type, target_actions__verb=event_document_new_version.id):
|
||||||
|
if document_version.exists():
|
||||||
|
total_usage += document_version.file.size
|
||||||
|
|
||||||
|
return total_usage
|
||||||
|
|
||||||
|
def display(self):
|
||||||
|
user = get_user_model().objects.get(username=self.username)
|
||||||
|
return _(
|
||||||
|
'Maximum storage usage: %(formatted_file_size)s (%(raw_file_size)s MB), for user %(user)s'
|
||||||
|
) % {
|
||||||
|
'formatted_file_size': filesizeformat(self._allowed()),
|
||||||
|
'raw_file_size': self.storage_size,
|
||||||
|
'user': user.get_full_name() or user
|
||||||
|
}
|
||||||
|
|
||||||
|
def process(self, **kwargs):
|
||||||
|
if self._usage() > self._allowed():
|
||||||
|
raise QuotaExceeded('Document count exceeded')
|
||||||
|
|
||||||
|
def usage(self):
|
||||||
|
return _('%(usage)s out of %(allowed)s') % {
|
||||||
|
'usage': filesizeformat(self._usage()),
|
||||||
|
'allowed': filesizeformat(self._allowed())
|
||||||
|
}
|
||||||
32
mayan/apps/quotas/urls.py
Normal file
32
mayan/apps/quotas/urls.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf.urls import url
|
||||||
|
|
||||||
|
from .views import (
|
||||||
|
QuotaBackendSelectionView, QuotaCreateView, QuotaDeleteView,
|
||||||
|
QuotaEditView, QuotaListView
|
||||||
|
)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
url(
|
||||||
|
r'^quotas/backend/selection/$',
|
||||||
|
QuotaBackendSelectionView.as_view(),
|
||||||
|
name='quota_backend_selection'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^quotas/(?P<class_path>[a-zA-Z0-9_.]+)/create/$',
|
||||||
|
QuotaCreateView.as_view(), name='quota_create'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^quotas/(?P<pk>\d+)/delete/$', QuotaDeleteView.as_view(),
|
||||||
|
name='quota_delete'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^quotas/(?P<pk>\d+)/edit/$', QuotaEditView.as_view(),
|
||||||
|
name='quota_edit'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^quotas/$', QuotaListView.as_view(),
|
||||||
|
name='quota_list'
|
||||||
|
),
|
||||||
|
]
|
||||||
108
mayan/apps/quotas/views.py
Normal file
108
mayan/apps/quotas/views.py
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from django.http import Http404, HttpResponseRedirect
|
||||||
|
from django.urls import reverse, reverse_lazy
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from common.generics import (
|
||||||
|
FormView, SingleObjectDeleteView, SingleObjectDynamicFormCreateView,
|
||||||
|
SingleObjectDynamicFormEditView, SingleObjectListView
|
||||||
|
)
|
||||||
|
|
||||||
|
from .classes import QuotaBackend
|
||||||
|
from .forms import QuotaBackendSelectionForm, QuotaDynamicForm
|
||||||
|
from .models import Quota
|
||||||
|
from .permissions import (
|
||||||
|
permission_quota_create, permission_quota_delete,
|
||||||
|
permission_quota_edit, permission_quota_view
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaBackendSelectionView(FormView):
|
||||||
|
extra_context = {
|
||||||
|
'title': _('New quota backend selection'),
|
||||||
|
}
|
||||||
|
form_class = QuotaBackendSelectionForm
|
||||||
|
view_permission = permission_quota_create
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
backend = form.cleaned_data['backend']
|
||||||
|
return HttpResponseRedirect(
|
||||||
|
reverse('quotas:quota_create', args=(backend,),)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaCreateView(SingleObjectDynamicFormCreateView):
|
||||||
|
form_class = QuotaDynamicForm
|
||||||
|
post_action_redirect = reverse_lazy('quotas:quota_list')
|
||||||
|
view_permission = permission_quota_create
|
||||||
|
|
||||||
|
def get_backend(self):
|
||||||
|
try:
|
||||||
|
return QuotaBackend.get(name=self.kwargs['class_path'])
|
||||||
|
except KeyError:
|
||||||
|
raise Http404(
|
||||||
|
'{} class not found'.format(self.kwargs['class_path'])
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_extra_context(self):
|
||||||
|
return {
|
||||||
|
'title': _(
|
||||||
|
'Create a "%s" quota'
|
||||||
|
) % self.get_backend().label,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_form_schema(self):
|
||||||
|
return {
|
||||||
|
'fields': self.get_backend().fields,
|
||||||
|
'widgets': getattr(self.get_backend(), 'widgets', {})
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_instance_extra_data(self):
|
||||||
|
return {'backend_path': self.kwargs['class_path']}
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaDeleteView(SingleObjectDeleteView):
|
||||||
|
object_permission = permission_quota_delete
|
||||||
|
post_action_redirect = reverse_lazy('quotas:quota_list')
|
||||||
|
|
||||||
|
def get_extra_context(self):
|
||||||
|
return {
|
||||||
|
'title': _('Delete quota: %s') % self.get_object(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Quota.objects.filter(editable=True)
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaEditView(SingleObjectDynamicFormEditView):
|
||||||
|
form_class = QuotaDynamicForm
|
||||||
|
object_permission = permission_quota_edit
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
return super(QuotaEditView, self).form_valid(form)
|
||||||
|
|
||||||
|
def get_extra_context(self):
|
||||||
|
return {
|
||||||
|
'title': _('Edit quota: %s') % self.get_object(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_form_schema(self):
|
||||||
|
return {
|
||||||
|
'fields': self.get_object().get_backend_class().fields,
|
||||||
|
'widgets': getattr(
|
||||||
|
self.get_object().get_backend_class(), 'widgets', {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Quota.objects.filter(editable=True)
|
||||||
|
|
||||||
|
|
||||||
|
class QuotaListView(SingleObjectListView):
|
||||||
|
extra_context = {
|
||||||
|
'hide_object': True,
|
||||||
|
'title': _('Quotas'),
|
||||||
|
}
|
||||||
|
model = Quota
|
||||||
|
object_permission = permission_quota_view
|
||||||
19
mayan/apps/rest_api/api_views.py
Normal file
19
mayan/apps/rest_api/api_views.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from rest_framework import generics
|
||||||
|
|
||||||
|
from rest_api.filters import MayanObjectPermissionsFilter
|
||||||
|
from rest_api.permissions import MayanPermission
|
||||||
|
|
||||||
|
from .classes import APIResource
|
||||||
|
from .serializers import APIResourceSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class APIResourceTypeListView(generics.ListAPIView):
|
||||||
|
"""
|
||||||
|
Returns a list of all the available API resources.
|
||||||
|
"""
|
||||||
|
|
||||||
|
serializer_class = APIResourceSerializer
|
||||||
|
def get_queryset(self):
|
||||||
|
return APIResource.all()
|
||||||
@@ -5,10 +5,34 @@ from django.conf import settings
|
|||||||
from django.utils.encoding import force_text, python_2_unicode_compatible
|
from django.utils.encoding import force_text, python_2_unicode_compatible
|
||||||
from django.utils.module_loading import import_string
|
from django.utils.module_loading import import_string
|
||||||
|
|
||||||
|
from .exceptions import APIResourcePatternError
|
||||||
|
|
||||||
|
|
||||||
|
class APIResource(object):
|
||||||
|
_registry = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def all(cls):
|
||||||
|
return cls._registry.values()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, name):
|
||||||
|
return cls._registry[name]
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return unicode(self.name)
|
||||||
|
|
||||||
|
def __init__(self, name, label, description=None):
|
||||||
|
self.label = label
|
||||||
|
self.name = name
|
||||||
|
self.description = description
|
||||||
|
self.__class__._registry[self.name] = self
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class APIEndPoint(object):
|
class APIEndPoint(object):
|
||||||
_registry = {}
|
_registry = {}
|
||||||
|
_patterns = []
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_all(cls):
|
def get_all(cls):
|
||||||
@@ -48,6 +72,12 @@ class APIEndPoint(object):
|
|||||||
def register_urls(self, urlpatterns):
|
def register_urls(self, urlpatterns):
|
||||||
from .urls import urlpatterns as app_urls
|
from .urls import urlpatterns as app_urls
|
||||||
|
|
||||||
app_urls += [
|
for url in urlpatterns:
|
||||||
url(r'^%s/' % (self.name or self.app.name), include(urlpatterns)),
|
if url.regex.pattern not in self.__class__._patterns:
|
||||||
]
|
app_urls.append(url)
|
||||||
|
self.__class__._patterns.append(url.regex.pattern)
|
||||||
|
else:
|
||||||
|
raise APIResourcePatternError(
|
||||||
|
'App "{}" tried to register API URL pattern "{}", which '
|
||||||
|
'already exists'.format(self.app.label, url.regex.pattern)
|
||||||
|
)
|
||||||
|
|||||||
16
mayan/apps/rest_api/exceptions.py
Normal file
16
mayan/apps/rest_api/exceptions.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
|
||||||
|
class APIError(Exception):
|
||||||
|
"""
|
||||||
|
Base exception for the API app
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class APIResourcePatternError(APIError):
|
||||||
|
"""
|
||||||
|
Raised when an app tries to override an existing URL regular expression
|
||||||
|
pattern
|
||||||
|
"""
|
||||||
|
pass
|
||||||
@@ -24,7 +24,10 @@ class DynamicSerializerField(serializers.ReadOnlyField):
|
|||||||
for klass, serializer_class in self.serializers.items():
|
for klass, serializer_class in self.serializers.items():
|
||||||
if isinstance(value, klass):
|
if isinstance(value, klass):
|
||||||
return serializer_class(
|
return serializer_class(
|
||||||
context={'request': self.context['request']}
|
context={
|
||||||
|
'format': self.context['format'],
|
||||||
|
'request': self.context['request']
|
||||||
|
}
|
||||||
).to_representation(instance=value)
|
).to_representation(instance=value)
|
||||||
|
|
||||||
return _('Unable to find serializer class for: %s') % value
|
return _('Unable to find serializer class for: %s') % value
|
||||||
|
|||||||
9
mayan/apps/rest_api/serializers.py
Normal file
9
mayan/apps/rest_api/serializers.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
|
||||||
|
class APIResourceSerializer(serializers.Serializer):
|
||||||
|
description = serializers.CharField()
|
||||||
|
label = serializers.CharField()
|
||||||
|
name = serializers.CharField()
|
||||||
@@ -2,15 +2,18 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from .views import APIBase, APIAppView, BrowseableObtainAuthToken
|
from .api_views import APIResourceTypeListView
|
||||||
|
from .views import APIBase, BrowseableObtainAuthToken
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = []
|
||||||
]
|
|
||||||
|
|
||||||
api_urls = [
|
api_urls = [
|
||||||
url(r'^$', APIBase.as_view(), name='api_root'),
|
url(r'^$', APIBase.as_view(), name='api_root'),
|
||||||
url(r'^api/(?P<path>.*)/?$', APIAppView.as_view(), name='api_app'),
|
url(
|
||||||
|
r'^resources/$', APIResourceTypeListView.as_view(),
|
||||||
|
name='resource-list'
|
||||||
|
),
|
||||||
url(
|
url(
|
||||||
r'^auth/token/obtain/$', BrowseableObtainAuthToken.as_view(),
|
r'^auth/token/obtain/$', BrowseableObtainAuthToken.as_view(),
|
||||||
name='auth_token_obtain'
|
name='auth_token_obtain'
|
||||||
|
|||||||
@@ -13,13 +13,6 @@ class APIBase(SwaggerResourcesView):
|
|||||||
renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer)
|
renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer)
|
||||||
|
|
||||||
|
|
||||||
class APIAppView(SwaggerApiView):
|
|
||||||
"""
|
|
||||||
Entry points of the selected app.
|
|
||||||
"""
|
|
||||||
renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer)
|
|
||||||
|
|
||||||
|
|
||||||
class BrowseableObtainAuthToken(ObtainAuthToken):
|
class BrowseableObtainAuthToken(ObtainAuthToken):
|
||||||
"""
|
"""
|
||||||
Obtain an API authentication token.
|
Obtain an API authentication token.
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ class Source(models.Model):
|
|||||||
logger.critical(
|
logger.critical(
|
||||||
'Unexpected exception while trying to create version for '
|
'Unexpected exception while trying to create version for '
|
||||||
'new document "%s" from source "%s"; %s',
|
'new document "%s" from source "%s"; %s',
|
||||||
label or file_object.name, self, exception
|
label or file_object.name, self, exception, exc_info=True
|
||||||
)
|
)
|
||||||
document.delete(to_trash=False)
|
document.delete(to_trash=False)
|
||||||
raise
|
raise
|
||||||
@@ -601,6 +601,12 @@ class EmailBaseModel(IntervalBaseModel):
|
|||||||
|
|
||||||
if raw_filename:
|
if raw_filename:
|
||||||
filename = collapse_rfc2231_value(raw_filename)
|
filename = collapse_rfc2231_value(raw_filename)
|
||||||
|
|
||||||
|
# Decode base64 encoded filename
|
||||||
|
# https://stackoverflow.com/a/21859258/1364435
|
||||||
|
if decode_header(filename)[0][1] is not None:
|
||||||
|
filename = str(decode_header(filename)[0][0]).decode(decode_header(filename)[0][1])
|
||||||
|
|
||||||
else:
|
else:
|
||||||
filename = _('attachment-%i') % counter
|
filename = _('attachment-%i') % counter
|
||||||
counter += 1
|
counter += 1
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from documents.tests import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from ..literals import SOURCE_UNCOMPRESS_CHOICE_Y
|
from ..literals import SOURCE_UNCOMPRESS_CHOICE_Y
|
||||||
from ..models import WatchFolderSource, WebFormSource
|
from ..models import WatchFolderSource, WebFormSource, EmailBaseModel
|
||||||
|
|
||||||
|
|
||||||
@override_settings(OCR_AUTO_OCR=False)
|
@override_settings(OCR_AUTO_OCR=False)
|
||||||
@@ -117,3 +117,55 @@ class CompressedUploadsTestCase(BaseTestCase):
|
|||||||
'label', flat=True
|
'label', flat=True
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
test_email = """From: noreply@example.com
|
||||||
|
To: test@example.com
|
||||||
|
Subject: Scan to E-mail Server Job
|
||||||
|
Date: Tue, 23 May 2017 23:03:37 +0200
|
||||||
|
Message-Id: <00000001.465619c9.1.00@BRN30055CCF4D76>
|
||||||
|
Mime-Version: 1.0
|
||||||
|
Content-Type: multipart/mixed;
|
||||||
|
boundary="RS1tYWlsIENsaWVudA=="
|
||||||
|
X-Mailer: E-mail Client
|
||||||
|
|
||||||
|
This is multipart message.
|
||||||
|
|
||||||
|
--RS1tYWlsIENsaWVudA==
|
||||||
|
Content-Type: text/plain; charset=iso-8859-1
|
||||||
|
Content-Transfer-Encoding: quoted-printable
|
||||||
|
|
||||||
|
Sending device cannot receive e-mail replies.
|
||||||
|
--RS1tYWlsIENsaWVudA==
|
||||||
|
Content-Type: text/plain
|
||||||
|
Content-Transfer-Encoding: base64
|
||||||
|
Content-Disposition: attachment; filename="=?UTF-8?B?QW1wZWxtw6RubmNoZW4udHh0?="
|
||||||
|
|
||||||
|
SGFsbG8gQW1wZWxtw6RubmNoZW4hCg==
|
||||||
|
|
||||||
|
--RS1tYWlsIENsaWVudA==--"""
|
||||||
|
|
||||||
|
|
||||||
|
class SourceStub():
|
||||||
|
subject_metadata_type = None
|
||||||
|
from_metadata_type = None
|
||||||
|
metadata_attachment_name = None
|
||||||
|
document_type = None
|
||||||
|
uncompress = None
|
||||||
|
store_body = False
|
||||||
|
label = ""
|
||||||
|
|
||||||
|
def handle_upload(self, file_object, description=None, document_type=None, expand=False, label=None, language=None,
|
||||||
|
metadata_dict_list=None, metadata_dictionary=None, tag_ids=None, user=None):
|
||||||
|
self.label = label
|
||||||
|
|
||||||
|
|
||||||
|
class EmailFilenameDecodingTestCase(BaseTestCase):
|
||||||
|
"""
|
||||||
|
Test decoding of base64 encoded e-mail attachment filename.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def test_decode_email_encoded_filename(self):
|
||||||
|
source_stub = SourceStub()
|
||||||
|
EmailBaseModel.process_message(source_stub, test_email)
|
||||||
|
self.assertEqual(source_stub.label, u'Ampelm\xe4nnchen.txt')
|
||||||
|
|||||||
@@ -242,18 +242,20 @@ class UploadInteractiveView(UploadBaseView):
|
|||||||
except Exception as exception:
|
except Exception as exception:
|
||||||
messages.error(self.request, exception)
|
messages.error(self.request, exception)
|
||||||
|
|
||||||
task_source_handle_upload.apply_async(kwargs=dict(
|
task_source_handle_upload.apply_async(
|
||||||
description=forms['document_form'].cleaned_data.get('description'),
|
kwargs=dict(
|
||||||
document_type_id=self.document_type.pk,
|
description=forms['document_form'].cleaned_data.get('description'),
|
||||||
expand=expand,
|
document_type_id=self.document_type.pk,
|
||||||
label=label,
|
expand=expand,
|
||||||
language=forms['document_form'].cleaned_data.get('language'),
|
label=label,
|
||||||
metadata_dict_list=decode_metadata_from_url(self.request.GET),
|
language=forms['document_form'].cleaned_data.get('language'),
|
||||||
shared_uploaded_file_id=shared_uploaded_file.pk,
|
metadata_dict_list=decode_metadata_from_url(self.request.GET),
|
||||||
source_id=self.source.pk,
|
shared_uploaded_file_id=shared_uploaded_file.pk,
|
||||||
tag_ids=self.request.GET.getlist('tags'),
|
source_id=self.source.pk,
|
||||||
user_id=user_id,
|
tag_ids=self.request.GET.getlist('tags'),
|
||||||
))
|
user_id=user_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
messages.success(
|
messages.success(
|
||||||
self.request,
|
self.request,
|
||||||
_(
|
_(
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from events.classes import Event
|
from events.classes import EventTypeNamespace
|
||||||
|
|
||||||
event_tag_attach = Event(
|
namespace = EventTypeNamespace(name='tag', label=_('Tags'))
|
||||||
name='tag_attach',
|
|
||||||
label=_('Tag attached to document')
|
event_tag_attach = namespace.add_event_type(
|
||||||
|
label=_('Tag attached to document'),
|
||||||
|
name='attach'
|
||||||
)
|
)
|
||||||
event_tag_remove = Event(
|
event_tag_remove = namespace.add_event_type(
|
||||||
name='tag_remove',
|
label=_('Tag removed from document'),
|
||||||
label=_('Tag removed from document')
|
name='remove'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -89,9 +89,6 @@ INSTALLED_APPS = (
|
|||||||
'document_states',
|
'document_states',
|
||||||
'documents',
|
'documents',
|
||||||
'events',
|
'events',
|
||||||
# Disable the folders app by default
|
|
||||||
# Will be removed in the next version
|
|
||||||
# 'folders',
|
|
||||||
'linking',
|
'linking',
|
||||||
'mailer',
|
'mailer',
|
||||||
'mayan_statistics',
|
'mayan_statistics',
|
||||||
@@ -99,6 +96,7 @@ INSTALLED_APPS = (
|
|||||||
'mirroring',
|
'mirroring',
|
||||||
'motd',
|
'motd',
|
||||||
'ocr',
|
'ocr',
|
||||||
|
'quotas',
|
||||||
'rest_api',
|
'rest_api',
|
||||||
'sources',
|
'sources',
|
||||||
'storage',
|
'storage',
|
||||||
|
|||||||
@@ -1,44 +1,44 @@
|
|||||||
Pillow==4.2.0
|
Pillow==4.2.1
|
||||||
PyYAML==3.12
|
PyYAML==3.12
|
||||||
|
|
||||||
celery==3.1.24
|
celery==3.1.24
|
||||||
cssmin==0.2.0
|
cssmin==0.2.0
|
||||||
|
|
||||||
django-activity-stream==0.6.3
|
django-activity-stream==0.6.4
|
||||||
django-autoadmin==1.1.1
|
django-autoadmin==1.1.1
|
||||||
django-celery==3.2.1
|
django-celery==3.2.1
|
||||||
django-colorful==1.2
|
django-colorful==1.2
|
||||||
django-compressor==2.1
|
django-compressor==2.2
|
||||||
django-cors-headers==1.2.2
|
django-cors-headers==2.1.0
|
||||||
django-downloadview==1.9
|
django-downloadview==1.9
|
||||||
django-formtools==2.0
|
django-formtools==2.0
|
||||||
django-pure-pagination==0.3.0
|
django-pure-pagination==0.3.0
|
||||||
django-mathfilters==0.4.0
|
django-mathfilters==0.4.0
|
||||||
django-model-utils==2.6.1
|
django-model-utils==3.0.0
|
||||||
django-mptt>=0.8.7
|
django-mptt==0.8.7
|
||||||
django-qsstats-magic==0.7.2
|
django-qsstats-magic==1.0.0
|
||||||
django-rest-swagger==0.3.10
|
django-rest-swagger==0.3.10
|
||||||
django-stronghold==0.2.8
|
django-stronghold==0.2.9
|
||||||
django-suit==0.2.25
|
django-suit==0.2.25
|
||||||
django-widget-tweaks==1.4.1
|
django-widget-tweaks==1.4.1
|
||||||
djangorestframework==3.3.2
|
djangorestframework==3.3.2
|
||||||
djangorestframework-recursive==0.1.1
|
djangorestframework-recursive==0.1.1
|
||||||
|
|
||||||
furl==1.0.0
|
furl==1.0.1
|
||||||
fusepy==2.0.4
|
fusepy==2.0.4
|
||||||
|
|
||||||
graphviz==0.8
|
graphviz==0.8
|
||||||
|
|
||||||
mock==2.0.0
|
mock==2.0.0
|
||||||
|
|
||||||
pycountry==1.20
|
pycountry==17.5.14
|
||||||
PyPDF2==1.26.0
|
PyPDF2==1.26.0
|
||||||
pyocr==0.4.5
|
pyocr==0.4.7
|
||||||
python-dateutil==2.5.3
|
python-dateutil==2.6.1
|
||||||
python-gnupg==0.3.9
|
python-gnupg==0.4.1
|
||||||
python-magic==0.4.13
|
python-magic==0.4.13
|
||||||
pytz==2016.7
|
pytz==2017.2
|
||||||
|
|
||||||
requests==2.18.4
|
requests==2.18.4
|
||||||
|
|
||||||
sh==1.12.11
|
sh==1.12.14
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
Django==1.10.7
|
Django==1.11.5
|
||||||
-r base.txt
|
-r base.txt
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
Werkzeug==0.12.2
|
Werkzeug==0.12.2
|
||||||
|
|
||||||
django-debug-toolbar==1.8
|
django-debug-toolbar==1.8
|
||||||
django-extensions==1.7.9
|
django-extensions==1.9.0
|
||||||
django-rosetta==0.7.13
|
django-rosetta==0.7.13
|
||||||
|
|
||||||
flake8==3.3.0
|
flake8==3.4.1
|
||||||
|
|
||||||
ipython==5.4.1
|
ipython==5.4.1
|
||||||
|
|
||||||
safety==1.4.0
|
safety==1.5.1
|
||||||
|
|
||||||
transifex-client==0.12.4
|
transifex-client==0.12.4
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Sphinx==1.5.3
|
Sphinx==1.6.3
|
||||||
|
|
||||||
sphinx-autobuild==0.6.0
|
sphinx-autobuild==0.7.1
|
||||||
sphinxcontrib-blockdiag==1.5.5
|
sphinxcontrib-blockdiag==1.5.5
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
codecov==2.0.9
|
codecov==2.0.9
|
||||||
coverage==4.4.1
|
coverage==4.4.1
|
||||||
coveralls==1.1
|
coveralls==1.2.0
|
||||||
django-test-without-migrations==0.6
|
django-test-without-migrations==0.6
|
||||||
tox==2.7.0
|
tox==2.8.1
|
||||||
psutil==5.2.2
|
psutil==5.3.0
|
||||||
|
|||||||
8
setup.py
8
setup.py
@@ -65,7 +65,7 @@ django-activity-stream==0.6.3
|
|||||||
django-autoadmin==1.1.1
|
django-autoadmin==1.1.1
|
||||||
django-celery==3.2.1
|
django-celery==3.2.1
|
||||||
django-colorful==1.2
|
django-colorful==1.2
|
||||||
django-compressor==2.1
|
django-compressor==2.1.1
|
||||||
django-cors-headers==1.2.2
|
django-cors-headers==1.2.2
|
||||||
django-downloadview==1.9
|
django-downloadview==1.9
|
||||||
django-formtools==2.0
|
django-formtools==2.0
|
||||||
@@ -86,13 +86,13 @@ graphviz==0.8
|
|||||||
mock==2.0.0
|
mock==2.0.0
|
||||||
pycountry==1.20
|
pycountry==1.20
|
||||||
PyPDF2==1.26.0
|
PyPDF2==1.26.0
|
||||||
pyocr==0.4.5
|
pyocr==0.4.7
|
||||||
python-dateutil==2.5.3
|
python-dateutil==2.5.3
|
||||||
python-gnupg==0.3.9
|
python-gnupg==0.3.9
|
||||||
python-magic==0.4.13
|
python-magic==0.4.13
|
||||||
pytz==2016.7
|
pytz==2016.7
|
||||||
requests==2.18.4
|
requests==2.18.4
|
||||||
sh==1.12.11
|
sh==1.12.13
|
||||||
""".split()
|
""".split()
|
||||||
|
|
||||||
with open('README.rst') as f:
|
with open('README.rst') as f:
|
||||||
|
|||||||
Reference in New Issue
Block a user