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)
================
- Add support for notifications. GitLab #262.
- Add quota support. GitLab #284.
2.7.3 (2017-09-11)
==================
- Fix task manager queue list view. Thanks to LeVon Smoker for
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)
==================
@@ -357,7 +360,6 @@
- Add roadmap documentation chapter.
- API updates.
2.0.2 (2016-02-09)
==================
- Install testing dependencies when installing development dependencies.

View File

@@ -10,9 +10,9 @@ APP_LIST = (
'converter', 'django_gpg', 'document_comments', 'document_indexing',
'document_parsing', 'document_signatures', 'document_states', 'documents',
'dynamic_search', 'events', 'linking', 'lock_manager', 'mayan_statistics',
'mailer', 'metadata', 'mirroring', 'motd', 'navigation', 'ocr',
'permissions', 'rest_api', 'smart_settings', 'sources', 'storage', 'tags',
'task_manager', 'user_management'
'mailer', 'metadata', 'mirroring', 'motd', 'navigation', 'ocr', 'permissions',
'rest_api', 'smart_settings', 'sources', 'storage', 'tags', 'task_manager',
'user_management'
)
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,
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
----------------
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
==========
Other changes
-------------
- Resource based API endpoints
- Update required versions of Pillow to 4.1.1, django-compressor to 2.1.1, django-suit to 0.2.25, pyocr to 0.4.7, sh to 1.12.13
- Add notifications support.
- Fix task manager queue list view. Thanks to LeVon Smoker for
the report.
- 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.
Removals
--------
- None
* None
Upgrading from a previous version
---------------------------------
If installed via PIP
~~~~~~~~~~~~~~~~~~~~
Using PIP
~~~~~~~~~
Type in the console::
@@ -30,8 +30,8 @@ Type in the console::
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::
@@ -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
existing installation.
Manually upgrade/add the new requirements::
Next upgrade/add the new requirements::
$ pip install --upgrade -r requirements.txt
Remove deprecated requirements::
$ pip uninstall -y -r removals.txt
Common steps
~~~~~~~~~~~~
@@ -71,9 +67,8 @@ Backward incompatible changes
Bugs fixed or issues closed
===========================
* `GitLab issue #366 <https://gitlab.com/mayan-edms/mayan-edms/issues/366>`_ Proofread documentation
* `GitLab issue #379 <https://gitlab.com/mayan-edms/mayan-edms/issues/379>`_ Add new document version list view permission.
* `GitLab issue #379 <https://gitlab.com/mayan-edms/mayan-edms/issues/379>`_ Add new document version list view permission.
* `GitLab issue #431 <https://gitlab.com/mayan-edms/mayan-edms/issues/431>`_ can't create new mailer
* `GitLab issue #436 <https://gitlab.com/mayan-edms/mayan-edms/issues/436>`_ New document source menu does not contain source_ids
.. _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::
:maxdepth: 1
2.7.3
2.7.2
2.7.1
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
To perform automatic PEP8 checks, install flake8's git hook using:
.. code-block:: bash
$ flake8 --install-hook git
Imports
~~~~~~~
@@ -251,6 +245,47 @@ Steps to deploy a development version
$ ./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
--------------------
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
__title__ = 'Mayan EDMS'
__version__ = '2.7.2'
__build__ = 0x020702
__version__ = '2.7.3'
__build__ = 0x020703
__author__ = 'Roberto Rosario'
__author_email__ = 'roberto.rosario@mayan-edms.com'
__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):
if user.is_superuser or user.is_staff:
logger.debug(
'Unfiltered queryset returned to user "%s" as superuser '
'or staff', user
)
logger.debug('Unfiltered queryset returned to user "%s" as superuser or staff',
user)
return queryset
try:

View File

@@ -28,19 +28,19 @@ urlpatterns = [
api_urls = [
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'
),
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'
),
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'
),
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'
),
]

View File

@@ -5,7 +5,6 @@ import logging
from django.utils.translation import ugettext_lazy as _
from common import MayanAppConfig, menu_user
from navigation.classes import Separator, Text
from .links import link_logout, link_password_change
@@ -22,6 +21,6 @@ class AuthenticationApp(MayanAppConfig):
menu_user.bind_links(
links=(
Separator(), link_password_change, link_logout
link_password_change, link_logout
), position=99
)

View File

@@ -2,15 +2,13 @@ from __future__ import absolute_import, unicode_literals
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 = namespace.add_event_type(
label=_('Document added to cabinet'),
name='add_document'
event_cabinets_add_document = Event(
name='cabinets_add_document',
label=_('Document added to cabinet')
)
event_cabinets_remove_document = namespace.add_event_type(
label=_('Document removed from cabinet'),
name='remove_document'
event_cabinets_remove_document = Event(
name='cabinets_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().verb,
event_cabinets_add_document.id
event_cabinets_add_document.name
)
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().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 acls.models import AccessControlList
from documents.models import Document
from documents.permissions import permission_document_view
from .models import DocumentCheckout
from .permissions import (
permission_document_checkin, permission_document_checkin_override
permission_document_checkout, permission_document_checkin,
permission_document_checkin_override
)
from .serializers import (
DocumentCheckoutSerializer, NewDocumentCheckoutSerializer
@@ -46,23 +48,12 @@ class APICheckedoutDocumentListView(generics.ListCreateAPIView):
APICheckedoutDocumentListView, self
).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):
"""
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():
document = get_object_or_404(
@@ -93,7 +84,6 @@ class APICheckedoutDocumentListView(generics.ListCreateAPIView):
return Response(status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
'''
class APICheckedoutDocumentView(generics.RetrieveDestroyAPIView):

View File

@@ -11,15 +11,10 @@ from django.utils.translation import ugettext_lazy as _
from acls import ModelPermission
from common import MayanAppConfig, menu_facet, menu_main, menu_sidebar
from common.dashboards import dashboard_main
from events import ModelEventType
from mayan.celery import app
from rest_api.classes import APIEndPoint
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 .links import (
link_checkin_document, link_checkout_document, link_checkout_info,
@@ -56,10 +51,7 @@ class CheckoutsApp(MayanAppConfig):
Document.add_to_class(
'check_in',
lambda document,
user=None: DocumentCheckout.objects.check_in_document(
document, user
)
lambda document, user=None: DocumentCheckout.objects.check_in_document(document, user)
)
Document.add_to_class(
'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(
model=Document, permissions=(
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 events import EventTypeNamespace
from events.classes import Event
namespace = EventTypeNamespace(name='checkouts', label=_('Checkouts'))
event_document_auto_check_in = namespace.add_event_type(
name='document_auto_check_in',
event_document_auto_check_in = Event(
name='checkouts_document_auto_check_in',
label=_('Document automatically checked in')
)
event_document_check_in = namespace.add_event_type(
name='document_check_in', label=_('Document checked in')
event_document_check_in = Event(
name='checkouts_document_check_in', label=_('Document checked in')
)
event_document_check_out = namespace.add_event_type(
name='document_check_out', label=_('Document checked out')
event_document_check_out = Event(
name='checkouts_document_check_out', label=_('Document checked out')
)
event_document_forceful_check_in = namespace.add_event_type(
name='document_forceful_check_in',
event_document_forceful_check_in = Event(
name='checkouts_document_forceful_check_in',
label=_('Document forcefully checked in')
)

View File

@@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
class DocumentCheckoutManager(models.Manager):
def checkout_document(self, document, expiration_datetime, user, block_new_version=True):
return self.create(
self.create(
document=document, expiration_datetime=expiration_datetime,
user=user, block_new_version=block_new_version
)

View File

@@ -1,19 +1,15 @@
from __future__ import unicode_literals
from django.utils.translation import ugettext_lazy as _
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 .permissions import permission_document_checkout
class DocumentCheckoutSerializer(serializers.ModelSerializer):
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)
self.fields['document'] = DocumentSerializer()
@@ -21,33 +17,7 @@ class DocumentCheckoutSerializer(serializers.ModelSerializer):
model = DocumentCheckout
class NewDocumentCheckoutSerializer(serializers.ModelSerializer):
block_new_version = serializers.BooleanField()
document_pk = serializers.IntegerField(
help_text=_('Primary key of the document to be checked out.'),
write_only=True
)
class NewDocumentCheckoutSerializer(serializers.Serializer):
document = serializers.IntegerField()
expiration_datetime = serializers.DateTimeField()
class Meta:
fields = (
'block_new_version', 'document', 'document_pk',
'expiration_datetime', 'id'
)
model = DocumentCheckout
read_only_fields = ('document',)
write_only_fields = ('document_pk',)
def create(self, validated_data):
document = Document.objects.get(pk=validated_data.pop('document_pk'))
AccessControlList.objects.check_access(
permissions=permission_document_checkout,
user=self.context['request'].user, obj=document
)
validated_data['document'] = document
validated_data['user'] = self.context['request'].user
return super(NewDocumentCheckoutSerializer, self).create(
validated_data
)
block_new_version = serializers.BooleanField()

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 = [
url(
r'^checkouts/$', APICheckedoutDocumentListView.as_view(),
r'^documents/$', APICheckedoutDocumentListView.as_view(),
name='checkout-document-list'
),
url(
r'^checkouts/(?P<pk>[0-9]+)/$', APICheckedoutDocumentView.as_view(),
r'^documents/(?P<pk>[0-9]+)/$', APICheckedoutDocumentView.as_view(),
name='checkedout-document-view'
),
]

View File

@@ -67,8 +67,8 @@ class MayanAppConfig(apps.AppConfig):
except ImportError as exception:
if force_text(exception) not in ('No module named urls', 'No module named \'{}.urls\''.format(self.name)):
logger.error(
'Import time error when running AppConfig.ready() of app '
'"%s".', self.name
'Import time error when running AppConfig.ready(). Check '
'apps.py, urls.py, views.py, etc.'
)
raise exception
@@ -127,6 +127,7 @@ class CommonApp(MayanAppConfig):
Text(text=CommonApp.get_user_label_text), Separator(),
link_current_user_details, link_current_user_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
subtemplate contexts
"""
new_context = Context(context.flatten())
new_context = Context(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

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.utils.translation import ugettext_lazy as _
@@ -6,13 +6,9 @@ from django.utils.translation import ugettext_lazy as _
from acls import ModelPermission
from common import MayanAppConfig, menu_facet, menu_object, menu_sidebar
from documents.search import document_page_search, document_search
from events import ModelEventType
from navigation import SourceColumn
from rest_api.classes import APIEndPoint
from .events import (
event_document_comment_create, event_document_comment_delete
)
from .links import (
link_comment_add, link_comment_delete, link_comments_for_document
)
@@ -40,12 +36,6 @@ class DocumentCommentsApp(MayanAppConfig):
Comment = self.get_model('Comment')
ModelEventType.register(
model=Document, event_types=(
event_document_comment_create, event_document_comment_delete
)
)
ModelPermission.register(
model=Document, permissions=(
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 events import EventTypeNamespace
from events.classes import Event
namespace = EventTypeNamespace(
name='document_comments', label=_('Document comments')
event_document_comment_create = Event(
name='document_comment_create',
label=_('Document comment created')
)
event_document_comment_create = namespace.add_event_type(
name='create', label=_('Document comment created')
)
event_document_comment_delete = namespace.add_event_type(
name='delete', label=_('Document comment deleted')
event_document_comment_delete = Event(
name='document_comment_delete',
label=_('Document comment deleted')
)

View File

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

View File

@@ -72,12 +72,12 @@ urlpatterns = [
api_urls = [
url(
r'^indexes/node/(?P<pk>[0-9]+)/documents/$',
r'^index/node/(?P<pk>[0-9]+)/documents/$',
APIIndexNodeInstanceDocumentListView.as_view(),
name='index-node-documents'
),
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'
),
url(
@@ -85,12 +85,12 @@ api_urls = [
name='index-detail'
),
url(
r'^indexes/(?P<pk>[0-9]+)/template/$',
r'^index/(?P<pk>[0-9]+)/template/$',
APIIndexTemplateListView.as_view(), name='index-template-detail'
),
url(r'^indexes/$', APIIndexListView.as_view(), name='index-list'),
url(
r'^documents/(?P<pk>[0-9]+)/indexes/$',
r'^document/(?P<pk>[0-9]+)/indexes/$',
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 events import EventTypeNamespace
from events.classes import Event
namespace = EventTypeNamespace(
name='parsing', label=_('Document parsing')
event_parsing_document_version_submit = Event(
name='parsing_document_version_submit',
label=_('Document version submitted for parsing')
)
event_parsing_document_version_submit = namespace.add_event_type(
label=_('Document version submitted for parsing'),
name='document_version_submit'
)
event_parsing_document_version_finish = namespace.add_event_type(
label=_('Document version parsing finished'),
name='document_version_finish'
event_parsing_document_version_finish = Event(
name='parsing_document_version_finish',
label=_('Document version parsing finished')
)

View File

@@ -24,7 +24,7 @@ class DocumentParsingEventsTestCase(GenericDocumentTestCase):
)
self.assertEqual(
Action.objects.last().verb,
event_parsing_document_version_submit.id
event_parsing_document_version_submit.name
)
def test_document_version_finish_event(self):
@@ -35,5 +35,5 @@ class DocumentParsingEventsTestCase(GenericDocumentTestCase):
)
self.assertEqual(
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(
links=(
link_document_version_signature_list,
), sources=(DocumentVersion,)
), position=9, sources=(DocumentVersion,)
)
menu_object.bind_links(

View File

@@ -4,7 +4,7 @@ from django.apps import apps
from django.utils.translation import ugettext_lazy as _
from document_indexing.tasks import task_index_document
from events.classes import EventType
from events.classes import Event
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]
workflow_instance.do_transition(
comment=_('Event trigger: %s') % EventType.get(
name=action.verb
).label, transition=transition
comment=_('Event trigger: %s') % Event.get(name=action.verb).label,
transition=transition
)

View File

@@ -9,7 +9,7 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('events', '0005_auto_20170731_0452'),
('events', '0001_initial'),
('document_states', '0004_workflow_internal_name'),
]
@@ -17,19 +17,8 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='WorkflowTransitionTriggerEvent',
fields=[
(
'id', models.AutoField(
auto_created=True, primary_key=True, serialize=False,
verbose_name='ID'
)
),
(
'stored_event_type', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='trigger_events',
to='events.StoredEventType', verbose_name='Event type'
)
),
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('stored_event_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='trigger_events', to='events.EventType', verbose_name='Event type')),
],
options={
'verbose_name': 'Workflow transition trigger event',
@@ -39,28 +28,16 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='workflowtransition',
name='trigger_time_period',
field=models.PositiveIntegerField(
blank=True, help_text='Amount of time after which this '
'transition will trigger on its own.', null=True,
verbose_name='Trigger time period'
),
field=models.PositiveIntegerField(blank=True, help_text='Amount of time after which this transition will trigger on its own.', null=True, verbose_name='Trigger time period'),
),
migrations.AddField(
model_name='workflowtransition',
name='trigger_time_unit',
field=models.CharField(
blank=True, choices=[
('days', 'Days'), ('hours', 'Hours'),
('minutes', 'Minutes')
], max_length=8, null=True, verbose_name='Trigger time unit'),
field=models.CharField(blank=True, choices=[('days', 'Days'), ('hours', 'Hours'), ('minutes', 'Minutes')], max_length=8, null=True, verbose_name='Trigger time unit'),
),
migrations.AddField(
model_name='workflowtransitiontriggerevent',
name='transition',
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='document_states.WorkflowTransition',
verbose_name='Transition'
),
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='document_states.WorkflowTransition', verbose_name='Transition'),
),
]

View File

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

View File

@@ -17,7 +17,7 @@ from django.utils.translation import ugettext_lazy as _
from acls.models import AccessControlList
from common.validators import validate_internal_name
from documents.models import Document, DocumentType
from events.models import StoredEventType
from events.models import EventType
from permissions import Permission
from .error_logs import error_log_state_actions
@@ -306,8 +306,7 @@ class WorkflowTransitionTriggerEvent(models.Model):
related_name='trigger_events', verbose_name=_('Transition')
)
event_type = models.ForeignKey(
StoredEventType, on_delete=models.CASCADE,
verbose_name=_('Event type')
EventType, on_delete=models.CASCADE, verbose_name=_('Event type')
)
class Meta:

View File

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

View File

@@ -17,8 +17,8 @@ from common.views import (
)
from documents.models import Document
from documents.views import DocumentListView
from events.classes import EventType
from events.models import StoredEventType
from events.classes import Event
from events.models import EventType
from .classes import WorkflowAction
from .forms import (
@@ -675,7 +675,7 @@ class WorkflowStateListView(SingleObjectListView):
class SetupWorkflowTransitionTriggerEventListView(FormView):
form_class = WorkflowTransitionTriggerEventRelationshipFormSet
submodel = StoredEventType
submodel = EventType
def dispatch(self, *args, **kwargs):
messages.warning(
@@ -689,7 +689,7 @@ class SetupWorkflowTransitionTriggerEventListView(FormView):
user=self.request.user, obj=self.get_object().workflow
)
EventType.refresh()
Event.refresh()
return super(
SetupWorkflowTransitionTriggerEventListView, self
).dispatch(*args, **kwargs)
@@ -735,10 +735,8 @@ class SetupWorkflowTransitionTriggerEventListView(FormView):
initial = []
# 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_queryset = StoredEventType.objects.filter(
name__in=event_type_ids
)
event_type_ids = [event_type.name for event_type in Event.all()]
event_type_queryset = EventType.objects.filter(name__in=event_type_ids)
for event_type in event_type_queryset:
initial.append({

View File

@@ -23,16 +23,12 @@ from converter.permissions import (
permission_transformation_delete, permission_transformation_edit,
permission_transformation_view,
)
from events import ModelEventType
from events.links import (
link_events_for_object, link_object_event_types_user_subcriptions_list,
link_object_event_types_user_subcriptions_list_with_icon
)
from events.links import link_events_for_object
from events.permissions import permission_events_view
from mayan.celery import app
from mayan_statistics.classes import StatisticNamespace, CharJSLine
from navigation import SourceColumn
from rest_api.classes import APIEndPoint, APIResource
from rest_api.classes import APIEndPoint
from rest_api.fields import DynamicSerializerField
from .dashboard_widgets import (
@@ -40,12 +36,6 @@ from .dashboard_widgets import (
widget_new_documents_this_month, widget_pages_per_month,
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 (
create_default_document_type, handler_scan_duplicates_for
)
@@ -117,9 +107,6 @@ class DocumentsApp(MayanAppConfig):
from actstream import registry
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')
Document = self.get_model('Document')
@@ -154,19 +141,6 @@ class DocumentsApp(MayanAppConfig):
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(
model=Document, permissions=(
permission_acl_edit, permission_acl_view,
@@ -402,8 +376,7 @@ class DocumentsApp(MayanAppConfig):
menu_object.bind_links(
links=(
link_document_type_edit, link_document_type_filename_list,
link_acl_list, link_object_event_types_user_subcriptions_list,
link_document_type_delete
link_acl_list, link_document_type_delete
), sources=(DocumentType,)
)
menu_object.bind_links(
@@ -460,22 +433,15 @@ class DocumentsApp(MayanAppConfig):
links=(link_document_properties,), sources=(Document,), position=2
)
menu_facet.bind_links(
links=(
link_events_for_object,
link_object_event_types_user_subcriptions_list_with_icon,
link_document_version_list,
), sources=(Document,), position=2
links=(link_events_for_object, 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_version_view,), sources=(DocumentVersion,)
)
# Document actions
menu_object.bind_links(
links=(
link_document_version_view, link_document_version_revert,
link_document_version_download
link_document_version_revert, link_document_version_download
),
sources=(DocumentVersion,)
)
@@ -524,6 +490,9 @@ class DocumentsApp(MayanAppConfig):
link_document_version_return_list
), sources=(DocumentVersion,)
)
menu_facet.bind_links(
links=(link_document_version_view,), sources=(DocumentVersion,)
)
namespace = StatisticNamespace(slug='documents', label=_('Documents'))
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 events import EventTypeNamespace
from events.classes import Event
namespace = EventTypeNamespace(name='documents', label=_('Documents'))
event_document_create = namespace.add_event_type(
name='document_create', label=_('Document created')
event_document_create = Event(
name='documents_document_create', label=_('Document created')
)
event_document_download = namespace.add_event_type(
name='document_download', label=_('Document downloaded')
event_document_download = Event(
name='documents_document_download',
label=_('Document downloaded')
)
event_document_properties_edit = namespace.add_event_type(
name='document_edit', label=_('Document properties edited')
event_document_properties_edit = Event(
name='documents_document_edit', label=_('Document properties edited')
)
event_document_type_change = namespace.add_event_type(
name='document_type_change', label=_('Document type changed')
event_document_type_change = Event(
name='documents_document_type_change', label=_('Document type changed')
)
event_document_new_version = namespace.add_event_type(
name='document_new_version', label=_('New version uploaded')
event_document_new_version = Event(
name='documents_document_new_version', label=_('New version uploaded')
)
event_document_version_revert = namespace.add_event_type(
name='document_version_revert', label=_('Document version reverted')
event_document_version_revert = Event(
name='documents_document_version_revert',
label=_('Document version reverted')
)
event_document_view = namespace.add_event_type(
name='document_view', label=_('Document viewed')
event_document_view = Event(
name='documents_document_view',
label=_('Document viewed')
)

View File

@@ -100,11 +100,6 @@ class DocumentType(models.Model):
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):
return (self.label,)
@@ -227,13 +222,9 @@ class Document(models.Model):
if new_document:
if user:
self.add_as_recent_document_for_user(user)
event_document_create.commit(
actor=user, target=self, action_object=self.document_type
)
event_document_create.commit(actor=user, target=self)
else:
event_document_create.commit(
target=self, action_object=self.document_type
)
event_document_create.commit(target=self)
else:
if _commit_events:
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
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'))

View File

@@ -64,7 +64,7 @@ class DocumentEventsTestCase(GenericDocumentViewTestCase):
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.actor, self.user)
@@ -98,6 +98,6 @@ class DocumentEventsTestCase(GenericDocumentViewTestCase):
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.actor, self.user)

View File

@@ -916,73 +916,6 @@ class DocumentVersionTestCase(GenericDocumentViewTestCase):
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):
def setUp(self):
super(DeletedDocumentTestCase, self).setUp()

View File

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

View File

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

View File

@@ -2,19 +2,9 @@ from __future__ import unicode_literals
from django.contrib import admin
from .models import EventSubscription, Notification, StoredEventType
from .models import EventType
@admin.register(EventSubscription)
class EventSubscriptionAdmin(admin.ModelAdmin):
list_display = ('user', 'stored_event_type')
@admin.register(StoredEventType)
class StoredEventTypeAdmin(admin.ModelAdmin):
@admin.register(EventType)
class EventTypeAdmin(admin.ModelAdmin):
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 rest_api.permissions import MayanPermission
from .classes import EventType, EventTypeNamespace
from .models import Notification
from .classes import Event
from .permissions import permission_events_view
from .serializers import (
EventSerializer, EventTypeSerializer, EventTypeNamespaceSerializer,
NotificationSerializer
)
from .serializers import EventSerializer, EventTypeSerializer
class APIObjectEventListView(generics.ListAPIView):
@@ -50,72 +46,13 @@ class APIObjectEventListView(generics.ListAPIView):
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):
"""
Returns a list of all the available event types.
"""
serializer_class = EventTypeSerializer
queryset = EventType.all()
def get_serializer_context(self):
return {
'format': self.format_kwarg,
'request': self.request,
'view': self
}
queryset = sorted(Event.all(), key=lambda event: event.name)
class APIEventListView(generics.ListAPIView):
@@ -127,20 +64,3 @@ class APIEventListView(generics.ListAPIView):
permission_classes = (MayanPermission,)
queryset = Action.objects.all()
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.utils.translation import ugettext_lazy as _
from common import (
MayanAppConfig, menu_main, menu_object, menu_secondary, menu_tools,
menu_user
)
from common.widgets import two_state_template
from common import MayanAppConfig, menu_tools
from navigation import SourceColumn
from rest_api.classes import APIEndPoint
from .links import (
link_events_list, link_event_types_subscriptions_list,
link_notification_mark_read, link_notification_mark_read_all,
link_user_notifications_list,
)
from .links import link_events_list
from .licenses import * # NOQA
from .widgets import event_object_link, event_type_link
@@ -36,8 +28,6 @@ class EventsApp(MayanAppConfig):
def ready(self):
super(EventsApp, self).ready()
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')
@@ -49,7 +39,7 @@ class EventsApp(MayanAppConfig):
func=lambda context: event_actor(context['object'])
)
SourceColumn(
source=Action, label=_('Event'),
source=Action, label=_('Verb'),
func=lambda context: event_type_link(context['object'])
)
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_user.bind_links(links=(link_event_types_subscriptions_list,))

View File

@@ -1,57 +1,18 @@
from __future__ import unicode_literals
import logging
from django.apps import apps
from django.contrib.auth import get_user_model
from django.core.exceptions import PermissionDenied
from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from actstream import action
from .permissions import permission_events_view
logger = logging.getLogger(__name__)
@python_2_unicode_compatible
class EventTypeNamespace(object):
class Event(object):
_registry = {}
@classmethod
def all(cls):
return sorted(cls._registry.values())
@classmethod
def get(cls, name):
return cls._registry[name]
def __init__(self, name, label):
self.name = name
self.label = label
self.event_types = []
self.__class__._registry[name] = self
def __str__(self):
return force_text(self.label)
def add_event_type(self, name, label):
event_type = EventType(namespace=self, name=name, label=label)
self.event_types.append(event_type)
return event_type
def get_event_types(self):
return EventType.sort(event_type_list=self.event_types)
@python_2_unicode_compatible
class EventType(object):
_registry = {}
@classmethod
def all(cls):
# Return sorted permisions by namespace.name
return EventType.sort(event_type_list=cls._registry.values())
return Event.sort(event_type_list=cls._registry.values())
@classmethod
def get(cls, name):
@@ -59,174 +20,51 @@ class EventType(object):
return cls._registry[name]
except 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):
self.namespace = namespace
self.name = name
self.label = label
self.stored_event_type = None
self.__class__._registry[self.id] = self
def __str__(self):
return force_text('{}: {}'.format(self.namespace.label, self.label))
@property
def id(self):
return '%s.%s' % (self.namespace.name, self.name)
@classmethod
def get_label(cls, name):
try:
return cls.get(name=name).label
except KeyError as exception:
return force_text(exception)
@classmethod
def refresh(cls):
for event_type in cls.all():
event_type.get_stored_event_type()
def get_stored_event_type(self):
if not self.stored_event_type:
StoredEventType = apps.get_model('events', 'StoredEventType')
self.stored_event_type, created = StoredEventType.objects.get_or_create(
name=self.id
)
return self.stored_event_type
event_type.get_type()
@staticmethod
def sort(event_type_list):
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):
AccessControlList = apps.get_model(
app_label='acls', model_name='AccessControlList'
)
Action = apps.get_model(
app_label='actstream', model_name='Action'
)
ContentType = apps.get_model(
app_label='contenttypes', model_name='ContentType'
)
Notification = apps.get_model(
app_label='events', model_name='Notification'
)
if not self.event_type:
EventType = apps.get_model('events', 'EventType')
self.event_type, created = EventType.objects.get_or_create(
name=self.name
)
results = action.send(
actor or target, actor=actor, verb=self.id,
action.send(
actor or target, actor=actor, verb=self.name,
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
def get_notification_count(context):
return context['request'].user.notifications.filter(read=False).count()
link_events_list = Link(
icon='fa fa-list-ol', permissions=(permission_events_view,),
text=_('Events'), view='events:events_list'
)
link_events_details = Link(
text=_('Events'), view='events:events_list'
)
link_events_for_object = Link(
icon='fa fa-list-ol', permissions=(permission_events_view,),
text=_('Events'), view='events:events_for_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 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.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 actstream.models import Action
from .classes import EventType
from .managers import (
EventSubscriptionManager, ObjectEventSubscriptionManager
)
from .classes import Event
@python_2_unicode_compatible
class StoredEventType(models.Model):
class EventType(models.Model):
name = models.CharField(
max_length=64, unique=True, verbose_name=_('Name')
)
class Meta:
verbose_name = _('Stored event type')
verbose_name_plural = _('Stored event types')
verbose_name = _('Event type')
verbose_name_plural = _('Event types')
def __str__(self):
return force_text(self.get_class())
return self.get_class().label
def get_class(self):
return EventType.get(name=self.name)
return Event.get(name=self.name)
@property
def label(self):
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 rest_framework import serializers
from rest_framework.reverse import reverse
from common.serializers import ContentTypeSerializer
from rest_api.fields import DynamicSerializerField
from user_management.serializers import UserSerializer
from .classes import EventType
from .models import Notification, StoredEventType
class EventTypeNamespaceSerializer(serializers.Serializer):
label = serializers.CharField()
name = serializers.CharField()
url = serializers.SerializerMethodField()
event_types_url = serializers.HyperlinkedIdentityField(
lookup_field='name',
view_name='rest_api:event-type-namespace-event-type-list',
)
def get_url(self, instance):
return reverse(
'rest_api:event-type-namespace-detail', args=(
instance.name,
), request=self.context['request'], format=self.context['format']
)
from .classes import Event
from .models import EventType
class EventTypeSerializer(serializers.Serializer):
label = 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):
if isinstance(instance, EventType):
if isinstance(instance, Event):
return super(EventTypeSerializer, self).to_representation(
instance
)
elif isinstance(instance, StoredEventType):
elif isinstance(instance, EventType):
return super(EventTypeSerializer, self).to_representation(
instance.get_class()
)
elif isinstance(instance, string_types):
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'
)
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 .api_views import (
APIEventListView, APIEventTypeListView, APIEventTypeNamespaceDetailView,
APIEventTypeNamespaceEventTypeListView, APIEventTypeNamespaceListView,
APINotificationListView, APIObjectEventListView
)
from .views import (
EventListView, EventTypeSubscriptionListView, NotificationListView,
NotificationMarkRead, NotificationMarkReadAll, ObjectEventListView,
ObjectEventTypeSubscriptionListView, VerbEventListView
APIEventListView, APIEventTypeListView, APIObjectEventListView
)
from .views import EventListView, ObjectEventListView, VerbEventListView
urlpatterns = [
url(r'^all/$', EventListView.as_view(), name='events_list'),
@@ -20,60 +14,16 @@ urlpatterns = [
ObjectEventListView.as_view(), name='events_for_object'
),
url(
r'^by_verb/(?P<verb>[\w\-\.]+)/$', VerbEventListView.as_view(),
r'^by_verb/(?P<verb>[\w\-]+)/$', VerbEventListView.as_view(),
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 = [
url(
r'^event_type_namespaces/(?P<name>[-\w]+)/$',
APIEventTypeNamespaceDetailView.as_view(),
name='event-type-namespace-detail'
),
url(
r'^event_type_namespaces/(?P<name>[-\w]+)/event_types/$',
APIEventTypeNamespaceEventTypeListView.as_view(),
name='event-type-namespace-event-type-list'
),
url(
r'^event_type_namespaces/$', APIEventTypeNamespaceListView.as_view(),
name='event-type-namespace-list'
),
url(
r'^event_types/$', APIEventTypeListView.as_view(),
name='event-type-list'
),
url(r'^types/$', APIEventTypeListView.as_view(), name='event-type-list'),
url(r'^events/$', APIEventListView.as_view(), name='event-list'),
url(
r'^notifications/$', APINotificationListView.as_view(),
name='notification-list'
),
url(
r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/events/$',
r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/events/$',
APIObjectEventListView.as_view(), name='object-event-list'
),
]

View File

@@ -1,24 +1,17 @@
from __future__ import absolute_import, unicode_literals
from django.contrib import messages
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.urls import reverse
from django.utils.translation import ugettext_lazy as _
from actstream.models import Action, any_stream
from acls.models import AccessControlList
from common.generics import FormView, SimpleView
from common.utils import encapsulate
from common.views import SingleObjectListView
from .classes import EventType, ModelEventType
from .forms import (
EventTypeUserRelationshipFormSet, ObjectEventTypeUserRelationshipFormSet
)
from .models import StoredEventType
from .classes import Event
from .permissions import permission_events_view
from .widgets import event_object_link
@@ -44,96 +37,6 @@ class EventListView(SingleObjectListView):
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):
view_permissions = None
@@ -170,76 +73,6 @@ class ObjectEventListView(EventListView):
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):
def get_extra_context(self):
return {
@@ -254,7 +87,7 @@ class VerbEventListView(SingleObjectListView):
'hide_object': True,
'title': _(
'Events of type: %s'
) % EventType.get(name=self.kwargs['verb']),
) % Event.get_label(self.kwargs['verb']),
}
def get_object_list(self):

View File

@@ -1,28 +1,23 @@
from __future__ import unicode_literals
from django.urls import reverse
from django.utils.encoding import force_text
from django.utils.safestring import mark_safe
from .classes import EventType
from .classes import Event
def event_object_link(entry, attribute='target'):
label = ''
url = '#'
obj_type = ''
obj = getattr(entry, attribute)
if obj:
obj_type = '{}: '.format(obj._meta.verbose_name)
if hasattr(obj, 'get_absolute_url'):
url = obj.get_absolute_url()
label = force_text(obj)
else:
obj_type = ''
return mark_safe(
'<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(
'<a href="%(url)s">%(label)s</a>' % {
'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(
MetadataType, on_delete=models.CASCADE, verbose_name=_('Type')
)
value = models.TextField(
blank=True, db_index=True, null=True,
value = models.CharField(
blank=True, db_index=True, max_length=255, null=True,
verbose_name=_('Value')
)

View File

@@ -190,25 +190,6 @@ class MetadataTestCase(MetadataTypeMixin, BaseTestCase):
self.metadata_type.save()
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):
"""
When switching document types, add the required metadata of the new

View File

@@ -1,5 +1,4 @@
from __future__ import unicode_literals
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:
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
return resolved_link

View File

@@ -7,5 +7,8 @@ TEST_PERMISSION_NAME = 'test permission name'
TEST_PERMISSION_LABEL = 'test permission label'
TEST_LINK_TEXT = 'test link text'
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_UNICODE_STRING = 'úñí©óðé'
TEST_URL = 'test-URL'

View File

@@ -15,7 +15,8 @@ from ..classes import Link, Menu
from .literals import (
TEST_PERMISSION_NAMESPACE_NAME, TEST_PERMISSION_NAMESPACE_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)
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):
def setUp(self):
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.response import Response
from django.shortcuts import get_object_or_404
from documents.models import Document, DocumentPage, DocumentVersion
from rest_api.permissions import MayanPermission
@@ -28,6 +26,10 @@ class APIDocumentOCRView(generics.GenericAPIView):
Submit a document for OCR.
---
omit_serializer: true
parameters:
- name: pk
paramType: path
type: number
responseMessages:
- code: 202
message: Accepted
@@ -38,19 +40,12 @@ class APIDocumentOCRView(generics.GenericAPIView):
class APIDocumentVersionOCRView(generics.GenericAPIView):
lookup_url_kwarg = 'version_pk'
mayan_object_permissions = {
'POST': (permission_ocr_document,)
}
permission_classes = (MayanPermission,)
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):
return None
@@ -59,6 +54,10 @@ class APIDocumentVersionOCRView(generics.GenericAPIView):
Submit a document version for OCR.
---
omit_serializer: true
parameters:
- name: pk
paramType: path
type: number
responseMessages:
- code: 202
message: Accepted
@@ -71,25 +70,20 @@ class APIDocumentVersionOCRView(generics.GenericAPIView):
class APIDocumentPageOCRContentView(generics.RetrieveAPIView):
"""
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 = {
'GET': (permission_ocr_content_view,),
}
permission_classes = (MayanPermission,)
serializer_class = DocumentPageOCRContentSerializer
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()
queryset = DocumentPage.objects.all()
def retrieve(self, request, *args, **kwargs):
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 events.classes import EventTypeNamespace
from events.classes import Event
namespace = EventTypeNamespace(name='ocr', label=_('OCR'))
event_ocr_document_version_submit = namespace.add_event_type(
label=_('Document version submitted for OCR'),
name='document_version_submit'
event_ocr_document_version_submit = Event(
name='ocr_document_version_submit',
label=_('Document version submitted for OCR')
)
event_ocr_document_version_finish = namespace.add_event_type(
label=_('Document version OCR finished'),
name='document_version_finish'
event_ocr_document_version_finish = Event(
name='ocr_document_version_finish',
label=_('Document version OCR finished')
)

View File

@@ -63,9 +63,7 @@ class OCRAPITestCase(BaseAPITestCase):
response = self.client.post(
reverse(
'rest_api:document-version-ocr-submit-view',
args=(
self.document.pk, self.document.latest_version.pk,
)
args=(self.document.latest_version.pk,)
)
)
@@ -79,10 +77,7 @@ class OCRAPITestCase(BaseAPITestCase):
response = self.client.get(
reverse(
'rest_api:document-page-content-view',
args=(
self.document.pk, self.document.latest_version.pk,
self.document.latest_version.pages.first().pk,
)
args=(self.document.latest_version.pages.first().pk,)
),
)

View File

@@ -19,7 +19,7 @@ class OCREventsTestCase(GenericDocumentTestCase):
)
self.assertEqual(
Action.objects.last().verb,
event_ocr_document_version_submit.id
event_ocr_document_version_submit.name
)
def test_document_version_finish_event(self):
@@ -31,5 +31,5 @@ class OCREventsTestCase(GenericDocumentTestCase):
)
self.assertEqual(
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 = [
url(
r'^documents/(?P<pk>\d+)/ocr/$', APIDocumentOCRView.as_view(),
r'^document/(?P<pk>\d+)/submit/$', APIDocumentOCRView.as_view(),
name='document-ocr-submit-view'
),
url(
r'^documents/(?P<document_pk>\d+)/versions/(?P<version_pk>\d+)/ocr/$',
r'^document_version/(?P<pk>\d+)/submit/$',
APIDocumentVersionOCRView.as_view(),
name='document-version-ocr-submit-view'
),
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(),
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.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
class APIEndPoint(object):
_registry = {}
_patterns = []
@classmethod
def get_all(cls):
@@ -72,12 +48,6 @@ class APIEndPoint(object):
def register_urls(self, urlpatterns):
from .urls import urlpatterns as app_urls
for url in urlpatterns:
if url.regex.pattern not in self.__class__._patterns:
app_urls.append(url)
self.__class__._patterns.append(url.regex.pattern)
else:
raise APIResourcePatternError(
'App "{}" tried to register API URL pattern "{}", which '
'already exists'.format(self.app.label, url.regex.pattern)
)
app_urls += [
url(r'^%s/' % (self.name or self.app.name), include(urlpatterns)),
]

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():
if isinstance(value, klass):
return serializer_class(
context={
'format': self.context['format'],
'request': self.context['request']
}
context={'request': self.context['request']}
).to_representation(instance=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 .api_views import APIResourceTypeListView
from .views import APIBase, BrowseableObtainAuthToken
from .views import APIBase, APIAppView, BrowseableObtainAuthToken
urlpatterns = []
urlpatterns = [
]
api_urls = [
url(r'^$', APIBase.as_view(), name='api_root'),
url(
r'^resources/$', APIResourceTypeListView.as_view(),
name='resource-list'
),
url(r'^api/(?P<path>.*)/?$', APIAppView.as_view(), name='api_app'),
url(
r'^auth/token/obtain/$', BrowseableObtainAuthToken.as_view(),
name='auth_token_obtain'

View File

@@ -13,6 +13,13 @@ class APIBase(SwaggerResourcesView):
renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer)
class APIAppView(SwaggerApiView):
"""
Entry points of the selected app.
"""
renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer)
class BrowseableObtainAuthToken(ObtainAuthToken):
"""
Obtain an API authentication token.

View File

@@ -124,7 +124,7 @@ class Source(models.Model):
logger.critical(
'Unexpected exception while trying to create version for '
'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)
raise
@@ -601,12 +601,6 @@ class EmailBaseModel(IntervalBaseModel):
if 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:
filename = _('attachment-%i') % counter
counter += 1

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