Add 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 5083a2d261
commit c0407652c0
30 changed files with 1193 additions and 111 deletions

View File

@@ -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
)

View File

@@ -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,

View File

@@ -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')
)

View File

@@ -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()
)
)

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.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,

View File

@@ -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')
)

View File

@@ -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)

View File

@@ -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')
)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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'

View File

@@ -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')

View File

@@ -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)

View File

@@ -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,))

View File

@@ -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)
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

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

View File

@@ -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,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),
]

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,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)

View File

@@ -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

View File

@@ -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<verb>[\w\-]+)/$', VerbEventListView.as_view(),
r'^by_verb/(?P<verb>[\w\-\.]+)/$', VerbEventListView.as_view(),
name='events_by_verb'
),
url(
r'^notifications/(?P<pk>\d+)/mark_read/$',
NotificationMarkRead.as_view(), name='notification_mark_read'
),
url(
r'^notifications/all/mark_read/$',
NotificationMarkReadAll.as_view(), name='notification_mark_read_all'
),
url(
r'^user/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/subscriptions/$',
ObjectEventTypeSubscriptionListView.as_view(),
name='object_event_types_user_subcriptions_list'
),
url(
r'^user/event_types/subscriptions/$',
EventTypeSubscriptionListView.as_view(),
name='event_types_user_subcriptions_list'
),
url(
r'^user/notifications/$',
NotificationListView.as_view(),
name='user_notifications_list'
),
]
api_urls = [
url(r'^event_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'^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'

View File

@@ -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']),
}

View File

@@ -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('<a href="%(url)s">%(label)s</a>' % {
'url': reverse('events:events_by_verb', kwargs={'verb': entry.verb}),
'label': Event.get_label(entry.verb)}
'label': EventType.get(name=entry.verb)}
)

View File

@@ -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

View File

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