From ce0a6368f2d2c724c298d95efcefb10699863dfd Mon Sep 17 00:00:00 2001 From: Roberto Rosario Date: Mon, 22 Apr 2019 01:32:52 -0400 Subject: [PATCH] Add support for showing the current user's events Add support kwargs to the SourceColumn class. Improve the event widgets, views and tests. Signed-off-by: Roberto Rosario --- HISTORY.rst | 3 + docs/releases/3.2.rst | 3 + mayan/apps/events/apps.py | 48 +++++++------- mayan/apps/events/html_widgets.py | 82 +++++++++++++++++++++++ mayan/apps/events/icons.py | 3 +- mayan/apps/events/links.py | 14 ++-- mayan/apps/events/tests/literals.py | 6 ++ mayan/apps/events/tests/mixins.py | 20 ++++++ mayan/apps/events/tests/test_api.py | 64 +++++++++++++++++- mayan/apps/events/tests/test_views.py | 21 +++--- mayan/apps/events/urls.py | 15 +++-- mayan/apps/events/views.py | 96 +++++++-------------------- mayan/apps/events/widgets.py | 49 -------------- mayan/apps/navigation/classes.py | 12 ++-- 14 files changed, 259 insertions(+), 177 deletions(-) create mode 100644 mayan/apps/events/html_widgets.py create mode 100644 mayan/apps/events/tests/literals.py create mode 100644 mayan/apps/events/tests/mixins.py delete mode 100644 mayan/apps/events/widgets.py diff --git a/HISTORY.rst b/HISTORY.rst index 74004b3a43..d8b045d2bd 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -104,6 +104,9 @@ * Add group created and group edited events. * Add support for SourceColumn widgets. * Improve styling of the template debug view. +* Add support for showing the current user's events. +* Add support kwargs to the SourceColumn class. +* Improve the event widgets, views and tests. 3.1.11 (2019-04-XX) =================== diff --git a/docs/releases/3.2.rst b/docs/releases/3.2.rst index d9778bceba..a6402d28e0 100644 --- a/docs/releases/3.2.rst +++ b/docs/releases/3.2.rst @@ -136,6 +136,9 @@ Other changes * Add group created and group edited events. * Add support for SourceColumn widgets. * Improve styling of the template debug view. +* Add support for showing the current user's events. +* Add support kwargs to the SourceColumn class. +* Improve the event widgets, views and tests. Removals -------- diff --git a/mayan/apps/events/apps.py b/mayan/apps/events/apps.py index 169227ed2e..729650a443 100644 --- a/mayan/apps/events/apps.py +++ b/mayan/apps/events/apps.py @@ -1,7 +1,6 @@ from __future__ import unicode_literals from django.apps import apps -from django.contrib.auth import get_user_model from django.utils.translation import ugettext_lazy as _ from mayan.apps.common.apps import MayanAppConfig @@ -11,13 +10,15 @@ from mayan.apps.common.menus import ( from mayan.apps.common.widgets import TwoStateWidget from mayan.apps.navigation import SourceColumn +from .html_widgets import ( + ObjectLinkWidget, widget_event_actor_link, widget_event_type_link +) from .links import ( - link_events_list, link_event_types_subscriptions_list, - link_notification_mark_read, link_notification_mark_read_all, - link_user_events, link_user_notifications_list, + link_current_user_events, link_event_types_subscriptions_list, + link_events_list, link_notification_mark_read, + link_notification_mark_read_all, link_user_notifications_list, ) from .licenses import * # NOQA -from .widgets import event_object_link, event_type_link, event_user_link class EventsApp(MayanAppConfig): @@ -33,24 +34,23 @@ class EventsApp(MayanAppConfig): Action = apps.get_model(app_label='actstream', model_name='Action') Notification = self.get_model(model_name='Notification') StoredEventType = self.get_model(model_name='StoredEventType') - User = get_user_model() SourceColumn( source=Action, label=_('Timestamp'), attribute='timestamp' ) SourceColumn( - source=Action, label=_('Actor'), - func=lambda context: event_user_link(context['object']) + func=widget_event_actor_link, label=_('Actor'), source=Action ) SourceColumn( - source=Action, label=_('Event'), - func=lambda context: event_type_link(context['object']) + func=widget_event_type_link, label=_('Event'), source=Action ) SourceColumn( - source=Action, label=_('Action object'), - func=lambda context: event_object_link( - entry=context['object'], attribute='action_object' - ) + attribute='action_object', label=_('Action object'), source=Action, + widget=ObjectLinkWidget + ) + SourceColumn( + attribute='target', label=_('Target'), source=Action, + widget=ObjectLinkWidget ) SourceColumn( @@ -64,17 +64,20 @@ class EventsApp(MayanAppConfig): source=Notification, label=_('Timestamp'), attribute='action.timestamp' ) + SourceColumn( - source=Notification, label=_('Actor'), attribute='action.actor' + func=widget_event_actor_link, label=_('Actor'), + kwargs={'attribute': 'action'}, source=Notification ) SourceColumn( - source=Notification, label=_('Event'), - func=lambda context: event_type_link(context['object'].action) + func=widget_event_type_link, label=_('Event'), + kwargs={'attribute': 'action'}, source=Notification ) SourceColumn( - source=Notification, label=_('Target'), - func=lambda context: event_object_link(context['object'].action) + attribute='action.target', label=_('Target'), source=Notification, + widget=ObjectLinkWidget ) + SourceColumn( source=Notification, label=_('Seen'), func=lambda context: TwoStateWidget( @@ -88,14 +91,13 @@ class EventsApp(MayanAppConfig): menu_object.bind_links( links=(link_notification_mark_read,), sources=(Notification,) ) - menu_object.bind_links( - links=(link_user_events,), sources=(User,) - ) 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,), position=50 + links=( + link_event_types_subscriptions_list, link_current_user_events + ), position=50 ) diff --git a/mayan/apps/events/html_widgets.py b/mayan/apps/events/html_widgets.py new file mode 100644 index 0000000000..3ce2babb92 --- /dev/null +++ b/mayan/apps/events/html_widgets.py @@ -0,0 +1,82 @@ +from __future__ import unicode_literals + +from django.apps import apps +from django.template import Context, Template +from django.urls import reverse +from django.utils.encoding import force_text +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext_lazy as _ + +from .classes import EventType + + +class ObjectLinkWidget(object): + template_string = '{{ object_type }}{{ label }}' + + def __init__(self): + self.template = Template(template_string=self.template_string) + + def render(self, name=None, value=None): + label = '' + object_type = '' + url = None + + if value: + label = force_text(value) + object_type = '{}: '.format(value._meta.verbose_name) + try: + url = value.get_absolute_url() + except AttributeError: + url = None + + return self.template.render( + context=Context( + {'label': label, 'object_type': object_type, 'url': url or '#'} + ) + ) + + +def widget_event_actor_link(context, attribute=None): + entry = context['object'] + + ContentType = apps.get_model( + app_label='contenttypes', model_name='ContentType' + ) + + if attribute: + entry = getattr(entry, attribute) + + if entry.actor == entry.target: + label = _('System') + url = None + else: + label = entry.actor + content_type = ContentType.objects.get_for_model(model=entry.actor) + + url = reverse( + viewname='events:events_for_object', kwargs={ + 'app_label': content_type.app_label, 'model': content_type.model, + 'object_id': entry.actor.pk + } + ) + + if url: + return Template( + template_string='{{ label }}' + ).render(context=Context({'label': entry.actor, 'url': url})) + else: + return label + + +def widget_event_type_link(context, attribute=None): + entry = context['object'] + + if attribute: + entry = getattr(entry, attribute) + + return mark_safe( + '%(label)s' % { + 'url': reverse(viewname='events:events_by_verb', kwargs={'verb': entry.verb}), + 'label': EventType.get(name=entry.verb) + } + ) diff --git a/mayan/apps/events/icons.py b/mayan/apps/events/icons.py index 9a4419ec3d..f02aeb483a 100644 --- a/mayan/apps/events/icons.py +++ b/mayan/apps/events/icons.py @@ -5,9 +5,8 @@ from mayan.apps.appearance.classes import Icon icon_event_types_subscriptions_list = Icon( driver_name='fontawesome', symbol='list-ol' ) -icon_events_list = Icon(driver_name='fontawesome', symbol='list-ol') icon_events_for_object = Icon(driver_name='fontawesome', symbol='list-ol') -icon_events_user_list = Icon(driver_name='fontawesome', symbol='rss') +icon_events_list = Icon(driver_name='fontawesome', symbol='list-ol') icon_object_event_types_user_subcriptions_list = Icon( driver_name='fontawesome', symbol='rss' ) diff --git a/mayan/apps/events/links.py b/mayan/apps/events/links.py index a3b45874d4..85c20d66a3 100644 --- a/mayan/apps/events/links.py +++ b/mayan/apps/events/links.py @@ -41,9 +41,9 @@ def get_unread_notification_count(context): ).filter(read=False).count() -link_events_list = Link( - icon_class=icon_events_list, permissions=(permission_events_view,), - text=_('Events'), view='events:events_list' +link_current_user_events = Link( + icon_class=icon_events_list, text=_('My events'), + view='events:current_user_events' ) link_events_details = Link( text=_('Events'), view='events:events_list' @@ -54,6 +54,10 @@ link_events_for_object = Link( permissions=(permission_events_view,), text=_('Events'), view='events:events_for_object', ) +link_events_list = Link( + icon_class=icon_events_list, permissions=(permission_events_view,), + text=_('Events'), view='events:events_list' +) link_event_types_subscriptions_list = Link( icon_class=icon_event_types_subscriptions_list, text=_('Event subscriptions'), @@ -72,10 +76,6 @@ link_object_event_types_user_subcriptions_list = Link( permissions=(permission_events_view,), text=_('Subscriptions'), view='events:object_event_types_user_subcriptions_list', ) -link_user_events = Link( - args='resolved_object.pk', text=_('User events'), - view='events:user_events' -) link_user_notifications_list = Link( badge_text=get_unread_notification_count, icon_class=icon_user_notifications_list, text='', diff --git a/mayan/apps/events/tests/literals.py b/mayan/apps/events/tests/literals.py new file mode 100644 index 0000000000..ac8a767583 --- /dev/null +++ b/mayan/apps/events/tests/literals.py @@ -0,0 +1,6 @@ +from __future__ import unicode_literals + +TEST_EVENT_TYPE_NAMESPACE_LABEL = 'test event type namespace label' +TEST_EVENT_TYPE_NAMESPACE_NAME = 'test_event_type_namespace_name' +TEST_EVENT_TYPE_LABEL = 'test event type label' +TEST_EVENT_TYPE_NAME = 'test_event_type_name' diff --git a/mayan/apps/events/tests/mixins.py b/mayan/apps/events/tests/mixins.py new file mode 100644 index 0000000000..e4ea3dd31b --- /dev/null +++ b/mayan/apps/events/tests/mixins.py @@ -0,0 +1,20 @@ +from __future__ import unicode_literals + +from ..classes import EventTypeNamespace + +from .literals import ( + TEST_EVENT_TYPE_LABEL, TEST_EVENT_TYPE_NAME, + TEST_EVENT_TYPE_NAMESPACE_LABEL, TEST_EVENT_TYPE_NAMESPACE_NAME +) + + +class EventTypeTestMixin(object): + def _create_test_event_type(self): + self.test_event_type_namespace = EventTypeNamespace( + label=TEST_EVENT_TYPE_NAMESPACE_LABEL, + name=TEST_EVENT_TYPE_NAMESPACE_NAME + ) + self.test_event_type = self.test_event_type_namespace.add_event_type( + label=TEST_EVENT_TYPE_LABEL, + name=TEST_EVENT_TYPE_NAME + ) diff --git a/mayan/apps/events/tests/test_api.py b/mayan/apps/events/tests/test_api.py index 449a564182..1312f8e741 100644 --- a/mayan/apps/events/tests/test_api.py +++ b/mayan/apps/events/tests/test_api.py @@ -1,9 +1,69 @@ from __future__ import unicode_literals +from django.contrib.contenttypes.models import ContentType + +from rest_framework import status + +from mayan.apps.documents.tests import DocumentTestMixin from mayan.apps.rest_api.tests import BaseAPITestCase +from ..permissions import permission_events_view + +from .mixins import EventTypeTestMixin + + +class EventTypeNamespaceAPITestCase(EventTypeTestMixin, BaseAPITestCase): + def setUp(self): + super(EventTypeNamespaceAPITestCase, self).setUp() + self._create_test_event_type() -class EventAPITestCase(BaseAPITestCase): def test_evet_type_list_view(self): response = self.get(viewname='rest_api:event-type-list') - self.assertEqual(response.status_code, 200) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_event_type_namespace_list_view(self): + response = self.get(viewname='rest_api:event-type-namespace-list') + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_event_type_namespace_event_type_list_view(self): + response = self.get( + viewname='rest_api:event-type-namespace-event-type-list', + kwargs={ + 'name': self.test_event_type_namespace.name + } + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + +class ObjectEventAPITestCase(DocumentTestMixin, BaseAPITestCase): + auto_upload_document = False + + def setUp(self): + super(ObjectEventAPITestCase, self).setUp() + self.test_object = self.test_document_type + + content_type = ContentType.objects.get_for_model(model=self.test_object) + + self.view_arguments = { + 'app_label': content_type.app_label, + 'model': content_type.model, + 'object_id': self.test_object.pk + } + + def _request_object_event_list_api_view(self): + return self.get( + viewname='rest_api:object-event-list', + kwargs=self.view_arguments + ) + + def test_object_event_list_view_no_permission(self): + response = self._request_object_event_list_api_view() + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_object_event_list_view_with_access(self): + self.grant_access( + obj=self.test_object, permission=permission_events_view + ) + response = self._request_object_event_list_api_view() + self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/mayan/apps/events/tests/test_views.py b/mayan/apps/events/tests/test_views.py index a199a0b7b4..f7fc4cb4c9 100644 --- a/mayan/apps/events/tests/test_views.py +++ b/mayan/apps/events/tests/test_views.py @@ -8,15 +8,18 @@ from ..permissions import permission_events_view class EventsViewTestCase(GenericDocumentViewTestCase): + auto_upload_document = False + def setUp(self): super(EventsViewTestCase, self).setUp() + self.test_object = self.test_document_type - content_type = ContentType.objects.get_for_model(self.test_document) + content_type = ContentType.objects.get_for_model(self.test_object) self.view_arguments = { 'app_label': content_type.app_label, 'model': content_type.model, - 'object_id': self.document.pk + 'object_id': self.test_object.pk } def _request_events_for_object_view(self): @@ -27,19 +30,15 @@ class EventsViewTestCase(GenericDocumentViewTestCase): def test_events_for_object_view_no_permission(self): response = self._request_events_for_object_view() self.assertNotContains( - response=response, text=self.test_document.label, status_code=403 - ) - self.assertNotContains( - response=response, text='otal:', status_code=403 + response=response, text=self.test_object.label, status_code=404 ) def test_events_for_object_view_with_permission(self): - self.grant_permission(permission=permission_events_view) + self.grant_access( + obj=self.test_object, permission=permission_events_view + ) response = self._request_events_for_object_view() self.assertContains( - response=response, text=self.test_document.label, status_code=200 - ) - self.assertNotContains( - response=response, text='otal: 0', status_code=200 + response=response, text=self.test_object.label, status_code=200 ) diff --git a/mayan/apps/events/urls.py b/mayan/apps/events/urls.py index 0c9ac0f4d1..f46d2b7ad2 100644 --- a/mayan/apps/events/urls.py +++ b/mayan/apps/events/urls.py @@ -8,9 +8,10 @@ from .api_views import ( APINotificationListView, APIObjectEventListView ) from .views import ( - EventListView, EventTypeSubscriptionListView, NotificationListView, - NotificationMarkRead, NotificationMarkReadAll, ObjectEventListView, - ObjectEventTypeSubscriptionListView, UserEventListView, VerbEventListView + CurrentUserEventListView, EventListView, EventTypeSubscriptionListView, + NotificationListView, NotificationMarkRead, NotificationMarkReadAll, + ObjectEventListView, ObjectEventTypeSubscriptionListView, + VerbEventListView ) urlpatterns = [ @@ -31,15 +32,15 @@ urlpatterns = [ regex=r'^notifications/all/mark_read/$', view=NotificationMarkReadAll.as_view(), name='notification_mark_read_all' ), + url( + regex=r'^user/events/$', name='current_user_events', + view=CurrentUserEventListView.as_view() + ), url( regex=r'^user/(?P[-\w]+)/(?P[-\w]+)/(?P\d+)/subscriptions/$', view=ObjectEventTypeSubscriptionListView.as_view(), name='object_event_types_user_subcriptions_list' ), - url( - regex=r'^user/(?P\d+)/events/$', view=UserEventListView.as_view(), - name='user_events' - ), url( regex=r'^user/event_types/subscriptions/$', view=EventTypeSubscriptionListView.as_view(), diff --git a/mayan/apps/events/views.py b/mayan/apps/events/views.py index 47b448892c..2af4834ede 100644 --- a/mayan/apps/events/views.py +++ b/mayan/apps/events/views.py @@ -1,7 +1,6 @@ from __future__ import absolute_import, unicode_literals from django.contrib import messages -from django.contrib.auth import get_user_model from django.contrib.contenttypes.models import ContentType from django.http import Http404, HttpResponseRedirect from django.shortcuts import get_object_or_404 @@ -13,7 +12,6 @@ from actstream.models import Action, any_stream from mayan.apps.acls.models import AccessControlList from mayan.apps.common.generics import FormView, SimpleView -from mayan.apps.common.utils import encapsulate from mayan.apps.common.views import SingleObjectListView from .classes import EventType, ModelEventType @@ -21,12 +19,11 @@ from .forms import ( EventTypeUserRelationshipFormSet, ObjectEventTypeUserRelationshipFormSet ) from .icons import ( - icon_events_list, icon_events_user_list, icon_user_notifications_list + icon_events_list, icon_user_notifications_list ) from .links import link_event_types_subscriptions_list from .models import StoredEventType from .permissions import permission_events_view -from .widgets import event_object_link class EventListView(SingleObjectListView): @@ -34,14 +31,6 @@ class EventListView(SingleObjectListView): def get_extra_context(self): return { - 'extra_columns': ( - { - 'name': _('Target'), - 'attribute': encapsulate( - lambda entry: event_object_link(entry) - ) - }, - ), 'hide_object': True, 'title': _('Events'), } @@ -154,26 +143,10 @@ class NotificationMarkReadAll(SimpleView): class ObjectEventListView(EventListView): - view_permissions = None + view_permission = None def dispatch(self, request, *args, **kwargs): - self.object_content_type = get_object_or_404( - klass=ContentType, app_label=self.kwargs['app_label'], - model=self.kwargs['model'] - ) - - try: - self.content_object = self.object_content_type.get_object_for_this_type( - pk=self.kwargs['object_id'] - ) - except self.object_content_type.model_class().DoesNotExist: - raise Http404 - - AccessControlList.objects.check_access( - permissions=permission_events_view, user=request.user, - obj=self.content_object - ) - + self.object = self.get_object() return super( ObjectEventListView, self ).dispatch(request, *args, **kwargs) @@ -188,13 +161,28 @@ class ObjectEventListView(EventListView): 'or using this object.' ), 'no_results_title': _('There are no events for this object'), - 'object': self.content_object, - 'title': _('Events for: %s') % self.content_object, + 'object': self.object, + 'title': _('Events for: %s') % self.object, }) return context + def get_object(self): + content_type = get_object_or_404( + klass=ContentType, app_label=self.kwargs['app_label'], + model=self.kwargs['model'] + ) + + queryset = AccessControlList.objects.filter_by_access( + permission=permission_events_view, + queryset=content_type.model_class().objects.all(), + user=self.request.user + ) + return get_object_or_404( + klass=queryset, pk=self.kwargs['object_id'] + ) + def get_object_list(self): - return any_stream(self.content_object) + return any_stream(self.object) class ObjectEventTypeSubscriptionListView(FormView): @@ -270,50 +258,14 @@ class ObjectEventTypeSubscriptionListView(FormView): return ModelEventType.get_for_instance(instance=self.get_object()) -class UserEventListView(SingleObjectListView): - view_permission = permission_events_view - - def get_extra_context(self): - return { - 'extra_columns': ( - { - 'name': _('Target'), - 'attribute': encapsulate( - lambda entry: event_object_link(entry) - ) - }, - ), - 'hide_object': True, - 'no_results_icon': icon_events_user_list, - 'no_results_text': _( - 'Events are actions that have been performed to this ' - 'user account or by this user account.' - ), - 'no_results_title': _('There are no events for this user'), - 'object': self.get_user(), - 'title': _( - 'Events for user: %s' - ) % self.get_user(), - } - - def get_object_list(self): - return Action.objects.actor(obj=self.get_user()) - - def get_user(self): - return get_object_or_404(klass=get_user_model(), pk=self.kwargs['pk']) +class CurrentUserEventListView(ObjectEventListView): + def get_object(self): + return self.request.user class VerbEventListView(SingleObjectListView): def get_extra_context(self): return { - 'extra_columns': ( - { - 'name': _('Target'), - 'attribute': encapsulate( - lambda entry: event_object_link(entry) - ) - }, - ), 'hide_object': True, 'title': _( 'Events of type: %s' diff --git a/mayan/apps/events/widgets.py b/mayan/apps/events/widgets.py deleted file mode 100644 index 0c69c9c6cd..0000000000 --- a/mayan/apps/events/widgets.py +++ /dev/null @@ -1,49 +0,0 @@ -from __future__ import unicode_literals - -from django.urls import reverse -from django.utils.encoding import force_text -from django.utils.safestring import mark_safe -from django.utils.translation import ugettext_lazy as _ - -from .classes import EventType - - -def event_object_link(entry, attribute='target'): - label = '' - url = '#' - obj_type = '' - - obj = getattr(entry, attribute) - - if obj: - obj_type = '{}: '.format(obj._meta.verbose_name) - if hasattr(obj, 'get_absolute_url'): - url = obj.get_absolute_url() - label = force_text(obj) - - return mark_safe( - '%(obj_type)s%(label)s' % { - 'url': url, 'label': label, 'obj_type': obj_type - } - ) - - -def event_type_link(entry): - return mark_safe( - '%(label)s' % { - 'url': reverse('events:events_by_verb', kwargs={'verb': entry.verb}), - 'label': EventType.get(name=entry.verb) - } - ) - - -def event_user_link(entry): - if entry.actor == entry.target: - return _('System') - else: - return mark_safe( - '%(label)s' % { - 'url': reverse('events:user_events', kwargs={'pk': entry.actor.pk}), - 'label': entry.actor - } - ) diff --git a/mayan/apps/navigation/classes.py b/mayan/apps/navigation/classes.py index cbf60fe7cc..a63face88f 100644 --- a/mayan/apps/navigation/classes.py +++ b/mayan/apps/navigation/classes.py @@ -14,7 +14,7 @@ from django.urls import resolve, reverse from django.utils.encoding import force_str, force_text from mayan.apps.common.settings import setting_home_view -from mayan.apps.common.utils import return_attrib +from mayan.apps.common.utils import resolve_attribute, return_attrib from mayan.apps.permissions import Permission from .utils import get_current_view_name @@ -536,11 +536,12 @@ class SourceColumn(object): # unhashable type: list return () - def __init__(self, source, label=None, attribute=None, func=None, order=None, widget=None): + def __init__(self, source, label=None, attribute=None, func=None, kwargs=None, order=None, widget=None): self.source = source self._label = label self.attribute = attribute self.func = func + self.kwargs = kwargs or {} self.order = order or 0 self.__class__._registry.setdefault(source, []) self.__class__._registry[source].append(self) @@ -561,9 +562,12 @@ class SourceColumn(object): def resolve(self, context): if self.attribute: - result = return_attrib(context['object'], self.attribute) + result = resolve_attribute( + attribute=self.attribute, kwargs=self.kwargs, + obj=context['object'] + ) elif self.func: - result = self.func(context=context) + result = self.func(context=context, **self.kwargs) if self.widget: widget_instance = self.widget()