diff --git a/mayan/apps/authentication/apps.py b/mayan/apps/authentication/apps.py index aeb0ca7a3c..86b2643208 100644 --- a/mayan/apps/authentication/apps.py +++ b/mayan/apps/authentication/apps.py @@ -5,6 +5,7 @@ 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 @@ -21,6 +22,6 @@ class AuthenticationApp(MayanAppConfig): menu_user.bind_links( links=( - link_password_change, link_logout + Separator(), link_password_change, link_logout ), position=99 ) diff --git a/mayan/apps/checkouts/apps.py b/mayan/apps/checkouts/apps.py index cef537929a..84345032a5 100644 --- a/mayan/apps/checkouts/apps.py +++ b/mayan/apps/checkouts/apps.py @@ -12,9 +12,14 @@ from django.utils.translation import ugettext_lazy as _ from acls import ModelPermission from common import MayanAppConfig, menu_facet, menu_main, menu_sidebar from common.classes import DashboardWidget +from events import ModelEventType from mayan.celery import app from rest_api.classes import APIEndPoint +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, @@ -82,6 +87,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( model=Document, permissions=( permission_document_checkout, diff --git a/mayan/apps/checkouts/events.py b/mayan/apps/checkouts/events.py index 854759ada8..2ce427beea 100644 --- a/mayan/apps/checkouts/events.py +++ b/mayan/apps/checkouts/events.py @@ -2,19 +2,21 @@ from __future__ import absolute_import, unicode_literals from django.utils.translation import ugettext_lazy as _ -from events.classes import Event +from events import EventTypeNamespace -event_document_auto_check_in = Event( - name='checkouts_document_auto_check_in', +namespace = EventTypeNamespace(name='checkouts', label=_('Checkouts')) + +event_document_auto_check_in = namespace.add_event_type( + name='document_auto_check_in', label=_('Document automatically checked in') ) -event_document_check_in = Event( - name='checkouts_document_check_in', label=_('Document checked in') +event_document_check_in = namespace.add_event_type( + name='document_check_in', label=_('Document checked in') ) -event_document_check_out = Event( - name='checkouts_document_check_out', label=_('Document checked out') +event_document_check_out = namespace.add_event_type( + name='document_check_out', label=_('Document checked out') ) -event_document_forceful_check_in = Event( - name='checkouts_document_forceful_check_in', +event_document_forceful_check_in = namespace.add_event_type( + name='document_forceful_check_in', label=_('Document forcefully checked in') ) diff --git a/mayan/apps/common/apps.py b/mayan/apps/common/apps.py index f4c738bb98..6ffd253fe7 100644 --- a/mayan/apps/common/apps.py +++ b/mayan/apps/common/apps.py @@ -63,8 +63,8 @@ class MayanAppConfig(apps.AppConfig): except ImportError as exception: if force_text(exception) != 'No module named urls': logger.error( - 'Import time error when running AppConfig.ready(). Check ' - 'apps.py, urls.py, views.py, etc.' + 'Import time error when running AppConfig.ready() of app ' + '"%s".', self.name ) raise exception @@ -123,7 +123,6 @@ 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() ) ) diff --git a/mayan/apps/document_comments/apps.py b/mayan/apps/document_comments/apps.py index d79f3bd7b3..9c3fff8d3b 100644 --- a/mayan/apps/document_comments/apps.py +++ b/mayan/apps/document_comments/apps.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals +from __future__ import absolute_import, unicode_literals from django.apps import apps 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 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 ) @@ -36,6 +40,12 @@ 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, diff --git a/mayan/apps/document_comments/events.py b/mayan/apps/document_comments/events.py index 2a8eaf1860..1876854d43 100644 --- a/mayan/apps/document_comments/events.py +++ b/mayan/apps/document_comments/events.py @@ -2,13 +2,15 @@ from __future__ import absolute_import, unicode_literals from django.utils.translation import ugettext_lazy as _ -from events.classes import Event +from events import EventTypeNamespace -event_document_comment_create = Event( - name='document_comment_create', - label=_('Document comment created') +namespace = EventTypeNamespace( + name='document_comments', label=_('Document comments') ) -event_document_comment_delete = Event( - name='document_comment_delete', - label=_('Document comment deleted') + +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') ) diff --git a/mayan/apps/documents/apps.py b/mayan/apps/documents/apps.py index e90a1371fe..e7d258d64e 100644 --- a/mayan/apps/documents/apps.py +++ b/mayan/apps/documents/apps.py @@ -23,7 +23,11 @@ from converter.permissions import ( permission_transformation_delete, permission_transformation_edit, 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 mayan.celery import app from navigation import SourceColumn @@ -31,6 +35,12 @@ from rest_api.classes import APIEndPoint, APIResource from rest_api.fields import DynamicSerializerField from statistics.classes import StatisticNamespace, CharJSLine +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 ) @@ -171,6 +181,19 @@ 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, @@ -386,7 +409,8 @@ class DocumentsApp(MayanAppConfig): menu_object.bind_links( links=( 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,) ) menu_object.bind_links( @@ -443,8 +467,11 @@ class DocumentsApp(MayanAppConfig): links=(link_document_properties,), sources=(Document,), position=2 ) menu_facet.bind_links( - links=(link_events_for_object, link_document_version_list,), - sources=(Document,), position=2 + links=( + 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,)) @@ -548,3 +575,4 @@ class DocumentsApp(MayanAppConfig): registry.register(DeletedDocument) registry.register(Document) + registry.register(DocumentType) diff --git a/mayan/apps/documents/events.py b/mayan/apps/documents/events.py index c474b6fbbd..16a0bdc3bb 100644 --- a/mayan/apps/documents/events.py +++ b/mayan/apps/documents/events.py @@ -2,29 +2,28 @@ from __future__ import absolute_import, unicode_literals from django.utils.translation import ugettext_lazy as _ -from events.classes import Event +from events import EventTypeNamespace -event_document_create = Event( - name='documents_document_create', label=_('Document created') +namespace = EventTypeNamespace(name='documents', label=_('Documents')) + +event_document_create = namespace.add_event_type( + name='document_create', label=_('Document created') ) -event_document_download = Event( - name='documents_document_download', - label=_('Document downloaded') +event_document_download = namespace.add_event_type( + name='document_download', label=_('Document downloaded') ) -event_document_properties_edit = Event( - name='documents_document_edit', label=_('Document properties edited') +event_document_properties_edit = namespace.add_event_type( + name='document_edit', label=_('Document properties edited') ) -event_document_type_change = Event( - name='documents_document_type_change', label=_('Document type changed') +event_document_type_change = namespace.add_event_type( + name='document_type_change', label=_('Document type changed') ) -event_document_new_version = Event( - name='documents_document_new_version', label=_('New version uploaded') +event_document_new_version = namespace.add_event_type( + name='document_new_version', label=_('New version uploaded') ) -event_document_version_revert = Event( - name='documents_document_version_revert', - label=_('Document version reverted') +event_document_version_revert = namespace.add_event_type( + name='document_version_revert', label=_('Document version reverted') ) -event_document_view = Event( - name='documents_document_view', - label=_('Document viewed') +event_document_view = namespace.add_event_type( + name='document_view', label=_('Document viewed') ) diff --git a/mayan/apps/documents/models.py b/mayan/apps/documents/models.py index 217542fa54..cca6a18471 100644 --- a/mayan/apps/documents/models.py +++ b/mayan/apps/documents/models.py @@ -219,9 +219,13 @@ 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) + event_document_create.commit( + actor=user, target=self, action_object=self.document_type + ) else: - event_document_create.commit(target=self) + event_document_create.commit( + target=self, action_object=self.document_type + ) else: event_document_properties_edit.commit(actor=user, target=self) diff --git a/mayan/apps/documents/tests/test_events.py b/mayan/apps/documents/tests/test_events.py index 09e59364b5..65c9bcd4e1 100644 --- a/mayan/apps/documents/tests/test_events.py +++ b/mayan/apps/documents/tests/test_events.py @@ -64,7 +64,7 @@ class DocumentEventsTestCase(GenericDocumentViewTestCase): 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.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.name) + self.assertEqual(event.verb, event_document_view.id) self.assertEqual(event.target, self.document) self.assertEqual(event.actor, self.user) diff --git a/mayan/apps/events/__init__.py b/mayan/apps/events/__init__.py index a0227939b4..d4884e06cb 100644 --- a/mayan/apps/events/__init__.py +++ b/mayan/apps/events/__init__.py @@ -1,5 +1,5 @@ from __future__ import unicode_literals -from .classes import Event # NOQA +from .classes import EventTypeNamespace, ModelEventType # NOQA default_app_config = 'events.apps.EventsApp' diff --git a/mayan/apps/events/admin.py b/mayan/apps/events/admin.py index dfab0098f0..28bcd16b21 100644 --- a/mayan/apps/events/admin.py +++ b/mayan/apps/events/admin.py @@ -2,9 +2,19 @@ from __future__ import unicode_literals from django.contrib import admin -from .models import EventType +from .models import EventSubscription, Notification, StoredEventType -@admin.register(EventType) -class EventTypeAdmin(admin.ModelAdmin): +@admin.register(EventSubscription) +class EventSubscriptionAdmin(admin.ModelAdmin): + list_display = ('user', 'stored_event_type') + + +@admin.register(StoredEventType) +class StoredEventTypeAdmin(admin.ModelAdmin): readonly_fields = ('name', '__str__') + + +@admin.register(Notification) +class NotificationAdmin(admin.ModelAdmin): + list_display = ('user', 'action', 'read') diff --git a/mayan/apps/events/api_views.py b/mayan/apps/events/api_views.py index 35cc03c14c..227e813c9b 100644 --- a/mayan/apps/events/api_views.py +++ b/mayan/apps/events/api_views.py @@ -10,9 +10,13 @@ from rest_framework import generics from acls.models import AccessControlList 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 .serializers import EventSerializer, EventTypeSerializer +from .serializers import ( + EventSerializer, EventTypeSerializer, EventTypeNamespaceSerializer, + NotificationSerializer +) class APIObjectEventListView(generics.ListAPIView): @@ -46,13 +50,72 @@ 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 = 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): @@ -64,3 +127,20 @@ 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) diff --git a/mayan/apps/events/apps.py b/mayan/apps/events/apps.py index b866d0d459..e1fdbf3e84 100644 --- a/mayan/apps/events/apps.py +++ b/mayan/apps/events/apps.py @@ -3,13 +3,21 @@ from __future__ import unicode_literals from django.apps import apps 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 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 .widgets import event_type_link +from .widgets import event_object_link, event_type_link class EventsApp(MayanAppConfig): @@ -20,6 +28,8 @@ 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') @@ -28,8 +38,48 @@ class EventsApp(MayanAppConfig): ) SourceColumn(source=Action, label=_('Actor'), attribute='actor') SourceColumn( - source=Action, label=_('Verb'), + source=Action, label=_('Event'), func=lambda context: event_type_link(context['object']) ) + 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,)) diff --git a/mayan/apps/events/classes.py b/mayan/apps/events/classes.py index c54e7aa0ca..fa5fdd26b4 100644 --- a/mayan/apps/events/classes.py +++ b/mayan/apps/events/classes.py @@ -1,13 +1,21 @@ from __future__ import unicode_literals +import logging + from django.apps import apps -from django.utils.encoding import force_text -from django.utils.translation import ugettext_lazy as _ +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 actstream import action +from .permissions import permission_events_view -class Event(object): +logger = logging.getLogger(__name__) + + +@python_2_unicode_compatible +class EventTypeNamespace(object): _registry = {} @classmethod @@ -16,44 +24,209 @@ class Event(object): @classmethod def get(cls, name): - try: - return cls._registry[name] - except KeyError: - raise KeyError( - _('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) + return cls._registry[name] def __init__(self, name, label): self.name = name self.label = label - self.event_type = None + self.event_types = [] self.__class__._registry[name] = self - def get_type(self): - if not self.event_type: - EventType = apps.get_model('events', 'EventType') + def __str__(self): + return force_text(self.label) - self.event_type, created = EventType.objects.get_or_create( - name=self.name + def add_event_type(self, name, label): + event_type = EventType(namespace=self, name=name, label=label) + self.event_types.append(event_type) + return event_type + + def get_event_types(self): + return EventType.sort(event_type_list=self.event_types) + + +@python_2_unicode_compatible +class EventType(object): + _registry = {} + + @classmethod + def all(cls): + # Return sorted permisions by namespace.name + return EventType.sort(event_type_list=cls._registry.values()) + + @classmethod + def get(cls, name): + try: + return cls._registry[name] + except KeyError: + raise KeyError( + 'Unknown or obsolete event type: {0}'.format(name) ) - return self.event_type + 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 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 + + @staticmethod + def sort(event_type_list): + return sorted( + event_type_list, key=lambda x: (x.namespace.label, x.label) + ) def commit(self, actor=None, action_object=None, target=None): - if not self.event_type: - EventType = apps.get_model('events', 'EventType') - self.event_type, created = EventType.objects.get_or_create( - name=self.name - ) + 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' + ) - action.send( - actor or target, actor=actor, verb=self.name, + results = action.send( + actor or target, actor=actor, verb=self.id, 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 diff --git a/mayan/apps/events/forms.py b/mayan/apps/events/forms.py new file mode 100644 index 0000000000..a9617c909e --- /dev/null +++ b/mayan/apps/events/forms.py @@ -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 +) diff --git a/mayan/apps/events/links.py b/mayan/apps/events/links.py index b8ea83d25c..63bc728cf8 100644 --- a/mayan/apps/events/links.py +++ b/mayan/apps/events/links.py @@ -26,12 +26,44 @@ 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' +) diff --git a/mayan/apps/events/managers.py b/mayan/apps/events/managers.py new file mode 100644 index 0000000000..ebb2dd8739 --- /dev/null +++ b/mayan/apps/events/managers.py @@ -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 + ) diff --git a/mayan/apps/events/migrations/0002_eventsubscription.py b/mayan/apps/events/migrations/0002_eventsubscription.py new file mode 100644 index 0000000000..8c84935e1a --- /dev/null +++ b/mayan/apps/events/migrations/0002_eventsubscription.py @@ -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', + }, + ), + ] diff --git a/mayan/apps/events/migrations/0003_notification.py b/mayan/apps/events/migrations/0003_notification.py new file mode 100644 index 0000000000..343ac7b91c --- /dev/null +++ b/mayan/apps/events/migrations/0003_notification.py @@ -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', + }, + ), + ] diff --git a/mayan/apps/events/migrations/0004_auto_20170731_0423.py b/mayan/apps/events/migrations/0004_auto_20170731_0423.py new file mode 100644 index 0000000000..af9d7379b0 --- /dev/null +++ b/mayan/apps/events/migrations/0004_auto_20170731_0423.py @@ -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'), + ), + ] diff --git a/mayan/apps/events/migrations/0005_auto_20170731_0452.py b/mayan/apps/events/migrations/0005_auto_20170731_0452.py new file mode 100644 index 0000000000..1b71cce768 --- /dev/null +++ b/mayan/apps/events/migrations/0005_auto_20170731_0452.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-07-31 04:52 +from __future__ import unicode_literals + +import re + +from django.db import migrations + + +def update_event_types_names(apps, schema_editor): + Action = apps.get_model('actstream', 'Action') + StoredEventType = apps.get_model('events', 'StoredEventType') + + known_namespaces = { + 'documents_': 'documents.', + 'checkouts_': 'checkouts.', + 'document_comment_': 'document_comments.', + } + + pattern = re.compile('|'.join(known_namespaces.keys())) + + for event_type in StoredEventType.objects.all(): + event_type.name = pattern.sub( + lambda x: known_namespaces[x.group()], event_type.name + ) + event_type.save() + + for action in Action.objects.all(): + action.verb = pattern.sub( + lambda x: known_namespaces[x.group()], action.verb + ) + action.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('events', '0004_auto_20170731_0423'), + ('actstream', '0001_initial'), + ] + + operations = [ + migrations.RunPython(update_event_types_names), + ] diff --git a/mayan/apps/events/migrations/0006_objecteventsubscription.py b/mayan/apps/events/migrations/0006_objecteventsubscription.py new file mode 100644 index 0000000000..15094ce433 --- /dev/null +++ b/mayan/apps/events/migrations/0006_objecteventsubscription.py @@ -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', + }, + ), + ] diff --git a/mayan/apps/events/models.py b/mayan/apps/events/models.py index 659236cfe8..1a8ae597fe 100644 --- a/mayan/apps/events/models.py +++ b/mayan/apps/events/models.py @@ -1,24 +1,111 @@ 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 python_2_unicode_compatible +from django.utils.encoding import force_text, python_2_unicode_compatible 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 -class EventType(models.Model): +class StoredEventType(models.Model): name = models.CharField( max_length=64, unique=True, verbose_name=_('Name') ) class Meta: - verbose_name = _('Event type') - verbose_name_plural = _('Event types') + verbose_name = _('Stored event type') + verbose_name_plural = _('Stored event types') def __str__(self): - return self.get_class().label + return force_text(self.get_class()) def get_class(self): - return Event.get_label(self.name) + return EventType.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) diff --git a/mayan/apps/events/serializers.py b/mayan/apps/events/serializers.py index aa3926eb5e..3f2c7d8e20 100644 --- a/mayan/apps/events/serializers.py +++ b/mayan/apps/events/serializers.py @@ -4,30 +4,59 @@ 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 Event -from .models import EventType +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'] + ) 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, Event): + if isinstance(instance, EventType): return super(EventTypeSerializer, self).to_representation( instance ) - elif isinstance(instance, EventType): + elif isinstance(instance, StoredEventType): return super(EventTypeSerializer, self).to_representation( instance.get_class() ) elif isinstance(instance, string_types): 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' ) model = Action + + +class NotificationSerializer(serializers.ModelSerializer): + user = UserSerializer(read_only=True) + action = EventSerializer(read_only=True) + + class Meta: + fields = ('action', 'read', 'user') + model = Notification diff --git a/mayan/apps/events/urls.py b/mayan/apps/events/urls.py index f5fc5b8fbb..f0c96c8467 100644 --- a/mayan/apps/events/urls.py +++ b/mayan/apps/events/urls.py @@ -3,9 +3,15 @@ from __future__ import unicode_literals from django.conf.urls import url 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 = [ url(r'^all/$', EventListView.as_view(), name='events_list'), @@ -14,14 +20,58 @@ urlpatterns = [ ObjectEventListView.as_view(), name='events_for_object' ), url( - r'^by_verb/(?P[\w\-]+)/$', VerbEventListView.as_view(), + r'^by_verb/(?P[\w\-\.]+)/$', VerbEventListView.as_view(), name='events_by_verb' ), + url( + r'^notifications/(?P\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[-\w]+)/(?P[-\w]+)/(?P\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_types/$', APIEventTypeListView.as_view(), name='event-type-list'), + url( + r'^event_type_namespaces/(?P[-\w]+)/$', + APIEventTypeNamespaceDetailView.as_view(), + name='event-type-namespace-detail' + ), + url( + r'^event_type_namespaces/(?P[-\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'^notifications/$', APINotificationListView.as_view(), + name='notification-list' + ), url( r'^objects/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/events/$', APIObjectEventListView.as_view(), name='object-event-list' diff --git a/mayan/apps/events/views.py b/mayan/apps/events/views.py index 326ba6cb18..f577696315 100644 --- a/mayan/apps/events/views.py +++ b/mayan/apps/events/views.py @@ -1,17 +1,24 @@ from __future__ import absolute_import, unicode_literals +from django.contrib import messages 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.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 Event +from .classes import EventType, ModelEventType +from .forms import ( + EventTypeUserRelationshipFormSet, ObjectEventTypeUserRelationshipFormSet +) +from .models import StoredEventType from .permissions import permission_events_view from .widgets import event_object_link @@ -37,6 +44,96 @@ class EventListView(SingleObjectListView): } +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_post_action_redirect(self): + return reverse('common:current_user_details') + + 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) + + +class NotificationListView(SingleObjectListView): + def get_queryset(self): + return self.request.user.notifications.all() + + def get_extra_context(self): + return { + 'hide_object': True, + 'object': self.request.user, + 'title': _('Notifications'), + } + + +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): view_permissions = None @@ -73,6 +170,76 @@ 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_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): def get_queryset(self): return Action.objects.filter(verb=self.kwargs['verb']) @@ -90,5 +257,5 @@ class VerbEventListView(SingleObjectListView): 'hide_object': True, 'title': _( 'Events of type: %s' - ) % Event.get_label(self.kwargs['verb']), + ) % EventType.get(name=self.kwargs['verb']), } diff --git a/mayan/apps/events/widgets.py b/mayan/apps/events/widgets.py index e2cb52cf1c..bf78fa509e 100644 --- a/mayan/apps/events/widgets.py +++ b/mayan/apps/events/widgets.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals from django.urls import reverse from django.utils.safestring import mark_safe -from .classes import Event +from .classes import EventType def event_object_link(entry): @@ -16,5 +16,5 @@ def event_object_link(entry): def event_type_link(entry): return mark_safe('%(label)s' % { 'url': reverse('events:events_by_verb', kwargs={'verb': entry.verb}), - 'label': Event.get_label(entry.verb)} + 'label': EventType.get(name=entry.verb)} ) diff --git a/mayan/apps/rest_api/fields.py b/mayan/apps/rest_api/fields.py index df2a5853d0..537bf945ff 100644 --- a/mayan/apps/rest_api/fields.py +++ b/mayan/apps/rest_api/fields.py @@ -24,7 +24,10 @@ class DynamicSerializerField(serializers.ReadOnlyField): for klass, serializer_class in self.serializers.items(): if isinstance(value, klass): return serializer_class( - context={'request': self.context['request']} + context={ + 'format': self.context['format'], + 'request': self.context['request'] + } ).to_representation(instance=value) return _('Unable to find serializer class for: %s') % value diff --git a/mayan/apps/sources/models.py b/mayan/apps/sources/models.py index d8ce28ff5e..f58f7cbce3 100644 --- a/mayan/apps/sources/models.py +++ b/mayan/apps/sources/models.py @@ -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 + label or file_object.name, self, exception, exc_info=True ) document.delete(to_trash=False) raise