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 <roberto.rosario.gonzalez@gmail.com>
This commit is contained in:
Roberto Rosario
2019-04-22 01:32:52 -04:00
parent 66e0d9f357
commit ce0a6368f2
14 changed files with 259 additions and 177 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/subscriptions/$',
view=ObjectEventTypeSubscriptionListView.as_view(),
name='object_event_types_user_subcriptions_list'
),
url(
regex=r'^user/(?P<pk>\d+)/events/$', view=UserEventListView.as_view(),
name='user_events'
),
url(
regex=r'^user/event_types/subscriptions/$',
view=EventTypeSubscriptionListView.as_view(),

View File

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

View File

@@ -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(
'<a href="%(url)s">%(obj_type)s%(label)s</a>' % {
'url': url, 'label': label, 'obj_type': obj_type
}
)
def event_type_link(entry):
return mark_safe(
'<a href="%(url)s">%(label)s</a>' % {
'url': reverse('events:events_by_verb', kwargs={'verb': entry.verb}),
'label': EventType.get(name=entry.verb)
}
)
def event_user_link(entry):
if entry.actor == entry.target:
return _('System')
else:
return mark_safe(
'<a href="%(url)s">%(label)s</a>' % {
'url': reverse('events:user_events', kwargs={'pk': entry.actor.pk}),
'label': entry.actor
}
)

View File

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