Backport support for global and object event notification. GitLab issue #262.

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
This commit is contained in:
Roberto Rosario
2017-08-01 01:18:07 -04:00
parent c4c0d4a007
commit f0baa16cde
38 changed files with 1274 additions and 159 deletions

View File

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

View File

@@ -2,13 +2,13 @@ from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from events.classes import Event from events import EventTypeNamespace
event_cabinets_add_document = Event( namespace = EventTypeNamespace(name='cabinets', label=_('Cabinets'))
name='cabinets_add_document',
label=_('Document added to cabinet') event_cabinets_add_document = namespace.add_event_type(
label=_('Document added to cabinet'), name='add_document'
) )
event_cabinets_remove_document = Event( event_cabinets_remove_document = namespace.add_event_type(
name='cabinets_remove_document', label=_('Document removed from cabinet'), name='remove_document'
label=_('Document removed from cabinet')
) )

View File

@@ -10,11 +10,17 @@ from django.utils.translation import ugettext_lazy as _
from acls import ModelPermission from acls import ModelPermission
from common import MayanAppConfig, menu_facet, menu_main, menu_sidebar from common import MayanAppConfig, menu_facet, menu_main, menu_sidebar
from common.classes import DashboardWidget
from common.dashboards import dashboard_main from common.dashboards import dashboard_main
from events import ModelEventType
from mayan.celery import app from mayan.celery import app
from rest_api.classes import APIEndPoint from rest_api.classes import APIEndPoint
from .dashboard_widgets import widget_checkouts from .dashboard_widgets import widget_checkouts
from .events import (
event_document_auto_check_in, event_document_check_in,
event_document_check_out, event_document_forceful_check_in
)
from .handlers import check_new_version_creation from .handlers import check_new_version_creation
from .links import ( from .links import (
link_checkin_document, link_checkout_document, link_checkout_info, link_checkin_document, link_checkout_document, link_checkout_info,
@@ -72,6 +78,13 @@ class CheckoutsApp(MayanAppConfig):
) )
) )
ModelEventType.register(
model=Document, event_types=(
event_document_auto_check_in, event_document_check_in,
event_document_check_out, event_document_forceful_check_in
)
)
ModelPermission.register( ModelPermission.register(
model=Document, permissions=( model=Document, permissions=(
permission_document_checkout, permission_document_checkout,

View File

@@ -2,19 +2,21 @@ from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from events.classes import Event from events import EventTypeNamespace
event_document_auto_check_in = Event( namespace = EventTypeNamespace(name='checkouts', label=_('Checkouts'))
name='checkouts_document_auto_check_in',
event_document_auto_check_in = namespace.add_event_type(
name='document_auto_check_in',
label=_('Document automatically checked in') label=_('Document automatically checked in')
) )
event_document_check_in = Event( event_document_check_in = namespace.add_event_type(
name='checkouts_document_check_in', label=_('Document checked in') name='document_check_in', label=_('Document checked in')
) )
event_document_check_out = Event( event_document_check_out = namespace.add_event_type(
name='checkouts_document_check_out', label=_('Document checked out') name='document_check_out', label=_('Document checked out')
) )
event_document_forceful_check_in = Event( event_document_forceful_check_in = namespace.add_event_type(
name='checkouts_document_forceful_check_in', name='document_forceful_check_in',
label=_('Document forcefully checked in') label=_('Document forcefully checked in')
) )

View File

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

View File

@@ -1,4 +1,4 @@
from __future__ import unicode_literals from __future__ import absolute_import, unicode_literals
from django.apps import apps from django.apps import apps
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@@ -6,9 +6,13 @@ from django.utils.translation import ugettext_lazy as _
from acls import ModelPermission from acls import ModelPermission
from common import MayanAppConfig, menu_facet, menu_object, menu_sidebar from common import MayanAppConfig, menu_facet, menu_object, menu_sidebar
from documents.search import document_page_search, document_search from documents.search import document_page_search, document_search
from events import ModelEventType
from navigation import SourceColumn from navigation import SourceColumn
from rest_api.classes import APIEndPoint from rest_api.classes import APIEndPoint
from .events import (
event_document_comment_create, event_document_comment_delete
)
from .links import ( from .links import (
link_comment_add, link_comment_delete, link_comments_for_document link_comment_add, link_comment_delete, link_comments_for_document
) )
@@ -36,6 +40,12 @@ class DocumentCommentsApp(MayanAppConfig):
Comment = self.get_model('Comment') Comment = self.get_model('Comment')
ModelEventType.register(
model=Document, event_types=(
event_document_comment_create, event_document_comment_delete
)
)
ModelPermission.register( ModelPermission.register(
model=Document, permissions=( model=Document, permissions=(
permission_comment_create, permission_comment_delete, permission_comment_create, permission_comment_delete,

View File

@@ -2,13 +2,15 @@ from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from events.classes import Event from events import EventTypeNamespace
event_document_comment_create = Event( namespace = EventTypeNamespace(
name='document_comment_create', name='document_comments', label=_('Document comments')
label=_('Document comment created')
) )
event_document_comment_delete = Event(
name='document_comment_delete', event_document_comment_create = namespace.add_event_type(
label=_('Document comment deleted') name='create', label=_('Document comment created')
)
event_document_comment_delete = namespace.add_event_type(
name='delete', label=_('Document comment deleted')
) )

View File

@@ -2,13 +2,15 @@ from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from events.classes import Event from events import EventTypeNamespace
event_parsing_document_version_submit = Event( namespace = EventTypeNamespace(
name='parsing_document_version_submit', name='document_parsing', label=_('Document parsing')
label=_('Document version submitted for parsing')
) )
event_parsing_document_version_finish = Event(
name='parsing_document_version_finish', event_parsing_document_version_submit = namespace.add_event_type(
label=_('Document version parsing finished') label=_('Document version submitted for parsing'), name='version_submit'
)
event_parsing_document_version_finish = namespace.add_event_type(
label=_('Document version parsing finished'), name='version_finish'
) )

View File

@@ -4,7 +4,7 @@ from django.apps import apps
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from document_indexing.tasks import task_index_document from document_indexing.tasks import task_index_document
from events.classes import Event from events.classes import EventType
def handler_index_document(sender, **kwargs): def handler_index_document(sender, **kwargs):
@@ -42,7 +42,7 @@ def handler_trigger_transition(sender, **kwargs):
transition = list(set(trigger_transitions) & set(workflow_instance.get_transition_choices()))[0] transition = list(set(trigger_transitions) & set(workflow_instance.get_transition_choices()))[0]
workflow_instance.do_transition( workflow_instance.do_transition(
comment=_('Event trigger: %s') % Event.get(name=action.verb).label, comment=_('Event trigger: %s') % EventType.get(name=action.verb).label,
transition=transition transition=transition
) )

View File

@@ -9,7 +9,7 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('events', '0001_initial'), ('events', '0005_auto_20170731_0452'),
('document_states', '0004_workflow_internal_name'), ('document_states', '0004_workflow_internal_name'),
] ]

View File

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

View File

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

View File

@@ -23,7 +23,11 @@ from converter.permissions import (
permission_transformation_delete, permission_transformation_edit, permission_transformation_delete, permission_transformation_edit,
permission_transformation_view, permission_transformation_view,
) )
from events.links import link_events_for_object from events import ModelEventType
from events.links import (
link_events_for_object, link_object_event_types_user_subcriptions_list,
link_object_event_types_user_subcriptions_list_with_icon
)
from events.permissions import permission_events_view from events.permissions import permission_events_view
from mayan.celery import app from mayan.celery import app
from mayan_statistics.classes import StatisticNamespace, CharJSLine from mayan_statistics.classes import StatisticNamespace, CharJSLine
@@ -36,6 +40,12 @@ from .dashboard_widgets import (
widget_new_documents_this_month, widget_pages_per_month, widget_new_documents_this_month, widget_pages_per_month,
widget_total_documents widget_total_documents
) )
from .events import (
event_document_create, event_document_download,
event_document_properties_edit, event_document_type_change,
event_document_new_version, event_document_version_revert,
event_document_view
)
from .handlers import ( from .handlers import (
create_default_document_type, handler_scan_duplicates_for create_default_document_type, handler_scan_duplicates_for
) )
@@ -142,6 +152,19 @@ class DocumentsApp(MayanAppConfig):
label=_('MIME type'), name='versions__mimetype', type_name='field' label=_('MIME type'), name='versions__mimetype', type_name='field'
) )
ModelEventType.register(
model=DocumentType, event_types=(
event_document_create,
)
)
ModelEventType.register(
model=Document, event_types=(
event_document_download, event_document_properties_edit,
event_document_type_change, event_document_new_version,
event_document_version_revert, event_document_view
)
)
ModelPermission.register( ModelPermission.register(
model=Document, permissions=( model=Document, permissions=(
permission_acl_edit, permission_acl_view, permission_acl_edit, permission_acl_view,
@@ -389,7 +412,8 @@ class DocumentsApp(MayanAppConfig):
menu_object.bind_links( menu_object.bind_links(
links=( links=(
link_document_type_edit, link_document_type_filename_list, link_document_type_edit, link_document_type_filename_list,
link_acl_list, link_document_type_delete link_acl_list, link_object_event_types_user_subcriptions_list,
link_document_type_delete
), sources=(DocumentType,) ), sources=(DocumentType,)
) )
menu_object.bind_links( menu_object.bind_links(
@@ -446,8 +470,11 @@ class DocumentsApp(MayanAppConfig):
links=(link_document_properties,), sources=(Document,), position=2 links=(link_document_properties,), sources=(Document,), position=2
) )
menu_facet.bind_links( menu_facet.bind_links(
links=(link_events_for_object, link_document_version_list,), links=(
sources=(Document,), position=2 link_events_for_object,
link_object_event_types_user_subcriptions_list_with_icon,
link_document_version_list,
), sources=(Document,), position=2
) )
menu_facet.bind_links(links=(link_document_pages,), sources=(Document,)) menu_facet.bind_links(links=(link_document_pages,), sources=(Document,))

View File

@@ -2,29 +2,28 @@ from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from events.classes import Event from events import EventTypeNamespace
event_document_create = Event( namespace = EventTypeNamespace(name='documents', label=_('Documents'))
name='documents_document_create', label=_('Document created')
event_document_create = namespace.add_event_type(
name='document_create', label=_('Document created')
) )
event_document_download = Event( event_document_download = namespace.add_event_type(
name='documents_document_download', name='document_download', label=_('Document downloaded')
label=_('Document downloaded')
) )
event_document_properties_edit = Event( event_document_properties_edit = namespace.add_event_type(
name='documents_document_edit', label=_('Document properties edited') name='document_edit', label=_('Document properties edited')
) )
event_document_type_change = Event( event_document_type_change = namespace.add_event_type(
name='documents_document_type_change', label=_('Document type changed') name='document_type_change', label=_('Document type changed')
) )
event_document_new_version = Event( event_document_new_version = namespace.add_event_type(
name='documents_document_new_version', label=_('New version uploaded') name='document_new_version', label=_('New version uploaded')
) )
event_document_version_revert = Event( event_document_version_revert = namespace.add_event_type(
name='documents_document_version_revert', name='document_version_revert', label=_('Document version reverted')
label=_('Document version reverted')
) )
event_document_view = Event( event_document_view = namespace.add_event_type(
name='documents_document_view', name='document_view', label=_('Document viewed')
label=_('Document viewed')
) )

View File

@@ -222,9 +222,13 @@ class Document(models.Model):
if new_document: if new_document:
if user: if user:
self.add_as_recent_document_for_user(user) self.add_as_recent_document_for_user(user)
event_document_create.commit(actor=user, target=self) event_document_create.commit(
actor=user, target=self, action_object=self.document_type
)
else: else:
event_document_create.commit(target=self) event_document_create.commit(
target=self, action_object=self.document_type
)
else: else:
if _commit_events: if _commit_events:
event_document_properties_edit.commit(actor=user, target=self) event_document_properties_edit.commit(actor=user, target=self)

View File

@@ -64,7 +64,7 @@ class DocumentEventsTestCase(GenericDocumentViewTestCase):
event = Action.objects.any(obj=self.document).first() event = Action.objects.any(obj=self.document).first()
self.assertEqual(event.verb, event_document_download.name) self.assertEqual(event.verb, event_document_download.id)
self.assertEqual(event.target, self.document) self.assertEqual(event.target, self.document)
self.assertEqual(event.actor, self.user) self.assertEqual(event.actor, self.user)
@@ -98,6 +98,6 @@ class DocumentEventsTestCase(GenericDocumentViewTestCase):
event = Action.objects.any(obj=self.document).first() event = Action.objects.any(obj=self.document).first()
self.assertEqual(event.verb, event_document_view.name) self.assertEqual(event.verb, event_document_view.id)
self.assertEqual(event.target, self.document) self.assertEqual(event.target, self.document)
self.assertEqual(event.actor, self.user) self.assertEqual(event.actor, self.user)

View File

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

View File

@@ -2,9 +2,19 @@ from __future__ import unicode_literals
from django.contrib import admin from django.contrib import admin
from .models import EventType from .models import EventSubscription, Notification, StoredEventType
@admin.register(EventType) @admin.register(EventSubscription)
class EventTypeAdmin(admin.ModelAdmin): class EventSubscriptionAdmin(admin.ModelAdmin):
list_display = ('user', 'stored_event_type')
@admin.register(StoredEventType)
class StoredEventTypeAdmin(admin.ModelAdmin):
readonly_fields = ('name', '__str__') readonly_fields = ('name', '__str__')
@admin.register(Notification)
class NotificationAdmin(admin.ModelAdmin):
list_display = ('user', 'action', 'read')

View File

@@ -10,9 +10,13 @@ from rest_framework import generics
from acls.models import AccessControlList from acls.models import AccessControlList
from rest_api.permissions import MayanPermission from rest_api.permissions import MayanPermission
from .classes import Event from .classes import EventType, EventTypeNamespace
from .models import Notification
from .permissions import permission_events_view from .permissions import permission_events_view
from .serializers import EventSerializer, EventTypeSerializer from .serializers import (
EventSerializer, EventTypeSerializer, EventTypeNamespaceSerializer,
NotificationSerializer
)
class APIObjectEventListView(generics.ListAPIView): class APIObjectEventListView(generics.ListAPIView):
@@ -46,13 +50,72 @@ class APIObjectEventListView(generics.ListAPIView):
return any_stream(obj) return any_stream(obj)
class APIEventTypeNamespaceDetailView(generics.RetrieveAPIView):
"""
Returns the details of an event type namespace.
"""
serializer_class = EventTypeNamespaceSerializer
def get_object(self):
try:
return EventTypeNamespace.get(name=self.kwargs['name'])
except KeyError:
raise Http404
class APIEventTypeNamespaceListView(generics.ListAPIView):
"""
Returns a list of all the available event type namespaces.
"""
serializer_class = EventTypeNamespaceSerializer
queryset = EventTypeNamespace.all()
def get_serializer_context(self):
return {
'format': self.format_kwarg,
'request': self.request,
'view': self
}
class APIEventTypeNamespaceEventTypeListView(generics.ListAPIView):
"""
Returns a list of all the available event types from a namespaces.
"""
serializer_class = EventTypeSerializer
def get_queryset(self):
try:
return EventTypeNamespace.get(
name=self.kwargs['name']
).get_event_types()
except KeyError:
raise Http404
def get_serializer_context(self):
return {
'format': self.format_kwarg,
'request': self.request,
'view': self
}
class APIEventTypeListView(generics.ListAPIView): class APIEventTypeListView(generics.ListAPIView):
""" """
Returns a list of all the available event types. Returns a list of all the available event types.
""" """
serializer_class = EventTypeSerializer serializer_class = EventTypeSerializer
queryset = sorted(Event.all(), key=lambda event: event.name) queryset = EventType.all()
def get_serializer_context(self):
return {
'format': self.format_kwarg,
'request': self.request,
'view': self
}
class APIEventListView(generics.ListAPIView): class APIEventListView(generics.ListAPIView):
@@ -64,3 +127,20 @@ class APIEventListView(generics.ListAPIView):
permission_classes = (MayanPermission,) permission_classes = (MayanPermission,)
queryset = Action.objects.all() queryset = Action.objects.all()
serializer_class = EventSerializer serializer_class = EventSerializer
def get_serializer_context(self):
return {
'format': self.format_kwarg,
'request': self.request,
'view': self
}
class APINotificationListView(generics.ListAPIView):
"""
Return a list of notifications for the current user.
"""
serializer_class = NotificationSerializer
def get_queryset(self):
return Notification.objects.filter(user=self.request.user)

View File

@@ -3,11 +3,19 @@ from __future__ import unicode_literals
from django.apps import apps from django.apps import apps
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common import MayanAppConfig, menu_tools from common import (
MayanAppConfig, menu_main, menu_object, menu_secondary, menu_tools,
menu_user
)
from common.widgets import two_state_template
from navigation import SourceColumn from navigation import SourceColumn
from rest_api.classes import APIEndPoint from rest_api.classes import APIEndPoint
from .links import link_events_list from .links import (
link_events_list, link_event_types_subscriptions_list,
link_notification_mark_read, link_notification_mark_read_all,
link_user_notifications_list,
)
from .licenses import * # NOQA from .licenses import * # NOQA
from .widgets import event_object_link, event_type_link from .widgets import event_object_link, event_type_link
@@ -28,6 +36,8 @@ class EventsApp(MayanAppConfig):
def ready(self): def ready(self):
super(EventsApp, self).ready() super(EventsApp, self).ready()
Action = apps.get_model(app_label='actstream', model_name='Action') Action = apps.get_model(app_label='actstream', model_name='Action')
Notification = self.get_model(model_name='Notification')
StoredEventType = self.get_model(model_name='StoredEventType')
APIEndPoint(app=self, version_string='1') APIEndPoint(app=self, version_string='1')
@@ -39,7 +49,7 @@ class EventsApp(MayanAppConfig):
func=lambda context: event_actor(context['object']) func=lambda context: event_actor(context['object'])
) )
SourceColumn( SourceColumn(
source=Action, label=_('Verb'), source=Action, label=_('Event'),
func=lambda context: event_type_link(context['object']) func=lambda context: event_type_link(context['object'])
) )
SourceColumn( SourceColumn(
@@ -49,4 +59,44 @@ class EventsApp(MayanAppConfig):
) )
) )
SourceColumn(
source=StoredEventType, label=_('Namespace'), attribute='namespace'
)
SourceColumn(
source=StoredEventType, label=_('Label'), attribute='label'
)
SourceColumn(
source=Notification, label=_('Timestamp'),
attribute='action.timestamp'
)
SourceColumn(
source=Notification, label=_('Actor'), attribute='action.actor'
)
SourceColumn(
source=Notification, label=_('Event'),
func=lambda context: event_type_link(context['object'].action)
)
SourceColumn(
source=Notification, label=_('Target'),
func=lambda context: event_object_link(context['object'].action)
)
SourceColumn(
source=Notification, label=_('Seen'),
func=lambda context: two_state_template(
state=context['object'].read
)
)
menu_main.bind_links(
links=(link_user_notifications_list,), position=99
)
menu_object.bind_links(
links=(link_notification_mark_read,), sources=(Notification,)
)
menu_secondary.bind_links(
links=(link_notification_mark_read_all,),
sources=('events:user_notifications_list',)
)
menu_tools.bind_links(links=(link_events_list,)) menu_tools.bind_links(links=(link_events_list,))
menu_user.bind_links(links=(link_event_types_subscriptions_list,))

View File

@@ -1,70 +1,231 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import logging
from django.apps import apps from django.apps import apps
from django.utils.encoding import force_text from django.contrib.auth import get_user_model
from django.core.exceptions import PermissionDenied
from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from actstream import action from actstream import action
from .permissions import permission_events_view
class Event(object): logger = logging.getLogger(__name__)
@python_2_unicode_compatible
class EventTypeNamespace(object):
_registry = {} _registry = {}
@classmethod @classmethod
def all(cls): def all(cls):
return Event.sort(event_type_list=cls._registry.values()) return sorted(cls._registry.values())
@classmethod
def get(cls, name):
return cls._registry[name]
def __init__(self, name, label):
self.name = name
self.label = label
self.event_types = []
self.__class__._registry[name] = self
def __str__(self):
return force_text(self.label)
def add_event_type(self, name, label):
event_type = EventType(namespace=self, name=name, label=label)
self.event_types.append(event_type)
return event_type
def get_event_types(self):
return EventType.sort(event_type_list=self.event_types)
@python_2_unicode_compatible
class EventType(object):
_registry = {}
@staticmethod
def sort(event_type_list):
return sorted(
event_type_list, key=lambda x: (x.namespace.label, x.label)
)
@classmethod
def all(cls):
# Return sorted permisions by namespace.name
return EventType.sort(event_type_list=cls._registry.values())
@classmethod @classmethod
def get(cls, name): def get(cls, name):
try: try:
return cls._registry[name] return cls._registry[name]
except KeyError: except KeyError:
raise KeyError( return _('Unknown or obsolete event type: %s') % name
_('Unknown or obsolete event type: {0}'.format(name))
)
@classmethod
def get_label(cls, name):
try:
return cls.get(name=name).label
except KeyError as exception:
return force_text(exception)
@classmethod @classmethod
def refresh(cls): def refresh(cls):
for event_type in cls.all(): for event_type in cls.all():
event_type.get_type() event_type.get_stored_event_type()
@staticmethod def __init__(self, namespace, name, label):
def sort(event_type_list): self.namespace = namespace
return sorted(
event_type_list, key=lambda x: x.label
)
def __init__(self, name, label):
self.name = name self.name = name
self.label = label self.label = label
self.event_type = None self.stored_event_type = None
self.__class__._registry[name] = self self.__class__._registry[self.id] = self
def get_type(self): def __str__(self):
if not self.event_type: return force_text('{}: {}'.format(self.namespace.label, self.label))
EventType = apps.get_model('events', 'EventType')
self.event_type, created = EventType.objects.get_or_create(
name=self.name
)
return self.event_type
def commit(self, actor=None, action_object=None, target=None): def commit(self, actor=None, action_object=None, target=None):
if not self.event_type: AccessControlList = apps.get_model(
EventType = apps.get_model('events', 'EventType') app_label='acls', model_name='AccessControlList'
self.event_type, created = EventType.objects.get_or_create( )
name=self.name Action = apps.get_model(
) app_label='actstream', model_name='Action'
)
ContentType = apps.get_model(
app_label='contenttypes', model_name='ContentType'
)
Notification = apps.get_model(
app_label='events', model_name='Notification'
)
action.send( results = action.send(
actor or target, actor=actor, verb=self.name, actor or target, actor=actor, verb=self.id,
action_object=action_object, target=target action_object=action_object, target=target
) )
for handler, result in results:
if isinstance(result, Action):
for user in get_user_model().objects.all():
notification = None
if user.event_subscriptions.filter(stored_event_type__name=result.verb).exists():
if result.target:
try:
AccessControlList.objects.check_access(
permissions=permission_events_view,
user=user, obj=result.target
)
except PermissionDenied:
pass
else:
notification = Notification.objects.create(action=result, user=user)
else:
notification = Notification.objects.create(action=result, user=user)
if result.target:
content_type = ContentType.objects.get_for_model(model=result.target)
relationship = user.object_subscriptions.filter(
content_type=content_type,
object_id=result.target.pk,
stored_event_type__name=result.verb
)
if relationship.exists():
try:
AccessControlList.objects.check_access(
permissions=permission_events_view,
user=user, obj=result.target
)
except PermissionDenied:
pass
else:
notification = Notification.objects.create(action=result, user=user)
if not notification and result.action_object:
content_type = ContentType.objects.get_for_model(model=result.action_object)
relationship = user.object_subscriptions.filter(
content_type=content_type,
object_id=result.action_object.pk,
stored_event_type__name=result.verb
)
if relationship.exists():
try:
AccessControlList.objects.check_access(
permissions=permission_events_view,
user=user, obj=result.action_object
)
except PermissionDenied:
pass
else:
notification = Notification.objects.create(action=result, user=user)
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
@property
def id(self):
return '%s.%s' % (self.namespace.name, self.name)
class ModelEventType(object):
"""
Class to allow matching a model to a specific set of events.
"""
_inheritances = {}
_proxies = {}
_registry = {}
@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(cls, model, event_types):
cls._registry.setdefault(model, [])
for event_type in event_types:
cls._registry[model].append(event_type)
@classmethod
def register_inheritance(cls, model, related):
cls._inheritances[model] = related
@classmethod
def register_proxy(cls, source, model):
cls._proxies[model] = source

122
mayan/apps/events/forms.py Normal file
View File

@@ -0,0 +1,122 @@
from __future__ import unicode_literals
from django import forms
from django.forms.formsets import formset_factory
from django.utils.translation import ugettext_lazy as _
from .models import EventSubscription, ObjectEventSubscription
class EventTypeUserRelationshipForm(forms.Form):
namespace = forms.CharField(
label=_('Namespace'), required=False,
widget=forms.TextInput(attrs={'readonly': 'readonly'})
)
label = forms.CharField(
label=_('Label'), required=False,
widget=forms.TextInput(attrs={'readonly': 'readonly'})
)
subscription = forms.ChoiceField(
label=_('Subscription'),
widget=forms.RadioSelect(), choices=(
('none', _('No')),
('subscribed', _('Subscribed')),
)
)
def __init__(self, *args, **kwargs):
super(EventTypeUserRelationshipForm, self).__init__(
*args, **kwargs
)
self.fields['namespace'].initial = self.initial['stored_event_type'].namespace
self.fields['label'].initial = self.initial['stored_event_type'].label
subscription = EventSubscription.objects.get_for(
stored_event_type=self.initial['stored_event_type'],
user=self.initial['user'],
)
if subscription.exists():
self.fields['subscription'].initial = 'subscribed'
else:
self.fields['subscription'].initial = 'none'
def save(self):
subscription = EventSubscription.objects.get_for(
stored_event_type=self.initial['stored_event_type'],
user=self.initial['user'],
)
if self.cleaned_data['subscription'] == 'none':
subscription.delete()
elif self.cleaned_data['subscription'] == 'subscribed':
if not subscription.exists():
EventSubscription.objects.create_for(
stored_event_type=self.initial['stored_event_type'],
user=self.initial['user']
)
EventTypeUserRelationshipFormSet = formset_factory(
EventTypeUserRelationshipForm, extra=0
)
class ObjectEventTypeUserRelationshipForm(forms.Form):
namespace = forms.CharField(
label=_('Namespace'), required=False,
widget=forms.TextInput(attrs={'readonly': 'readonly'})
)
label = forms.CharField(
label=_('Label'), required=False,
widget=forms.TextInput(attrs={'readonly': 'readonly'})
)
subscription = forms.ChoiceField(
label=_('Subscription'),
widget=forms.RadioSelect(), choices=(
('none', _('No')),
('subscribed', _('Subscribed')),
)
)
def __init__(self, *args, **kwargs):
super(ObjectEventTypeUserRelationshipForm, self).__init__(
*args, **kwargs
)
self.fields['namespace'].initial = self.initial['stored_event_type'].namespace
self.fields['label'].initial = self.initial['stored_event_type'].label
subscription = ObjectEventSubscription.objects.get_for(
obj=self.initial['object'],
stored_event_type=self.initial['stored_event_type'],
user=self.initial['user'],
)
if subscription.exists():
self.fields['subscription'].initial = 'subscribed'
else:
self.fields['subscription'].initial = 'none'
def save(self):
subscription = ObjectEventSubscription.objects.get_for(
obj=self.initial['object'],
stored_event_type=self.initial['stored_event_type'],
user=self.initial['user'],
)
if self.cleaned_data['subscription'] == 'none':
subscription.delete()
elif self.cleaned_data['subscription'] == 'subscribed':
if not subscription.exists():
ObjectEventSubscription.objects.create_for(
obj=self.initial['object'],
stored_event_type=self.initial['stored_event_type'],
user=self.initial['user']
)
ObjectEventTypeUserRelationshipFormSet = formset_factory(
ObjectEventTypeUserRelationshipForm, extra=0
)

View File

@@ -26,12 +26,44 @@ def get_kwargs_factory(variable_name):
return get_kwargs return get_kwargs
def get_notification_count(context):
return context['request'].user.notifications.filter(read=False).count()
link_events_list = Link( link_events_list = Link(
icon='fa fa-list-ol', permissions=(permission_events_view,), icon='fa fa-list-ol', permissions=(permission_events_view,),
text=_('Events'), view='events:events_list' text=_('Events'), view='events:events_list'
) )
link_events_details = Link(
text=_('Events'), view='events:events_list'
)
link_events_for_object = Link( link_events_for_object = Link(
icon='fa fa-list-ol', permissions=(permission_events_view,), icon='fa fa-list-ol', permissions=(permission_events_view,),
text=_('Events'), view='events:events_for_object', text=_('Events'), view='events:events_for_object',
kwargs=get_kwargs_factory('resolved_object') kwargs=get_kwargs_factory('resolved_object')
) )
link_event_types_subscriptions_list = Link(
icon='fa fa-list-ol', text=_('Event subscriptions'),
view='events:event_types_user_subcriptions_list'
)
link_notification_mark_read = Link(
args='object.pk', text=_('Mark as seen'),
view='events:notification_mark_read'
)
link_notification_mark_read_all = Link(
text=_('Mark all as seen'), view='events:notification_mark_read_all'
)
link_object_event_types_user_subcriptions_list = Link(
kwargs=get_kwargs_factory('resolved_object'),
permissions=(permission_events_view,), text=_('Subscriptions'),
view='events:object_event_types_user_subcriptions_list',
)
link_object_event_types_user_subcriptions_list_with_icon = Link(
kwargs=get_kwargs_factory('resolved_object'), icon='fa fa-rss',
permissions=(permission_events_view,), text=_('Subscriptions'),
view='events:object_event_types_user_subcriptions_list',
)
link_user_notifications_list = Link(
icon='fa fa-bell', text=get_notification_count,
view='events:user_notifications_list'
)

View File

@@ -0,0 +1,34 @@
from __future__ import unicode_literals
from django.contrib.contenttypes.models import ContentType
from django.db import models
class EventSubscriptionManager(models.Manager):
def create_for(self, stored_event_type, user):
return self.create(
stored_event_type=stored_event_type, user=user
)
def get_for(self, stored_event_type, user):
return self.filter(
stored_event_type=stored_event_type, user=user
)
class ObjectEventSubscriptionManager(models.Manager):
def create_for(self, obj, stored_event_type, user):
content_type = ContentType.objects.get_for_model(model=obj)
return self.create(
content_type=content_type, object_id=obj.pk,
stored_event_type=stored_event_type, user=user
)
def get_for(self, obj, stored_event_type, user):
content_type = ContentType.objects.get_for_model(model=obj)
return self.filter(
content_type=content_type, object_id=obj.pk,
stored_event_type=stored_event_type, user=user
)

View File

@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-07-29 07:04
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('events', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='EventSubscription',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('event_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='subscriptions', to='events.EventType', verbose_name='Event type')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'Event subscription',
'verbose_name_plural': 'Event subscriptions',
},
),
]

View File

@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-07-29 07:23
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('actstream', '0002_remove_action_data'),
('events', '0002_eventsubscription'),
]
operations = [
migrations.CreateModel(
name='Notification',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('read', models.BooleanField(default=False, verbose_name='Read')),
('action', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to='actstream.Action', verbose_name='Action')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'Notification',
'verbose_name_plural': 'Notifications',
},
),
]

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-07-31 04:23
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('events', '0003_notification'),
]
operations = [
migrations.RenameModel(
old_name='EventType',
new_name='StoredEventType',
),
migrations.AlterModelOptions(
name='storedeventtype',
options={'verbose_name': 'Stored event type', 'verbose_name_plural': 'Stored event types'},
),
migrations.RemoveField(
model_name='eventsubscription',
name='event_type',
),
migrations.AddField(
model_name='eventsubscription',
name='stored_event_type',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='event_subscriptions', to='events.StoredEventType', verbose_name='Event type'),
preserve_default=False,
),
migrations.AlterField(
model_name='eventsubscription',
name='user',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='event_subscriptions', to=settings.AUTH_USER_MODEL, verbose_name='User'),
),
]

View File

@@ -0,0 +1,86 @@
# -*- 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.',
'parsing_document_': 'document_parsing.',
'ocr_': 'ocr.',
'tag_': 'tags.',
}
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()
def revert_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_comments\.': 'document_comment_',
'document_parsing\.': 'parsing_document_',
'ocr\.': 'ocr_',
'tags\.': 'tag_',
}
pattern = re.compile('|'.join(known_namespaces.keys()))
for event_type in StoredEventType.objects.all():
old_name = event_type.name
new_name = pattern.sub(
lambda x: known_namespaces[x.group().replace('.', '\\.')],
event_type.name
)
event_type.name = new_name
if old_name == new_name:
event_type.delete()
else:
event_type.save()
for action in Action.objects.all():
new_name = pattern.sub(
lambda x: known_namespaces[x.group().replace('.', '\\.')],
action.verb
)
action.verb = new_name
action.save()
class Migration(migrations.Migration):
dependencies = [
('events', '0004_auto_20170731_0423'),
('actstream', '0001_initial'),
]
operations = [
migrations.RunPython(
code=update_event_types_names,
reverse_code=revert_event_types_names
),
]

View File

@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-07-31 06:40
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('events', '0005_auto_20170731_0452'),
]
operations = [
migrations.CreateModel(
name='ObjectEventSubscription',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('object_id', models.PositiveIntegerField()),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
('stored_event_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='object_subscriptions', to='events.StoredEventType', verbose_name='Event type')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='object_subscriptions', to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'Object event subscription',
'verbose_name_plural': 'Object event subscriptions',
},
),
]

View File

@@ -1,28 +1,111 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.utils.encoding import python_2_unicode_compatible from django.utils.encoding import force_text, python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from .classes import Event from actstream.models import Action
from .classes import EventType
from .managers import (
EventSubscriptionManager, ObjectEventSubscriptionManager
)
@python_2_unicode_compatible @python_2_unicode_compatible
class EventType(models.Model): class StoredEventType(models.Model):
name = models.CharField( name = models.CharField(
max_length=64, unique=True, verbose_name=_('Name') max_length=64, unique=True, verbose_name=_('Name')
) )
class Meta: class Meta:
verbose_name = _('Event type') verbose_name = _('Stored event type')
verbose_name_plural = _('Event types') verbose_name_plural = _('Stored event types')
def __str__(self): def __str__(self):
return self.get_class().label return force_text(self.get_class())
def get_class(self): def get_class(self):
return Event.get(name=self.name) return EventType.get(name=self.name)
@property @property
def label(self): def label(self):
return self.get_class().label return self.get_class().label
@property
def namespace(self):
return self.get_class().namespace
@python_2_unicode_compatible
class EventSubscription(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL, db_index=True, on_delete=models.CASCADE,
related_name='event_subscriptions', verbose_name=_('User')
)
stored_event_type = models.ForeignKey(
StoredEventType, on_delete=models.CASCADE,
related_name='event_subscriptions', verbose_name=_('Event type')
)
objects = EventSubscriptionManager()
class Meta:
verbose_name = _('Event subscription')
verbose_name_plural = _('Event subscriptions')
def __str__(self):
return force_text(self.stored_event_type)
@python_2_unicode_compatible
class Notification(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL, db_index=True, on_delete=models.CASCADE,
related_name='notifications', verbose_name=_('User')
)
action = models.ForeignKey(
Action, on_delete=models.CASCADE, related_name='notifications',
verbose_name=_('Action')
)
read = models.BooleanField(default=False, verbose_name=_('Read'))
class Meta:
ordering = ('-action__timestamp',)
verbose_name = _('Notification')
verbose_name_plural = _('Notifications')
def __str__(self):
return force_text(self.action)
@python_2_unicode_compatible
class ObjectEventSubscription(models.Model):
content_type = models.ForeignKey(
ContentType, on_delete=models.CASCADE,
)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey(
ct_field='content_type',
fk_field='object_id',
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL, db_index=True, on_delete=models.CASCADE,
related_name='object_subscriptions', verbose_name=_('User')
)
stored_event_type = models.ForeignKey(
StoredEventType, on_delete=models.CASCADE,
related_name='object_subscriptions', verbose_name=_('Event type')
)
objects = ObjectEventSubscriptionManager()
class Meta:
verbose_name = _('Object event subscription')
verbose_name_plural = _('Object event subscriptions')
def __str__(self):
return force_text(self.stored_event_type)

View File

@@ -4,30 +4,59 @@ from django.utils.six import string_types
from actstream.models import Action from actstream.models import Action
from rest_framework import serializers from rest_framework import serializers
from rest_framework.reverse import reverse
from common.serializers import ContentTypeSerializer from common.serializers import ContentTypeSerializer
from rest_api.fields import DynamicSerializerField from rest_api.fields import DynamicSerializerField
from user_management.serializers import UserSerializer
from .classes import Event from .classes import EventType
from .models import EventType from .models import Notification, StoredEventType
class EventTypeNamespaceSerializer(serializers.Serializer):
label = serializers.CharField()
name = serializers.CharField()
url = serializers.SerializerMethodField()
event_types_url = serializers.HyperlinkedIdentityField(
lookup_field='name',
view_name='rest_api:event-type-namespace-event-type-list',
)
def get_url(self, instance):
return reverse(
'rest_api:event-type-namespace-detail', args=(
instance.name,
), request=self.context['request'], format=self.context['format']
)
class EventTypeSerializer(serializers.Serializer): class EventTypeSerializer(serializers.Serializer):
label = serializers.CharField() label = serializers.CharField()
name = serializers.CharField() name = serializers.CharField()
id = serializers.CharField()
event_type_namespace_url = serializers.SerializerMethodField()
def get_event_type_namespace_url(self, instance):
return reverse(
'rest_api:event-type-namespace-detail', args=(
instance.namespace.name,
), request=self.context['request'], format=self.context['format']
)
def to_representation(self, instance): def to_representation(self, instance):
if isinstance(instance, Event): if isinstance(instance, EventType):
return super(EventTypeSerializer, self).to_representation( return super(EventTypeSerializer, self).to_representation(
instance instance
) )
elif isinstance(instance, EventType): elif isinstance(instance, StoredEventType):
return super(EventTypeSerializer, self).to_representation( return super(EventTypeSerializer, self).to_representation(
instance.get_class() instance.get_class()
) )
elif isinstance(instance, string_types): elif isinstance(instance, string_types):
return super(EventTypeSerializer, self).to_representation( return super(EventTypeSerializer, self).to_representation(
Event.get(name=instance) EventType.get(name=instance)
) )
@@ -43,3 +72,12 @@ class EventSerializer(serializers.ModelSerializer):
'action_object_content_type', 'action_object_object_id' 'action_object_content_type', 'action_object_object_id'
) )
model = Action model = Action
class NotificationSerializer(serializers.ModelSerializer):
user = UserSerializer(read_only=True)
action = EventSerializer(read_only=True)
class Meta:
fields = ('action', 'read', 'user')
model = Notification

View File

@@ -3,9 +3,15 @@ from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
from .api_views import ( from .api_views import (
APIEventListView, APIEventTypeListView, APIObjectEventListView APIEventListView, APIEventTypeListView, APIEventTypeNamespaceDetailView,
APIEventTypeNamespaceEventTypeListView, APIEventTypeNamespaceListView,
APINotificationListView, APIObjectEventListView
)
from .views import (
EventListView, EventTypeSubscriptionListView, NotificationListView,
NotificationMarkRead, NotificationMarkReadAll, ObjectEventListView,
ObjectEventTypeSubscriptionListView, VerbEventListView
) )
from .views import EventListView, ObjectEventListView, VerbEventListView
urlpatterns = [ urlpatterns = [
url(r'^all/$', EventListView.as_view(), name='events_list'), url(r'^all/$', EventListView.as_view(), name='events_list'),
@@ -14,16 +20,60 @@ urlpatterns = [
ObjectEventListView.as_view(), name='events_for_object' ObjectEventListView.as_view(), name='events_for_object'
), ),
url( url(
r'^by_verb/(?P<verb>[\w\-]+)/$', VerbEventListView.as_view(), r'^by_verb/(?P<verb>[\w\-\.]+)/$', VerbEventListView.as_view(),
name='events_by_verb' name='events_by_verb'
), ),
url(
r'^notifications/(?P<pk>\d+)/mark_read/$',
NotificationMarkRead.as_view(), name='notification_mark_read'
),
url(
r'^notifications/all/mark_read/$',
NotificationMarkReadAll.as_view(), name='notification_mark_read_all'
),
url(
r'^user/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/subscriptions/$',
ObjectEventTypeSubscriptionListView.as_view(),
name='object_event_types_user_subcriptions_list'
),
url(
r'^user/event_types/subscriptions/$',
EventTypeSubscriptionListView.as_view(),
name='event_types_user_subcriptions_list'
),
url(
r'^user/notifications/$',
NotificationListView.as_view(),
name='user_notifications_list'
),
] ]
api_urls = [ api_urls = [
url(r'^types/$', APIEventTypeListView.as_view(), name='event-type-list'), url(
r'^event_type_namespaces/(?P<name>[-\w]+)/$',
APIEventTypeNamespaceDetailView.as_view(),
name='event-type-namespace-detail'
),
url(
r'^event_type_namespaces/(?P<name>[-\w]+)/event_types/$',
APIEventTypeNamespaceEventTypeListView.as_view(),
name='event-type-namespace-event-type-list'
),
url(
r'^event_type_namespaces/$', APIEventTypeNamespaceListView.as_view(),
name='event-type-namespace-list'
),
url(
r'^event_types/$', APIEventTypeListView.as_view(),
name='event-type-list'
),
url(r'^events/$', APIEventListView.as_view(), name='event-list'), url(r'^events/$', APIEventListView.as_view(), name='event-list'),
url( url(
r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/events/$', r'^notifications/$', APINotificationListView.as_view(),
name='notification-list'
),
url(
r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/events/$',
APIObjectEventListView.as_view(), name='object-event-list' APIObjectEventListView.as_view(), name='object-event-list'
), ),
] ]

View File

@@ -1,17 +1,24 @@
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
from django.contrib import messages
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.http import Http404 from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from actstream.models import Action, any_stream from actstream.models import Action, any_stream
from acls.models import AccessControlList from acls.models import AccessControlList
from common.generics import FormView, SimpleView
from common.utils import encapsulate from common.utils import encapsulate
from common.views import SingleObjectListView from common.views import SingleObjectListView
from .classes import Event from .classes import EventType, ModelEventType
from .forms import (
EventTypeUserRelationshipFormSet, ObjectEventTypeUserRelationshipFormSet
)
from .models import StoredEventType
from .permissions import permission_events_view from .permissions import permission_events_view
from .widgets import event_object_link from .widgets import event_object_link
@@ -37,6 +44,96 @@ class EventListView(SingleObjectListView):
return Action.objects.all() return Action.objects.all()
class EventTypeSubscriptionListView(FormView):
form_class = EventTypeUserRelationshipFormSet
main_model = 'user'
submodel = StoredEventType
def dispatch(self, *args, **kwargs):
EventType.refresh()
return super(EventTypeSubscriptionListView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
try:
for instance in form:
instance.save()
except Exception as exception:
messages.error(
self.request,
_('Error updating event subscription; %s') % exception
)
else:
messages.success(
self.request, _('Event subscriptions updated successfully')
)
return super(
EventTypeSubscriptionListView, self
).form_valid(form=form)
def get_object(self):
return self.request.user
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_queryset(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_queryset(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_queryset(self):
return self.request.user.notifications.all()
class ObjectEventListView(EventListView): class ObjectEventListView(EventListView):
view_permissions = None view_permissions = None
@@ -73,6 +170,76 @@ class ObjectEventListView(EventListView):
return any_stream(self.content_object) return any_stream(self.content_object)
class ObjectEventTypeSubscriptionListView(FormView):
form_class = ObjectEventTypeUserRelationshipFormSet
def dispatch(self, *args, **kwargs):
EventType.refresh()
return super(ObjectEventTypeSubscriptionListView, self).dispatch(*args, **kwargs)
def form_valid(self, form):
try:
for instance in form:
instance.save()
except Exception as exception:
messages.error(
self.request,
_('Error updating object event subscription; %s') % exception
)
else:
messages.success(
self.request, _('Object event subscriptions updated successfully')
)
return super(
ObjectEventTypeSubscriptionListView, self
).form_valid(form=form)
def get_object(self):
object_content_type = get_object_or_404(
ContentType, app_label=self.kwargs['app_label'],
model=self.kwargs['model']
)
try:
content_object = object_content_type.get_object_for_this_type(
pk=self.kwargs['object_id']
)
except object_content_type.model_class().DoesNotExist:
raise Http404
AccessControlList.objects.check_access(
permissions=permission_events_view, user=self.request.user,
obj=content_object
)
return content_object
def get_extra_context(self):
return {
'form_display_mode_table': True,
'object': self.get_object(),
'title': _(
'Event subscriptions for: %s'
) % self.get_object()
}
def get_initial(self):
obj = self.get_object()
initial = []
for element in self.get_queryset():
initial.append({
'user': self.request.user,
'object': obj,
'stored_event_type': element,
})
return initial
def get_queryset(self):
return ModelEventType.get_for_instance(instance=self.get_object())
class VerbEventListView(SingleObjectListView): class VerbEventListView(SingleObjectListView):
def get_extra_context(self): def get_extra_context(self):
return { return {
@@ -87,7 +254,7 @@ class VerbEventListView(SingleObjectListView):
'hide_object': True, 'hide_object': True,
'title': _( 'title': _(
'Events of type: %s' 'Events of type: %s'
) % Event.get_label(self.kwargs['verb']), ) % EventType.get(name=self.kwargs['verb']),
} }
def get_object_list(self): def get_object_list(self):

View File

@@ -3,7 +3,7 @@ from __future__ import unicode_literals
from django.urls import reverse from django.urls import reverse
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from .classes import Event from .classes import EventType
def event_object_link(entry, attribute='target'): def event_object_link(entry, attribute='target'):
@@ -26,6 +26,6 @@ def event_type_link(entry):
return mark_safe( return mark_safe(
'<a href="%(url)s">%(label)s</a>' % { '<a href="%(url)s">%(label)s</a>' % {
'url': reverse('events:events_by_verb', kwargs={'verb': entry.verb}), 'url': reverse('events:events_by_verb', kwargs={'verb': entry.verb}),
'label': Event.get_label(entry.verb) 'label': EventType.get(name=entry.verb)
} }
) )

View File

@@ -2,13 +2,15 @@ from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from events.classes import Event from events import EventTypeNamespace
event_ocr_document_version_submit = Event( namespace = EventTypeNamespace(name='ocr', label=_('OCR'))
name='ocr_document_version_submit',
label=_('Document version submitted for OCR') event_ocr_document_version_submit = namespace.add_event_type(
label=_('Document version submitted for OCR'),
name='document_version_submit'
) )
event_ocr_document_version_finish = Event( event_ocr_document_version_finish = namespace.add_event_type(
name='ocr_document_version_finish', label=_('Document version OCR finished'),
label=_('Document version OCR finished') name='document_version_finish'
) )

View File

@@ -24,7 +24,10 @@ class DynamicSerializerField(serializers.ReadOnlyField):
for klass, serializer_class in self.serializers.items(): for klass, serializer_class in self.serializers.items():
if isinstance(value, klass): if isinstance(value, klass):
return serializer_class( return serializer_class(
context={'request': self.context['request']} context={
'format': self.context['format'],
'request': self.context['request']
}
).to_representation(instance=value) ).to_representation(instance=value)
return _('Unable to find serializer class for: %s') % value return _('Unable to find serializer class for: %s') % value

View File

@@ -124,7 +124,7 @@ class Source(models.Model):
logger.critical( logger.critical(
'Unexpected exception while trying to create version for ' 'Unexpected exception while trying to create version for '
'new document "%s" from source "%s"; %s', 'new document "%s" from source "%s"; %s',
label or file_object.name, self, exception label or file_object.name, self, exception, exc_info=True
) )
document.delete(to_trash=False) document.delete(to_trash=False)
raise raise

View File

@@ -2,13 +2,13 @@ from __future__ import absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from events.classes import Event from events import EventTypeNamespace
event_tag_attach = Event( namespace = EventTypeNamespace(name='tags', label=_('Tags'))
name='tag_attach',
label=_('Tag attached to document') event_tag_attach = namespace.add_event_type(
label=_('Tag attached to document'), name='attach'
) )
event_tag_remove = Event( event_tag_remove = namespace.add_event_type(
name='tag_remove', label=_('Tag removed from document'), name='remove'
label=_('Tag removed from document')
) )