Compare commits

..

17 Commits

Author SHA1 Message Date
Roberto Rosario
72f01707fa Merge branch 'master' into feature/tornado
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2017-09-14 00:32:22 -04:00
Roberto Rosario
ad3bce178c Merge remote-tracking branch 'origin/master' into master 2017-09-11 00:25:37 -04:00
Roberto Rosario
fea83c5bbc Bump version to 2.7.3
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2017-09-11 00:23:28 -04:00
Roberto Rosario
c5ed81c130 Set release notes date.
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2017-09-11 00:22:13 -04:00
Roberto Rosario
6ea647822f Tweak position of the document version links for clarity.
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2017-09-11 00:19:33 -04:00
Roberto Rosario
02f28b1ac0 Cleanup source app test literals.
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2017-09-11 00:00:52 -04:00
Roberto Rosario
da8fa6f91c Fix resolved link class URL mangling when the keep_query argument is used.
Fixes source navigation on the document upload wizard. Thanks to
Nick Douma (LordGaav) for the report and diagnostic information. GitLab
issue #436.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2017-09-10 20:32:13 -04:00
Roberto Rosario
51026cc55e Fix task manager queue list view.
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
2017-09-07 23:12:02 -04:00
Roberto Rosario
bd4a48c42c Merge branch 'revert-273f94e9' into 'master'
Revert "Merge branch 'fix-context' into 'master'"

See merge request !21
2017-09-07 05:14:35 +00:00
Roberto Rosario
63a7bb0b86 Revert "Merge branch 'fix-context' into 'master'"
This reverts merge request !15
2017-09-07 05:12:21 +00:00
Roberto Rosario
273f94e9b6 Merge branch 'fix-context' into 'master'
Fix error in context: it must be a dict

See merge request !15
2017-09-07 05:09:54 +00:00
Alessandro Pasotti
13bb415187 Fix error in context: it must be a dict 2017-06-15 08:05:17 +02:00
Roberto Rosario
c17d2f5709 Rename server app to kaze, add release notes. 2016-04-26 16:23:21 -04:00
Roberto Rosario
b169d037bf Rename the tornado server app to 'kaze'. 2016-04-25 19:29:15 -04:00
Roberto Rosario
8f553091e4 Merge branch 'development' into feature/tornado 2016-04-25 18:51:20 -04:00
Roberto Rosario
46b4390480 Merge branch 'development' into feature/tornado 2016-04-21 17:02:08 -04:00
Roberto Rosario
94c4df1f5e Add app to server Mayan EDMS using tornado. 2016-03-27 05:00:13 -04:00
118 changed files with 834 additions and 2671 deletions

View File

@@ -1,7 +1,10 @@
3.0 (2017-XX-XX) 2.7.3 (2017-09-11)
================ ==================
- Add support for notifications. GitLab #262. - Fix task manager queue list view. Thanks to LeVon Smoker for
- Add quota support. GitLab #284. the report.
- Fix resolved link class URL mangling when the keep_query argument is
used. Thanks to Nick Douma(LordGaav) for the report and diagnostic
information. Fixes source navigation on the document upload wizard.
2.7.2 (2017-09-06) 2.7.2 (2017-09-06)
================== ==================
@@ -357,7 +360,6 @@
- Add roadmap documentation chapter. - Add roadmap documentation chapter.
- API updates. - API updates.
2.0.2 (2016-02-09) 2.0.2 (2016-02-09)
================== ==================
- Install testing dependencies when installing development dependencies. - Install testing dependencies when installing development dependencies.

View File

@@ -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', 'mailer', 'metadata', 'mirroring', 'motd', 'navigation', 'ocr', 'permissions',
'permissions', 'rest_api', 'smart_settings', 'sources', 'storage', 'tags', 'rest_api', 'smart_settings', 'sources', 'storage', 'tags', 'task_manager',
'task_manager', 'user_management' 'user_management'
) )
LANGUAGE_LIST = ( LANGUAGE_LIST = (

View File

@@ -108,6 +108,15 @@ screen. These messages can have an activation and an expiration date and
time. These messages are useful for display company access policies, time. These messages are useful for display company access policies,
maintenance announcement, etc. maintenance announcement, etc.
New server app using tornado
----------------------------
This release includes a simple app that can serve Mayan EDMS using the tornado
server. Using this app users can start using Mayan EDMS with minimal setup
(just install Redis). By default the server will run on port 52723, but users
can change the port with the --port option. For privileged port (ports
below 1024) the command must be run as superadmin. This is an experimental
feature, feedback and patches are appreciated.
Document signing Document signing
---------------- ----------------
The biggest change for this release if the addition of document signing from The biggest change for this release if the addition of document signing from

View File

@@ -1,28 +1,28 @@
============================= ===============================
Mayan EDMS v3.0 release notes Mayan EDMS v2.7.3 release notes
============================= ===============================
Released: XX XX, 2017 Released: September 11, 2017
What's new What's new
========== ==========
- Fix task manager queue list view. Thanks to LeVon Smoker for
Other changes the report.
------------- - Fix resolved link class URL mangling when the keep_query argument is
- Resource based API endpoints used. Fixes source navigation on the document upload wizard. Thanks to
- 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 Nick Douma(LordGaav) for the report and diagnostic information. GitLab
- Add notifications support. issue #436.
Removals Removals
-------- --------
- None * None
Upgrading from a previous version Upgrading from a previous version
--------------------------------- ---------------------------------
If installed via PIP Using PIP
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~
Type in the console:: Type in the console::
@@ -30,8 +30,8 @@ Type in the console::
the requirements will also be updated automatically. the requirements will also be updated automatically.
If installed using Git Using Git
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~
If you installed Mayan EDMS by cloning the Git repository issue the commands:: If you installed Mayan EDMS by cloning the Git repository issue the commands::
@@ -41,14 +41,10 @@ If you installed Mayan EDMS by cloning the Git repository issue the commands::
otherwise download the compressed archived and uncompress it overriding the otherwise download the compressed archived and uncompress it overriding the
existing installation. existing installation.
Manually upgrade/add the new requirements:: Next upgrade/add the new requirements::
$ pip install --upgrade -r requirements.txt $ pip install --upgrade -r requirements.txt
Remove deprecated requirements::
$ pip uninstall -y -r removals.txt
Common steps Common steps
~~~~~~~~~~~~ ~~~~~~~~~~~~
@@ -71,9 +67,8 @@ Backward incompatible changes
Bugs fixed or issues closed Bugs fixed or issues closed
=========================== ===========================
* `GitLab issue #366 <https://gitlab.com/mayan-edms/mayan-edms/issues/366>`_ Proofread documentation * `GitLab issue #431 <https://gitlab.com/mayan-edms/mayan-edms/issues/431>`_ can't create new mailer
* `GitLab issue #379 <https://gitlab.com/mayan-edms/mayan-edms/issues/379>`_ Add new document version list view permission. * `GitLab issue #436 <https://gitlab.com/mayan-edms/mayan-edms/issues/436>`_ New document source menu does not contain source_ids
* `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/ .. _PyPI: https://pypi.python.org/pypi/mayan-edms/

View File

@@ -22,6 +22,7 @@ versions of the documentation contain the release notes for any later releases.
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 1
2.7.3
2.7.2 2.7.2
2.7.1 2.7.1
2.7 2.7

View File

@@ -54,12 +54,6 @@ 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
~~~~~~~ ~~~~~~~
@@ -251,6 +245,47 @@ 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

View File

@@ -1,8 +1,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
__title__ = 'Mayan EDMS' __title__ = 'Mayan EDMS'
__version__ = '2.7.2' __version__ = '2.7.3'
__build__ = 0x020702 __build__ = 0x020703
__author__ = 'Roberto Rosario' __author__ = 'Roberto Rosario'
__author_email__ = 'roberto.rosario@mayan-edms.com' __author_email__ = 'roberto.rosario@mayan-edms.com'
__description__ = 'Free Open Source Electronic Document Management System' __description__ = 'Free Open Source Electronic Document Management System'

View File

@@ -92,10 +92,8 @@ 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( logger.debug('Unfiltered queryset returned to user "%s" as superuser or staff',
'Unfiltered queryset returned to user "%s" as superuser ' user)
'or staff', user
)
return queryset return queryset
try: try:

View File

@@ -28,19 +28,19 @@ urlpatterns = [
api_urls = [ api_urls = [
url( url(
r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/$', r'^object/(?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'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/(?P<pk>\d+)/$', r'^object/(?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'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/(?P<pk>\d+)/permissions/$', r'^object/(?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'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_pk>\d+)/acls/(?P<pk>\d+)/permissions/(?P<permission_pk>\d+)/$', r'^object/(?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'
), ),
] ]

View File

@@ -5,7 +5,6 @@ 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
@@ -22,6 +21,6 @@ class AuthenticationApp(MayanAppConfig):
menu_user.bind_links( menu_user.bind_links(
links=( links=(
Separator(), link_password_change, link_logout link_password_change, link_logout
), position=99 ), position=99
) )

View File

@@ -2,15 +2,13 @@ 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 EventTypeNamespace from events.classes import Event
namespace = EventTypeNamespace(name='cabinets', label=_('Cabinets')) event_cabinets_add_document = Event(
name='cabinets_add_document',
event_cabinets_add_document = namespace.add_event_type( label=_('Document added to cabinet')
label=_('Document added to cabinet'),
name='add_document'
) )
event_cabinets_remove_document = namespace.add_event_type( event_cabinets_remove_document = Event(
label=_('Document removed from cabinet'), name='cabinets_remove_document',
name='remove_document' label=_('Document removed from cabinet')
) )

View File

@@ -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.id event_cabinets_add_document.name
) )
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.id event_cabinets_remove_document.name
) )

View File

@@ -9,11 +9,13 @@ 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_checkin, permission_document_checkin_override permission_document_checkout, permission_document_checkin,
permission_document_checkin_override
) )
from .serializers import ( from .serializers import (
DocumentCheckoutSerializer, NewDocumentCheckoutSerializer DocumentCheckoutSerializer, NewDocumentCheckoutSerializer
@@ -46,23 +48,12 @@ 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.file) serializer = self.get_serializer(data=request.DATA, files=request.FILES)
if serializer.is_valid(): if serializer.is_valid():
document = get_object_or_404( document = get_object_or_404(
@@ -93,7 +84,6 @@ 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):

View File

@@ -11,15 +11,10 @@ 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,
@@ -56,10 +51,7 @@ class CheckoutsApp(MayanAppConfig):
Document.add_to_class( Document.add_to_class(
'check_in', 'check_in',
lambda document, lambda document, user=None: DocumentCheckout.objects.check_in_document(document, user)
user=None: DocumentCheckout.objects.check_in_document(
document, user
)
) )
Document.add_to_class( Document.add_to_class(
'checkout_info', 'checkout_info',
@@ -80,13 +72,6 @@ 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,

View File

@@ -2,21 +2,19 @@ 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 import EventTypeNamespace from events.classes import Event
namespace = EventTypeNamespace(name='checkouts', label=_('Checkouts')) event_document_auto_check_in = Event(
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 = namespace.add_event_type( event_document_check_in = Event(
name='document_check_in', label=_('Document checked in') name='checkouts_document_check_in', label=_('Document checked in')
) )
event_document_check_out = namespace.add_event_type( event_document_check_out = Event(
name='document_check_out', label=_('Document checked out') name='checkouts_document_check_out', label=_('Document checked out')
) )
event_document_forceful_check_in = namespace.add_event_type( event_document_forceful_check_in = Event(
name='document_forceful_check_in', name='checkouts_document_forceful_check_in',
label=_('Document forcefully checked in') label=_('Document forcefully checked in')
) )

View File

@@ -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):
return self.create( 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
) )

View File

@@ -1,19 +1,15 @@
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()
@@ -21,33 +17,7 @@ class DocumentCheckoutSerializer(serializers.ModelSerializer):
model = DocumentCheckout model = DocumentCheckout
class NewDocumentCheckoutSerializer(serializers.ModelSerializer): class NewDocumentCheckoutSerializer(serializers.Serializer):
block_new_version = serializers.BooleanField() document = serializers.IntegerField()
document_pk = serializers.IntegerField(
help_text=_('Primary key of the document to be checked out.'),
write_only=True
)
expiration_datetime = serializers.DateTimeField() expiration_datetime = serializers.DateTimeField()
block_new_version = serializers.BooleanField()
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
)

View File

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

View File

@@ -26,11 +26,11 @@ urlpatterns = [
api_urls = [ api_urls = [
url( url(
r'^checkouts/$', APICheckedoutDocumentListView.as_view(), r'^documents/$', APICheckedoutDocumentListView.as_view(),
name='checkout-document-list' name='checkout-document-list'
), ),
url( url(
r'^checkouts/(?P<pk>[0-9]+)/$', APICheckedoutDocumentView.as_view(), r'^documents/(?P<pk>[0-9]+)/$', APICheckedoutDocumentView.as_view(),
name='checkedout-document-view' name='checkedout-document-view'
), ),
] ]

View File

@@ -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() of app ' 'Import time error when running AppConfig.ready(). Check '
'"%s".', self.name 'apps.py, urls.py, views.py, etc.'
) )
raise exception raise exception
@@ -127,6 +127,7 @@ 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()
) )
) )

View File

@@ -83,9 +83,10 @@ 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.flatten()) return get_template(template_name).render(new_context)
@register.simple_tag @register.simple_tag

View File

@@ -1,4 +1,4 @@
from __future__ import absolute_import, unicode_literals 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 _
@@ -6,13 +6,9 @@ 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
) )
@@ -40,12 +36,6 @@ 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,

View File

@@ -2,15 +2,13 @@ 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 import EventTypeNamespace from events.classes import Event
namespace = EventTypeNamespace( event_document_comment_create = Event(
name='document_comments', label=_('Document comments') name='document_comment_create',
label=_('Document comment created')
) )
event_document_comment_delete = Event(
event_document_comment_create = namespace.add_event_type( name='document_comment_delete',
name='create', label=_('Document comment created') label=_('Document comment deleted')
)
event_document_comment_delete = namespace.add_event_type(
name='delete', label=_('Document comment deleted')
) )

View File

@@ -25,11 +25,11 @@ urlpatterns = [
api_urls = [ api_urls = [
url( url(
r'^documents/(?P<document_pk>[0-9]+)/comments/$', r'^document/(?P<document_pk>[0-9]+)/comments/$',
APICommentListView.as_view(), name='comment-list' APICommentListView.as_view(), name='comment-list'
), ),
url( url(
r'^documents/(?P<document_pk>[0-9]+)/comments/(?P<comment_pk>[0-9]+)/$', r'^document/(?P<document_pk>[0-9]+)/comments/(?P<comment_pk>[0-9]+)/$',
APICommentView.as_view(), name='comment-detail' APICommentView.as_view(), name='comment-detail'
), ),
] ]

View File

@@ -72,12 +72,12 @@ urlpatterns = [
api_urls = [ api_urls = [
url( url(
r'^indexes/node/(?P<pk>[0-9]+)/documents/$', r'^index/node/(?P<pk>[0-9]+)/documents/$',
APIIndexNodeInstanceDocumentListView.as_view(), APIIndexNodeInstanceDocumentListView.as_view(),
name='index-node-documents' name='index-node-documents'
), ),
url( url(
r'^indexes/template/(?P<pk>[0-9]+)/$', APIIndexTemplateView.as_view(), r'^index/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'^indexes/(?P<pk>[0-9]+)/template/$', r'^index/(?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'^documents/(?P<pk>[0-9]+)/indexes/$', r'^document/(?P<pk>[0-9]+)/indexes/$',
APIDocumentIndexListView.as_view(), name='document-index-list' APIDocumentIndexListView.as_view(), name='document-index-list'
), ),
] ]

View File

@@ -2,17 +2,13 @@ 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 import EventTypeNamespace from events.classes import Event
namespace = EventTypeNamespace( event_parsing_document_version_submit = Event(
name='parsing', label=_('Document parsing') name='parsing_document_version_submit',
label=_('Document version submitted for parsing')
) )
event_parsing_document_version_finish = Event(
event_parsing_document_version_submit = namespace.add_event_type( name='parsing_document_version_finish',
label=_('Document version submitted for parsing'), label=_('Document version parsing finished')
name='document_version_submit'
)
event_parsing_document_version_finish = namespace.add_event_type(
label=_('Document version parsing finished'),
name='document_version_finish'
) )

View File

@@ -24,7 +24,7 @@ class DocumentParsingEventsTestCase(GenericDocumentTestCase):
) )
self.assertEqual( self.assertEqual(
Action.objects.last().verb, Action.objects.last().verb,
event_parsing_document_version_submit.id event_parsing_document_version_submit.name
) )
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.id event_parsing_document_version_finish.name
) )

View File

@@ -135,7 +135,7 @@ class DocumentSignaturesApp(MayanAppConfig):
menu_facet.bind_links( menu_facet.bind_links(
links=( links=(
link_document_version_signature_list, link_document_version_signature_list,
), sources=(DocumentVersion,) ), position=9, sources=(DocumentVersion,)
) )
menu_object.bind_links( menu_object.bind_links(

View File

@@ -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 EventType from events.classes import Event
def handler_index_document(sender, **kwargs): def handler_index_document(sender, **kwargs):
@@ -42,9 +42,8 @@ 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') % EventType.get( comment=_('Event trigger: %s') % Event.get(name=action.verb).label,
name=action.verb transition=transition
).label, transition=transition
) )

View File

@@ -9,7 +9,7 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('events', '0005_auto_20170731_0452'), ('events', '0001_initial'),
('document_states', '0004_workflow_internal_name'), ('document_states', '0004_workflow_internal_name'),
] ]
@@ -17,19 +17,8 @@ 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')),
'id', models.AutoField( ('stored_event_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trigger_events', to='events.EventType', verbose_name='Event type')),
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',
@@ -39,28 +28,16 @@ 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( 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'),
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( field=models.CharField(blank=True, choices=[('days', 'Days'), ('hours', 'Hours'), ('minutes', 'Minutes')], max_length=8, null=True, verbose_name='Trigger time unit'),
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( field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='document_states.WorkflowTransition', verbose_name='Transition'),
on_delete=django.db.models.deletion.CASCADE,
to='document_states.WorkflowTransition',
verbose_name='Transition'
),
), ),
] ]

View File

@@ -17,28 +17,16 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='workflowinstancelogentry', model_name='workflowinstancelogentry',
name='user', name='user',
field=models.ForeignKey( field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User'),
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( field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.EventType', verbose_name='Event type'),
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( field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trigger_events', to='document_states.WorkflowTransition', verbose_name='Transition'),
on_delete=django.db.models.deletion.CASCADE,
related_name='trigger_events',
to='document_states.WorkflowTransition',
verbose_name='Transition'
),
), ),
] ]

View File

@@ -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 StoredEventType from events.models import EventType
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,8 +306,7 @@ 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(
StoredEventType, on_delete=models.CASCADE, EventType, on_delete=models.CASCADE, verbose_name=_('Event type')
verbose_name=_('Event type')
) )
class Meta: class Meta:

View File

@@ -215,20 +215,20 @@ api_urls = [
APIWorkflowTransitionView.as_view(), name='workflowtransition-detail' APIWorkflowTransitionView.as_view(), name='workflowtransition-detail'
), ),
url( url(
r'^documents/(?P<pk>[0-9]+)/workflows/$', r'^document/(?P<pk>[0-9]+)/workflows/$',
APIWorkflowInstanceListView.as_view(), name='workflowinstance-list' APIWorkflowInstanceListView.as_view(), name='workflowinstance-list'
), ),
url( url(
r'^documents/(?P<pk>[0-9]+)/workflows/(?P<workflow_pk>[0-9]+)/$', r'^document/(?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'^documents/(?P<pk>[0-9]+)/workflows/(?P<workflow_pk>[0-9]+)/log_entries/$', r'^document/(?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_types/(?P<pk>[0-9]+)/workflows/$', r'^document_type/(?P<pk>[0-9]+)/workflows/$',
APIDocumentTypeWorkflowListView.as_view(), APIDocumentTypeWorkflowListView.as_view(),
name='documenttype-workflow-list' name='documenttype-workflow-list'
), ),

View File

@@ -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 EventType from events.classes import Event
from events.models import StoredEventType from events.models import EventType
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 = StoredEventType submodel = EventType
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
) )
EventType.refresh() Event.refresh()
return super( return super(
SetupWorkflowTransitionTriggerEventListView, self SetupWorkflowTransitionTriggerEventListView, self
).dispatch(*args, **kwargs) ).dispatch(*args, **kwargs)
@@ -735,10 +735,8 @@ 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 EventType.all()] event_type_ids = [event_type.name for event_type in Event.all()]
event_type_queryset = StoredEventType.objects.filter( event_type_queryset = EventType.objects.filter(name__in=event_type_ids)
name__in=event_type_ids
)
for event_type in event_type_queryset: for event_type in event_type_queryset:
initial.append({ initial.append({

View File

@@ -23,16 +23,12 @@ from converter.permissions import (
permission_transformation_delete, permission_transformation_edit, permission_transformation_delete, permission_transformation_edit,
permission_transformation_view, permission_transformation_view,
) )
from events import ModelEventType from events.links import link_events_for_object
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, APIResource from rest_api.classes import APIEndPoint
from rest_api.fields import DynamicSerializerField from rest_api.fields import DynamicSerializerField
from .dashboard_widgets import ( from .dashboard_widgets import (
@@ -40,12 +36,6 @@ 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
) )
@@ -117,9 +107,6 @@ 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')
@@ -154,19 +141,6 @@ 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,
@@ -402,8 +376,7 @@ 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_object_event_types_user_subcriptions_list, link_acl_list, link_document_type_delete
link_document_type_delete
), sources=(DocumentType,) ), sources=(DocumentType,)
) )
menu_object.bind_links( menu_object.bind_links(
@@ -460,22 +433,15 @@ 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=( links=(link_events_for_object, link_document_version_list,),
link_events_for_object, sources=(Document,), position=2
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(
links=(link_document_version_view,), sources=(DocumentVersion,)
)
# Document actions # Document actions
menu_object.bind_links( menu_object.bind_links(
links=( links=(
link_document_version_view, link_document_version_revert, link_document_version_revert, link_document_version_download
link_document_version_download
), ),
sources=(DocumentVersion,) sources=(DocumentVersion,)
) )
@@ -524,6 +490,9 @@ class DocumentsApp(MayanAppConfig):
link_document_version_return_list link_document_version_return_list
), sources=(DocumentVersion,) ), sources=(DocumentVersion,)
) )
menu_facet.bind_links(
links=(link_document_version_view,), sources=(DocumentVersion,)
)
namespace = StatisticNamespace(slug='documents', label=_('Documents')) namespace = StatisticNamespace(slug='documents', label=_('Documents'))
namespace.add_statistic( namespace.add_statistic(

View File

@@ -2,28 +2,29 @@ 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 import EventTypeNamespace from events.classes import Event
namespace = EventTypeNamespace(name='documents', label=_('Documents')) event_document_create = Event(
name='documents_document_create', label=_('Document created')
event_document_create = namespace.add_event_type(
name='document_create', label=_('Document created')
) )
event_document_download = namespace.add_event_type( event_document_download = Event(
name='document_download', label=_('Document downloaded') name='documents_document_download',
label=_('Document downloaded')
) )
event_document_properties_edit = namespace.add_event_type( event_document_properties_edit = Event(
name='document_edit', label=_('Document properties edited') name='documents_document_edit', label=_('Document properties edited')
) )
event_document_type_change = namespace.add_event_type( event_document_type_change = Event(
name='document_type_change', label=_('Document type changed') name='documents_document_type_change', label=_('Document type changed')
) )
event_document_new_version = namespace.add_event_type( event_document_new_version = Event(
name='document_new_version', label=_('New version uploaded') name='documents_document_new_version', label=_('New version uploaded')
) )
event_document_version_revert = namespace.add_event_type( event_document_version_revert = Event(
name='document_version_revert', label=_('Document version reverted') name='documents_document_version_revert',
label=_('Document version reverted')
) )
event_document_view = namespace.add_event_type( event_document_view = Event(
name='document_view', label=_('Document viewed') name='documents_document_view',
label=_('Document viewed')
) )

View File

@@ -100,11 +100,6 @@ 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,)
@@ -227,13 +222,9 @@ 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( event_document_create.commit(actor=user, target=self)
actor=user, target=self, action_object=self.document_type
)
else: else:
event_document_create.commit( event_document_create.commit(target=self)
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)

View File

@@ -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.alpha_3, i.name) for i in list(pycountry.languages) (i.iso639_3_code, i.name) for i in list(pycountry.languages)
] ]
namespace = Namespace(name='documents', label=_('Documents')) namespace = Namespace(name='documents', label=_('Documents'))

View File

@@ -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.id) self.assertEqual(event.verb, event_document_download.name)
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.id) self.assertEqual(event.verb, event_document_view.name)
self.assertEqual(event.target, self.document) self.assertEqual(event.target, self.document)
self.assertEqual(event.actor, self.user) self.assertEqual(event.actor, self.user)

View File

@@ -916,73 +916,6 @@ 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()

View File

@@ -35,7 +35,7 @@ api_urls = [
name='search-view' name='search-view'
), ),
url( url(
r'^search/advanced/(?P<search_model>[\.\w]+)/$', APIAdvancedSearchView.as_view(), r'^advanced/(?P<search_model>[\.\w]+)/$', APIAdvancedSearchView.as_view(),
name='advanced-search-view' name='advanced-search-view'
), ),
] ]

View File

@@ -1,5 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from .classes import EventTypeNamespace, ModelEventType # NOQA from .classes import Event # NOQA
default_app_config = 'events.apps.EventsApp' default_app_config = 'events.apps.EventsApp'

View File

@@ -2,19 +2,9 @@ from __future__ import unicode_literals
from django.contrib import admin from django.contrib import admin
from .models import EventSubscription, Notification, StoredEventType from .models import EventType
@admin.register(EventSubscription) @admin.register(EventType)
class EventSubscriptionAdmin(admin.ModelAdmin): class EventTypeAdmin(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')

View File

@@ -10,13 +10,9 @@ 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 EventType, EventTypeNamespace from .classes import Event
from .models import Notification
from .permissions import permission_events_view from .permissions import permission_events_view
from .serializers import ( from .serializers import EventSerializer, EventTypeSerializer
EventSerializer, EventTypeSerializer, EventTypeNamespaceSerializer,
NotificationSerializer
)
class APIObjectEventListView(generics.ListAPIView): class APIObjectEventListView(generics.ListAPIView):
@@ -50,72 +46,13 @@ 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 = EventType.all() queryset = sorted(Event.all(), key=lambda event: event.name)
def get_serializer_context(self):
return {
'format': self.format_kwarg,
'request': self.request,
'view': self
}
class APIEventListView(generics.ListAPIView): class APIEventListView(generics.ListAPIView):
@@ -127,20 +64,3 @@ 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)

View File

@@ -3,19 +3,11 @@ 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 ( from common import MayanAppConfig, menu_tools
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 ( from .links import link_events_list
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
@@ -36,8 +28,6 @@ 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')
@@ -49,7 +39,7 @@ class EventsApp(MayanAppConfig):
func=lambda context: event_actor(context['object']) func=lambda context: event_actor(context['object'])
) )
SourceColumn( SourceColumn(
source=Action, label=_('Event'), source=Action, label=_('Verb'),
func=lambda context: event_type_link(context['object']) func=lambda context: event_type_link(context['object'])
) )
SourceColumn( SourceColumn(
@@ -59,44 +49,4 @@ 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,))

View File

@@ -1,57 +1,18 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import logging
from django.apps import apps from django.apps import apps
from django.contrib.auth import get_user_model from django.utils.encoding import force_text
from django.core.exceptions import PermissionDenied from django.utils.translation import ugettext_lazy as _
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
logger = logging.getLogger(__name__) class Event(object):
@python_2_unicode_compatible
class EventTypeNamespace(object):
_registry = {} _registry = {}
@classmethod @classmethod
def all(cls): def all(cls):
return sorted(cls._registry.values()) return Event.sort(event_type_list=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):
@@ -59,174 +20,51 @@ class EventType(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))
) )
def __init__(self, namespace, name, label): @classmethod
self.namespace = namespace def get_label(cls, name):
self.name = name try:
self.label = label return cls.get(name=name).label
self.stored_event_type = None except KeyError as exception:
self.__class__._registry[self.id] = self return force_text(exception)
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_stored_event_type() event_type.get_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.namespace.label, x.label) event_type_list, key=lambda x: 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):
AccessControlList = apps.get_model( if not self.event_type:
app_label='acls', model_name='AccessControlList' EventType = apps.get_model('events', 'EventType')
) self.event_type, created = EventType.objects.get_or_create(
Action = apps.get_model( name=self.name
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'
)
results = action.send( action.send(
actor or target, actor=actor, verb=self.id, actor or target, actor=actor, verb=self.name,
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

View File

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

View File

@@ -26,44 +26,12 @@ 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'
)

View File

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

View File

@@ -1,30 +0,0 @@
# -*- 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',
},
),
]

View File

@@ -1,32 +0,0 @@
# -*- 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',
},
),
]

View File

@@ -1,40 +0,0 @@
# -*- 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'),
),
]

View File

@@ -1,44 +0,0 @@
# -*- 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),
]

View File

@@ -1,33 +0,0 @@
# -*- 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',
},
),
]

View File

@@ -1,19 +0,0 @@
# -*- 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'},
),
]

View File

@@ -1,111 +1,28 @@
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 force_text, python_2_unicode_compatible from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from actstream.models import Action from .classes import Event
from .classes import EventType
from .managers import (
EventSubscriptionManager, ObjectEventSubscriptionManager
)
@python_2_unicode_compatible @python_2_unicode_compatible
class StoredEventType(models.Model): class EventType(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 = _('Stored event type') verbose_name = _('Event type')
verbose_name_plural = _('Stored event types') verbose_name_plural = _('Event types')
def __str__(self): def __str__(self):
return force_text(self.get_class()) return self.get_class().label
def get_class(self): def get_class(self):
return EventType.get(name=self.name) return Event.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)

View File

@@ -4,59 +4,30 @@ 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 EventType from .classes import Event
from .models import Notification, StoredEventType from .models import EventType
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, EventType): if isinstance(instance, Event):
return super(EventTypeSerializer, self).to_representation( return super(EventTypeSerializer, self).to_representation(
instance instance
) )
elif isinstance(instance, StoredEventType): elif isinstance(instance, EventType):
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(
EventType.get(name=instance) Event.get(name=instance)
) )
@@ -72,12 +43,3 @@ 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

View File

@@ -3,15 +3,9 @@ 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, APIEventTypeNamespaceDetailView, APIEventListView, APIEventTypeListView, APIObjectEventListView
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'),
@@ -20,60 +14,16 @@ 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( url(r'^types/$', APIEventTypeListView.as_view(), name='event-type-list'),
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'^notifications/$', APINotificationListView.as_view(), r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/events/$',
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'
), ),
] ]

View File

@@ -1,24 +1,17 @@
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, HttpResponseRedirect from django.http import Http404
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 EventType, ModelEventType from .classes import Event
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
@@ -44,96 +37,6 @@ 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
@@ -170,76 +73,6 @@ 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 {
@@ -254,7 +87,7 @@ class VerbEventListView(SingleObjectListView):
'hide_object': True, 'hide_object': True,
'title': _( 'title': _(
'Events of type: %s' 'Events of type: %s'
) % EventType.get(name=self.kwargs['verb']), ) % Event.get_label(self.kwargs['verb']),
} }
def get_object_list(self): def get_object_list(self):

View File

@@ -1,28 +1,23 @@
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 EventType from .classes import Event
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)
if hasattr(obj, 'get_absolute_url'): else:
url = obj.get_absolute_url() obj_type = ''
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': url, 'label': label, 'obj_type': obj_type 'url': obj.get_absolute_url() if obj else '#',
'label': obj or '', 'obj_type': obj_type
} }
) )
@@ -31,6 +26,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': EventType.get(name=entry.verb) 'label': Event.get_label(entry.verb)
} }
) )

View File

@@ -0,0 +1,3 @@
from __future__ import unicode_literals
default_app_config = 'kaze.apps.KazeApp'

13
mayan/apps/kaze/apps.py Normal file
View File

@@ -0,0 +1,13 @@
from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _
from common import MayanAppConfig
class KazeApp(MayanAppConfig):
name = 'kaze'
verbose_name = _('Kaze')
def ready(self):
super(KazeApp, self).ready()

View File

@@ -0,0 +1,69 @@
from __future__ import unicode_literals
import os
from django.core import management
from django.core.wsgi import get_wsgi_application
import tornado.httpserver
import tornado.ioloop
from tornado.process import Subprocess
import tornado.web
import tornado.wsgi
DEFAULT_PORT = 8080
class Command(management.BaseCommand):
help = 'Launches a local Tornado server.'
def add_arguments(self, parser):
parser.add_argument(
'--single-process',
action='store_true',
dest='single-process',
default=False,
help='Forces only one server process.'
)
parser.add_argument(
'--port',
action='store',
dest='port',
default=DEFAULT_PORT,
help='Port on which to bind the server.'
)
def handle(self, *args, **options):
wsgi_application = get_wsgi_application()
wsgi_container = tornado.wsgi.WSGIContainer(wsgi_application)
tornado_application = tornado.web.Application(
handlers=(
(
r'/static/(.*)', tornado.web.StaticFileHandler,
{'path': 'mayan/media/static'},
),
(
'.*', tornado.web.FallbackHandler,
dict(fallback=wsgi_container)
),
)
)
http_server = tornado.httpserver.HTTPServer(tornado_application)
try:
if options['single-process']:
http_server.listen(options['port'])
ioloop = tornado.ioloop.IOLoop.instance()
Subprocess(['./manage.py', 'celery', 'worker', '-O', 'fair'])
ioloop.start()
else:
http_server.bind(options['port'])
http_server.start(0) # forks one process per cpu
ioloop = tornado.ioloop.IOLoop.current()
Subprocess(['./manage.py', 'celery', 'worker', '-O', 'fair'])
ioloop.start()
except KeyboardInterrupt:
tornado.ioloop.IOLoop.instance().stop()

View File

@@ -1,20 +0,0 @@
# -*- 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'),
),
]

View File

@@ -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.TextField( value = models.CharField(
blank=True, db_index=True, null=True, blank=True, db_index=True, max_length=255, null=True,
verbose_name=_('Value') verbose_name=_('Value')
) )

View File

@@ -190,25 +190,6 @@ 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

View File

@@ -1,5 +1,4 @@
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

View File

@@ -363,7 +363,10 @@ class Link(object):
except KeyError: except KeyError:
pass pass
resolved_link.url = parsed_url.url # Use the link's URL but with the previous URL querystring
new_url = furl(resolved_link.url)
new_url.args = parsed_url.querystr
resolved_link.url = new_url.url
resolved_link.context = context resolved_link.context = context
return resolved_link return resolved_link

View File

@@ -7,5 +7,8 @@ TEST_PERMISSION_NAME = 'test permission name'
TEST_PERMISSION_LABEL = 'test permission label' TEST_PERMISSION_LABEL = 'test permission label'
TEST_LINK_TEXT = 'test link text' TEST_LINK_TEXT = 'test link text'
TEST_MENU_NAME = 'menu test' TEST_MENU_NAME = 'menu test'
TEST_QUERYSTRING_ONE_KEY = 'key1=value1'
TEST_QUERYSTRING_TWO_KEYS = 'key1=value1&key2=value2'
TEST_SUBMENU_NAME = 'submenu test' TEST_SUBMENU_NAME = 'submenu test'
TEST_UNICODE_STRING = 'úñí©óðé' TEST_UNICODE_STRING = 'úñí©óðé'
TEST_URL = 'test-URL'

View File

@@ -15,7 +15,8 @@ from ..classes import Link, Menu
from .literals import ( from .literals import (
TEST_PERMISSION_NAMESPACE_NAME, TEST_PERMISSION_NAMESPACE_TEXT, TEST_PERMISSION_NAMESPACE_NAME, TEST_PERMISSION_NAMESPACE_TEXT,
TEST_PERMISSION_NAME, TEST_PERMISSION_LABEL, TEST_LINK_TEXT, TEST_PERMISSION_NAME, TEST_PERMISSION_LABEL, TEST_LINK_TEXT,
TEST_MENU_NAME, TEST_SUBMENU_NAME, TEST_UNICODE_STRING TEST_MENU_NAME, TEST_QUERYSTRING_ONE_KEY, TEST_QUERYSTRING_TWO_KEYS,
TEST_SUBMENU_NAME, TEST_UNICODE_STRING, TEST_URL
) )
@@ -115,6 +116,43 @@ class LinkClassTestCase(GenericViewTestCase):
self.assertEqual(resolved_link.url, url.url) self.assertEqual(resolved_link.url, url.url)
def test_link_with_querystring_preservation(self):
previous_url = '{}?{}'.format(
reverse(TEST_VIEW_NAME), TEST_QUERYSTRING_TWO_KEYS
)
self.link.keep_query = True
self.link.url = TEST_URL
self.link.view = None
response = self.get(path=previous_url)
context = Context({'request': response.wsgi_request})
resolved_link = self.link.resolve(context=context)
self.assertEqual(
resolved_link.url,
'{}?{}'.format(TEST_URL, TEST_QUERYSTRING_TWO_KEYS)
)
def test_link_with_querystring_preservation_with_key_removal(self):
previous_url = '{}?{}'.format(
reverse(TEST_VIEW_NAME), TEST_QUERYSTRING_TWO_KEYS
)
self.link.keep_query = True
self.link.url = TEST_URL
self.link.view = None
self.link.remove_from_query = ['key2']
response = self.get(path=previous_url)
context = Context({'request': response.wsgi_request})
resolved_link = self.link.resolve(context=context)
self.assertEqual(
resolved_link.url,
'{}?{}'.format(TEST_URL, TEST_QUERYSTRING_ONE_KEY)
)
class MenuClassTestCase(GenericViewTestCase): class MenuClassTestCase(GenericViewTestCase):
def setUp(self): def setUp(self):
super(MenuClassTestCase, self).setUp() super(MenuClassTestCase, self).setUp()

View File

@@ -3,8 +3,6 @@ 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
@@ -28,6 +26,10 @@ 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
@@ -38,19 +40,12 @@ 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
@@ -59,6 +54,10 @@ 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
@@ -71,25 +70,20 @@ 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()

View File

@@ -2,15 +2,13 @@ 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 EventTypeNamespace from events.classes import Event
namespace = EventTypeNamespace(name='ocr', label=_('OCR')) event_ocr_document_version_submit = Event(
name='ocr_document_version_submit',
event_ocr_document_version_submit = namespace.add_event_type( label=_('Document version submitted for OCR')
label=_('Document version submitted for OCR'),
name='document_version_submit'
) )
event_ocr_document_version_finish = namespace.add_event_type( event_ocr_document_version_finish = Event(
label=_('Document version OCR finished'), name='ocr_document_version_finish',
name='document_version_finish' label=_('Document version OCR finished')
) )

View File

@@ -63,9 +63,7 @@ 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=( args=(self.document.latest_version.pk,)
self.document.pk, self.document.latest_version.pk,
)
) )
) )
@@ -79,10 +77,7 @@ 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=( args=(self.document.latest_version.pages.first().pk,)
self.document.pk, self.document.latest_version.pk,
self.document.latest_version.pages.first().pk,
)
), ),
) )

View File

@@ -19,7 +19,7 @@ class OCREventsTestCase(GenericDocumentTestCase):
) )
self.assertEqual( self.assertEqual(
Action.objects.last().verb, Action.objects.last().verb,
event_ocr_document_version_submit.id event_ocr_document_version_submit.name
) )
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.id event_ocr_document_version_finish.name
) )

View File

@@ -47,16 +47,16 @@ urlpatterns = [
api_urls = [ api_urls = [
url( url(
r'^documents/(?P<pk>\d+)/ocr/$', APIDocumentOCRView.as_view(), r'^document/(?P<pk>\d+)/submit/$', APIDocumentOCRView.as_view(),
name='document-ocr-submit-view' name='document-ocr-submit-view'
), ),
url( url(
r'^documents/(?P<document_pk>\d+)/versions/(?P<version_pk>\d+)/ocr/$', r'^document_version/(?P<pk>\d+)/submit/$',
APIDocumentVersionOCRView.as_view(), APIDocumentVersionOCRView.as_view(),
name='document-version-ocr-submit-view' name='document-version-ocr-submit-view'
), ),
url( url(
r'^documents/(?P<document_pk>\d+)/versions/(?P<version_pk>\d+)/pages/(?P<page_pk>\d+)/ocr/$', r'^page/(?P<pk>\d+)/content/$',
APIDocumentPageOCRContentView.as_view(), APIDocumentPageOCRContentView.as_view(),
name='document-page-content-view' name='document-page-content-view'
), ),

View File

@@ -1,3 +0,0 @@
from __future__ import unicode_literals
default_app_config = 'quotas.apps.QuotasApp'

View File

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

View File

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

View File

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

View File

@@ -1,12 +0,0 @@
from __future__ import unicode_literals
class QuotaBaseException(Exception):
"""
Base exception for the quota app
"""
pass
class QuotaExceeded(QuotaBaseException):
pass

View File

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

View File

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

View File

@@ -1,38 +0,0 @@
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',
)

View File

@@ -1,29 +0,0 @@
# -*- 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',
},
),
]

View File

@@ -1,20 +0,0 @@
# -*- 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'),
),
]

View File

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

View File

@@ -1,20 +0,0 @@
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')
)

View File

@@ -1,211 +0,0 @@
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())
}

View File

@@ -1,32 +0,0 @@
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'
),
]

View File

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

View File

@@ -1,19 +0,0 @@
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()

View File

@@ -5,34 +5,10 @@ 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):
@@ -72,12 +48,6 @@ 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
for url in urlpatterns: app_urls += [
if url.regex.pattern not in self.__class__._patterns: url(r'^%s/' % (self.name or self.app.name), include(urlpatterns)),
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)
)

View File

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

View File

@@ -24,10 +24,7 @@ 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={ context={'request': self.context['request']}
'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

View File

@@ -1,9 +0,0 @@
from __future__ import unicode_literals
from rest_framework import serializers
class APIResourceSerializer(serializers.Serializer):
description = serializers.CharField()
label = serializers.CharField()
name = serializers.CharField()

View File

@@ -2,18 +2,15 @@ from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
from .api_views import APIResourceTypeListView from .views import APIBase, APIAppView, BrowseableObtainAuthToken
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( url(r'^api/(?P<path>.*)/?$', APIAppView.as_view(), name='api_app'),
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'

View File

@@ -13,6 +13,13 @@ 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.

View File

@@ -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, exc_info=True label or file_object.name, self, exception
) )
document.delete(to_trash=False) document.delete(to_trash=False)
raise raise
@@ -601,12 +601,6 @@ 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

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