Backport support for global and object event notification. GitLab issue #262.
Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import logging
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from common import MayanAppConfig, menu_user
|
from common import MayanAppConfig, menu_user
|
||||||
|
from navigation.classes import Separator, Text
|
||||||
|
|
||||||
from .links import link_logout, link_password_change
|
from .links import link_logout, link_password_change
|
||||||
|
|
||||||
@@ -21,6 +22,6 @@ class AuthenticationApp(MayanAppConfig):
|
|||||||
|
|
||||||
menu_user.bind_links(
|
menu_user.bind_links(
|
||||||
links=(
|
links=(
|
||||||
link_password_change, link_logout
|
Separator(), link_password_change, link_logout
|
||||||
), position=99
|
), position=99
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from events.classes import Event
|
from events import EventTypeNamespace
|
||||||
|
|
||||||
event_cabinets_add_document = Event(
|
namespace = EventTypeNamespace(name='cabinets', label=_('Cabinets'))
|
||||||
name='cabinets_add_document',
|
|
||||||
label=_('Document added to cabinet')
|
event_cabinets_add_document = namespace.add_event_type(
|
||||||
|
label=_('Document added to cabinet'), name='add_document'
|
||||||
)
|
)
|
||||||
event_cabinets_remove_document = Event(
|
event_cabinets_remove_document = namespace.add_event_type(
|
||||||
name='cabinets_remove_document',
|
label=_('Document removed from cabinet'), name='remove_document'
|
||||||
label=_('Document removed from cabinet')
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,11 +10,17 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
|
|
||||||
from acls import ModelPermission
|
from acls import ModelPermission
|
||||||
from common import MayanAppConfig, menu_facet, menu_main, menu_sidebar
|
from common import MayanAppConfig, menu_facet, menu_main, menu_sidebar
|
||||||
|
from common.classes import DashboardWidget
|
||||||
from common.dashboards import dashboard_main
|
from common.dashboards import dashboard_main
|
||||||
|
from events import ModelEventType
|
||||||
from mayan.celery import app
|
from mayan.celery import app
|
||||||
from rest_api.classes import APIEndPoint
|
from rest_api.classes import APIEndPoint
|
||||||
|
|
||||||
from .dashboard_widgets import widget_checkouts
|
from .dashboard_widgets import widget_checkouts
|
||||||
|
from .events import (
|
||||||
|
event_document_auto_check_in, event_document_check_in,
|
||||||
|
event_document_check_out, event_document_forceful_check_in
|
||||||
|
)
|
||||||
from .handlers import check_new_version_creation
|
from .handlers import check_new_version_creation
|
||||||
from .links import (
|
from .links import (
|
||||||
link_checkin_document, link_checkout_document, link_checkout_info,
|
link_checkin_document, link_checkout_document, link_checkout_info,
|
||||||
@@ -72,6 +78,13 @@ class CheckoutsApp(MayanAppConfig):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ModelEventType.register(
|
||||||
|
model=Document, event_types=(
|
||||||
|
event_document_auto_check_in, event_document_check_in,
|
||||||
|
event_document_check_out, event_document_forceful_check_in
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
ModelPermission.register(
|
ModelPermission.register(
|
||||||
model=Document, permissions=(
|
model=Document, permissions=(
|
||||||
permission_document_checkout,
|
permission_document_checkout,
|
||||||
|
|||||||
@@ -2,19 +2,21 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from events.classes import Event
|
from events import EventTypeNamespace
|
||||||
|
|
||||||
event_document_auto_check_in = Event(
|
namespace = EventTypeNamespace(name='checkouts', label=_('Checkouts'))
|
||||||
name='checkouts_document_auto_check_in',
|
|
||||||
|
event_document_auto_check_in = namespace.add_event_type(
|
||||||
|
name='document_auto_check_in',
|
||||||
label=_('Document automatically checked in')
|
label=_('Document automatically checked in')
|
||||||
)
|
)
|
||||||
event_document_check_in = Event(
|
event_document_check_in = namespace.add_event_type(
|
||||||
name='checkouts_document_check_in', label=_('Document checked in')
|
name='document_check_in', label=_('Document checked in')
|
||||||
)
|
)
|
||||||
event_document_check_out = Event(
|
event_document_check_out = namespace.add_event_type(
|
||||||
name='checkouts_document_check_out', label=_('Document checked out')
|
name='document_check_out', label=_('Document checked out')
|
||||||
)
|
)
|
||||||
event_document_forceful_check_in = Event(
|
event_document_forceful_check_in = namespace.add_event_type(
|
||||||
name='checkouts_document_forceful_check_in',
|
name='document_forceful_check_in',
|
||||||
label=_('Document forcefully checked in')
|
label=_('Document forcefully checked in')
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ class MayanAppConfig(apps.AppConfig):
|
|||||||
except ImportError as exception:
|
except ImportError as exception:
|
||||||
if force_text(exception) not in ('No module named urls', 'No module named \'{}.urls\''.format(self.name)):
|
if force_text(exception) not in ('No module named urls', 'No module named \'{}.urls\''.format(self.name)):
|
||||||
logger.error(
|
logger.error(
|
||||||
'Import time error when running AppConfig.ready(). Check '
|
'Import time error when running AppConfig.ready() of app '
|
||||||
'apps.py, urls.py, views.py, etc.'
|
'"%s".', self.name
|
||||||
)
|
)
|
||||||
raise exception
|
raise exception
|
||||||
|
|
||||||
@@ -127,7 +127,6 @@ class CommonApp(MayanAppConfig):
|
|||||||
Text(text=CommonApp.get_user_label_text), Separator(),
|
Text(text=CommonApp.get_user_label_text), Separator(),
|
||||||
link_current_user_details, link_current_user_edit,
|
link_current_user_details, link_current_user_edit,
|
||||||
link_current_user_locale_profile_edit,
|
link_current_user_locale_profile_edit,
|
||||||
Separator()
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
@@ -6,9 +6,13 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
from acls import ModelPermission
|
from acls import ModelPermission
|
||||||
from common import MayanAppConfig, menu_facet, menu_object, menu_sidebar
|
from common import MayanAppConfig, menu_facet, menu_object, menu_sidebar
|
||||||
from documents.search import document_page_search, document_search
|
from documents.search import document_page_search, document_search
|
||||||
|
from events import ModelEventType
|
||||||
from navigation import SourceColumn
|
from navigation import SourceColumn
|
||||||
from rest_api.classes import APIEndPoint
|
from rest_api.classes import APIEndPoint
|
||||||
|
|
||||||
|
from .events import (
|
||||||
|
event_document_comment_create, event_document_comment_delete
|
||||||
|
)
|
||||||
from .links import (
|
from .links import (
|
||||||
link_comment_add, link_comment_delete, link_comments_for_document
|
link_comment_add, link_comment_delete, link_comments_for_document
|
||||||
)
|
)
|
||||||
@@ -36,6 +40,12 @@ class DocumentCommentsApp(MayanAppConfig):
|
|||||||
|
|
||||||
Comment = self.get_model('Comment')
|
Comment = self.get_model('Comment')
|
||||||
|
|
||||||
|
ModelEventType.register(
|
||||||
|
model=Document, event_types=(
|
||||||
|
event_document_comment_create, event_document_comment_delete
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
ModelPermission.register(
|
ModelPermission.register(
|
||||||
model=Document, permissions=(
|
model=Document, permissions=(
|
||||||
permission_comment_create, permission_comment_delete,
|
permission_comment_create, permission_comment_delete,
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from events.classes import Event
|
from events import EventTypeNamespace
|
||||||
|
|
||||||
event_document_comment_create = Event(
|
namespace = EventTypeNamespace(
|
||||||
name='document_comment_create',
|
name='document_comments', label=_('Document comments')
|
||||||
label=_('Document comment created')
|
|
||||||
)
|
)
|
||||||
event_document_comment_delete = Event(
|
|
||||||
name='document_comment_delete',
|
event_document_comment_create = namespace.add_event_type(
|
||||||
label=_('Document comment deleted')
|
name='create', label=_('Document comment created')
|
||||||
|
)
|
||||||
|
event_document_comment_delete = namespace.add_event_type(
|
||||||
|
name='delete', label=_('Document comment deleted')
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from events.classes import Event
|
from events import EventTypeNamespace
|
||||||
|
|
||||||
event_parsing_document_version_submit = Event(
|
namespace = EventTypeNamespace(
|
||||||
name='parsing_document_version_submit',
|
name='document_parsing', label=_('Document parsing')
|
||||||
label=_('Document version submitted for parsing')
|
|
||||||
)
|
)
|
||||||
event_parsing_document_version_finish = Event(
|
|
||||||
name='parsing_document_version_finish',
|
event_parsing_document_version_submit = namespace.add_event_type(
|
||||||
label=_('Document version parsing finished')
|
label=_('Document version submitted for parsing'), name='version_submit'
|
||||||
|
)
|
||||||
|
event_parsing_document_version_finish = namespace.add_event_type(
|
||||||
|
label=_('Document version parsing finished'), name='version_finish'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from django.apps import apps
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from document_indexing.tasks import task_index_document
|
from document_indexing.tasks import task_index_document
|
||||||
from events.classes import Event
|
from events.classes import EventType
|
||||||
|
|
||||||
|
|
||||||
def handler_index_document(sender, **kwargs):
|
def handler_index_document(sender, **kwargs):
|
||||||
@@ -42,7 +42,7 @@ def handler_trigger_transition(sender, **kwargs):
|
|||||||
transition = list(set(trigger_transitions) & set(workflow_instance.get_transition_choices()))[0]
|
transition = list(set(trigger_transitions) & set(workflow_instance.get_transition_choices()))[0]
|
||||||
|
|
||||||
workflow_instance.do_transition(
|
workflow_instance.do_transition(
|
||||||
comment=_('Event trigger: %s') % Event.get(name=action.verb).label,
|
comment=_('Event trigger: %s') % EventType.get(name=action.verb).label,
|
||||||
transition=transition
|
transition=transition
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import django.db.models.deletion
|
|||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('events', '0001_initial'),
|
('events', '0005_auto_20170731_0452'),
|
||||||
('document_states', '0004_workflow_internal_name'),
|
('document_states', '0004_workflow_internal_name'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
from acls.models import AccessControlList
|
from acls.models import AccessControlList
|
||||||
from common.validators import validate_internal_name
|
from common.validators import validate_internal_name
|
||||||
from documents.models import Document, DocumentType
|
from documents.models import Document, DocumentType
|
||||||
from events.models import EventType
|
from events.models import StoredEventType
|
||||||
from permissions import Permission
|
from permissions import Permission
|
||||||
|
|
||||||
from .error_logs import error_log_state_actions
|
from .error_logs import error_log_state_actions
|
||||||
@@ -306,7 +306,8 @@ class WorkflowTransitionTriggerEvent(models.Model):
|
|||||||
related_name='trigger_events', verbose_name=_('Transition')
|
related_name='trigger_events', verbose_name=_('Transition')
|
||||||
)
|
)
|
||||||
event_type = models.ForeignKey(
|
event_type = models.ForeignKey(
|
||||||
EventType, on_delete=models.CASCADE, verbose_name=_('Event type')
|
StoredEventType, on_delete=models.CASCADE,
|
||||||
|
verbose_name=_('Event type')
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ from common.views import (
|
|||||||
)
|
)
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.views import DocumentListView
|
from documents.views import DocumentListView
|
||||||
from events.classes import Event
|
from events.classes import EventType
|
||||||
from events.models import EventType
|
from events.models import StoredEventType
|
||||||
|
|
||||||
from .classes import WorkflowAction
|
from .classes import WorkflowAction
|
||||||
from .forms import (
|
from .forms import (
|
||||||
@@ -675,7 +675,7 @@ class WorkflowStateListView(SingleObjectListView):
|
|||||||
|
|
||||||
class SetupWorkflowTransitionTriggerEventListView(FormView):
|
class SetupWorkflowTransitionTriggerEventListView(FormView):
|
||||||
form_class = WorkflowTransitionTriggerEventRelationshipFormSet
|
form_class = WorkflowTransitionTriggerEventRelationshipFormSet
|
||||||
submodel = EventType
|
submodel = StoredEventType
|
||||||
|
|
||||||
def dispatch(self, *args, **kwargs):
|
def dispatch(self, *args, **kwargs):
|
||||||
messages.warning(
|
messages.warning(
|
||||||
@@ -689,7 +689,7 @@ class SetupWorkflowTransitionTriggerEventListView(FormView):
|
|||||||
user=self.request.user, obj=self.get_object().workflow
|
user=self.request.user, obj=self.get_object().workflow
|
||||||
)
|
)
|
||||||
|
|
||||||
Event.refresh()
|
EventType.refresh()
|
||||||
return super(
|
return super(
|
||||||
SetupWorkflowTransitionTriggerEventListView, self
|
SetupWorkflowTransitionTriggerEventListView, self
|
||||||
).dispatch(*args, **kwargs)
|
).dispatch(*args, **kwargs)
|
||||||
@@ -735,8 +735,10 @@ class SetupWorkflowTransitionTriggerEventListView(FormView):
|
|||||||
initial = []
|
initial = []
|
||||||
|
|
||||||
# Return the queryset by name from the sorted list of the class
|
# Return the queryset by name from the sorted list of the class
|
||||||
event_type_ids = [event_type.name for event_type in Event.all()]
|
event_type_ids = [event_type.id for event_type in Event.all()]
|
||||||
event_type_queryset = EventType.objects.filter(name__in=event_type_ids)
|
event_type_queryset = StoredEventType.objects.filter(
|
||||||
|
name__in=event_type_ids
|
||||||
|
)
|
||||||
|
|
||||||
for event_type in event_type_queryset:
|
for event_type in event_type_queryset:
|
||||||
initial.append({
|
initial.append({
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ from converter.permissions import (
|
|||||||
permission_transformation_delete, permission_transformation_edit,
|
permission_transformation_delete, permission_transformation_edit,
|
||||||
permission_transformation_view,
|
permission_transformation_view,
|
||||||
)
|
)
|
||||||
from events.links import link_events_for_object
|
from events import ModelEventType
|
||||||
|
from events.links import (
|
||||||
|
link_events_for_object, link_object_event_types_user_subcriptions_list,
|
||||||
|
link_object_event_types_user_subcriptions_list_with_icon
|
||||||
|
)
|
||||||
from events.permissions import permission_events_view
|
from events.permissions import permission_events_view
|
||||||
from mayan.celery import app
|
from mayan.celery import app
|
||||||
from mayan_statistics.classes import StatisticNamespace, CharJSLine
|
from mayan_statistics.classes import StatisticNamespace, CharJSLine
|
||||||
@@ -36,6 +40,12 @@ from .dashboard_widgets import (
|
|||||||
widget_new_documents_this_month, widget_pages_per_month,
|
widget_new_documents_this_month, widget_pages_per_month,
|
||||||
widget_total_documents
|
widget_total_documents
|
||||||
)
|
)
|
||||||
|
from .events import (
|
||||||
|
event_document_create, event_document_download,
|
||||||
|
event_document_properties_edit, event_document_type_change,
|
||||||
|
event_document_new_version, event_document_version_revert,
|
||||||
|
event_document_view
|
||||||
|
)
|
||||||
from .handlers import (
|
from .handlers import (
|
||||||
create_default_document_type, handler_scan_duplicates_for
|
create_default_document_type, handler_scan_duplicates_for
|
||||||
)
|
)
|
||||||
@@ -142,6 +152,19 @@ class DocumentsApp(MayanAppConfig):
|
|||||||
label=_('MIME type'), name='versions__mimetype', type_name='field'
|
label=_('MIME type'), name='versions__mimetype', type_name='field'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ModelEventType.register(
|
||||||
|
model=DocumentType, event_types=(
|
||||||
|
event_document_create,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ModelEventType.register(
|
||||||
|
model=Document, event_types=(
|
||||||
|
event_document_download, event_document_properties_edit,
|
||||||
|
event_document_type_change, event_document_new_version,
|
||||||
|
event_document_version_revert, event_document_view
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
ModelPermission.register(
|
ModelPermission.register(
|
||||||
model=Document, permissions=(
|
model=Document, permissions=(
|
||||||
permission_acl_edit, permission_acl_view,
|
permission_acl_edit, permission_acl_view,
|
||||||
@@ -389,7 +412,8 @@ class DocumentsApp(MayanAppConfig):
|
|||||||
menu_object.bind_links(
|
menu_object.bind_links(
|
||||||
links=(
|
links=(
|
||||||
link_document_type_edit, link_document_type_filename_list,
|
link_document_type_edit, link_document_type_filename_list,
|
||||||
link_acl_list, link_document_type_delete
|
link_acl_list, link_object_event_types_user_subcriptions_list,
|
||||||
|
link_document_type_delete
|
||||||
), sources=(DocumentType,)
|
), sources=(DocumentType,)
|
||||||
)
|
)
|
||||||
menu_object.bind_links(
|
menu_object.bind_links(
|
||||||
@@ -446,8 +470,11 @@ class DocumentsApp(MayanAppConfig):
|
|||||||
links=(link_document_properties,), sources=(Document,), position=2
|
links=(link_document_properties,), sources=(Document,), position=2
|
||||||
)
|
)
|
||||||
menu_facet.bind_links(
|
menu_facet.bind_links(
|
||||||
links=(link_events_for_object, link_document_version_list,),
|
links=(
|
||||||
sources=(Document,), position=2
|
link_events_for_object,
|
||||||
|
link_object_event_types_user_subcriptions_list_with_icon,
|
||||||
|
link_document_version_list,
|
||||||
|
), sources=(Document,), position=2
|
||||||
)
|
)
|
||||||
menu_facet.bind_links(links=(link_document_pages,), sources=(Document,))
|
menu_facet.bind_links(links=(link_document_pages,), sources=(Document,))
|
||||||
|
|
||||||
|
|||||||
@@ -2,29 +2,28 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from events.classes import Event
|
from events import EventTypeNamespace
|
||||||
|
|
||||||
event_document_create = Event(
|
namespace = EventTypeNamespace(name='documents', label=_('Documents'))
|
||||||
name='documents_document_create', label=_('Document created')
|
|
||||||
|
event_document_create = namespace.add_event_type(
|
||||||
|
name='document_create', label=_('Document created')
|
||||||
)
|
)
|
||||||
event_document_download = Event(
|
event_document_download = namespace.add_event_type(
|
||||||
name='documents_document_download',
|
name='document_download', label=_('Document downloaded')
|
||||||
label=_('Document downloaded')
|
|
||||||
)
|
)
|
||||||
event_document_properties_edit = Event(
|
event_document_properties_edit = namespace.add_event_type(
|
||||||
name='documents_document_edit', label=_('Document properties edited')
|
name='document_edit', label=_('Document properties edited')
|
||||||
)
|
)
|
||||||
event_document_type_change = Event(
|
event_document_type_change = namespace.add_event_type(
|
||||||
name='documents_document_type_change', label=_('Document type changed')
|
name='document_type_change', label=_('Document type changed')
|
||||||
)
|
)
|
||||||
event_document_new_version = Event(
|
event_document_new_version = namespace.add_event_type(
|
||||||
name='documents_document_new_version', label=_('New version uploaded')
|
name='document_new_version', label=_('New version uploaded')
|
||||||
)
|
)
|
||||||
event_document_version_revert = Event(
|
event_document_version_revert = namespace.add_event_type(
|
||||||
name='documents_document_version_revert',
|
name='document_version_revert', label=_('Document version reverted')
|
||||||
label=_('Document version reverted')
|
|
||||||
)
|
)
|
||||||
event_document_view = Event(
|
event_document_view = namespace.add_event_type(
|
||||||
name='documents_document_view',
|
name='document_view', label=_('Document viewed')
|
||||||
label=_('Document viewed')
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -222,9 +222,13 @@ class Document(models.Model):
|
|||||||
if new_document:
|
if new_document:
|
||||||
if user:
|
if user:
|
||||||
self.add_as_recent_document_for_user(user)
|
self.add_as_recent_document_for_user(user)
|
||||||
event_document_create.commit(actor=user, target=self)
|
event_document_create.commit(
|
||||||
|
actor=user, target=self, action_object=self.document_type
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
event_document_create.commit(target=self)
|
event_document_create.commit(
|
||||||
|
target=self, action_object=self.document_type
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if _commit_events:
|
if _commit_events:
|
||||||
event_document_properties_edit.commit(actor=user, target=self)
|
event_document_properties_edit.commit(actor=user, target=self)
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class DocumentEventsTestCase(GenericDocumentViewTestCase):
|
|||||||
|
|
||||||
event = Action.objects.any(obj=self.document).first()
|
event = Action.objects.any(obj=self.document).first()
|
||||||
|
|
||||||
self.assertEqual(event.verb, event_document_download.name)
|
self.assertEqual(event.verb, event_document_download.id)
|
||||||
self.assertEqual(event.target, self.document)
|
self.assertEqual(event.target, self.document)
|
||||||
self.assertEqual(event.actor, self.user)
|
self.assertEqual(event.actor, self.user)
|
||||||
|
|
||||||
@@ -98,6 +98,6 @@ class DocumentEventsTestCase(GenericDocumentViewTestCase):
|
|||||||
|
|
||||||
event = Action.objects.any(obj=self.document).first()
|
event = Action.objects.any(obj=self.document).first()
|
||||||
|
|
||||||
self.assertEqual(event.verb, event_document_view.name)
|
self.assertEqual(event.verb, event_document_view.id)
|
||||||
self.assertEqual(event.target, self.document)
|
self.assertEqual(event.target, self.document)
|
||||||
self.assertEqual(event.actor, self.user)
|
self.assertEqual(event.actor, self.user)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
from .classes import Event # NOQA
|
from .classes import EventTypeNamespace, ModelEventType # NOQA
|
||||||
|
|
||||||
default_app_config = 'events.apps.EventsApp'
|
default_app_config = 'events.apps.EventsApp'
|
||||||
|
|||||||
@@ -2,9 +2,19 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import EventType
|
from .models import EventSubscription, Notification, StoredEventType
|
||||||
|
|
||||||
|
|
||||||
@admin.register(EventType)
|
@admin.register(EventSubscription)
|
||||||
class EventTypeAdmin(admin.ModelAdmin):
|
class EventSubscriptionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('user', 'stored_event_type')
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(StoredEventType)
|
||||||
|
class StoredEventTypeAdmin(admin.ModelAdmin):
|
||||||
readonly_fields = ('name', '__str__')
|
readonly_fields = ('name', '__str__')
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(Notification)
|
||||||
|
class NotificationAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('user', 'action', 'read')
|
||||||
|
|||||||
@@ -10,9 +10,13 @@ from rest_framework import generics
|
|||||||
from acls.models import AccessControlList
|
from acls.models import AccessControlList
|
||||||
from rest_api.permissions import MayanPermission
|
from rest_api.permissions import MayanPermission
|
||||||
|
|
||||||
from .classes import Event
|
from .classes import EventType, EventTypeNamespace
|
||||||
|
from .models import Notification
|
||||||
from .permissions import permission_events_view
|
from .permissions import permission_events_view
|
||||||
from .serializers import EventSerializer, EventTypeSerializer
|
from .serializers import (
|
||||||
|
EventSerializer, EventTypeSerializer, EventTypeNamespaceSerializer,
|
||||||
|
NotificationSerializer
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class APIObjectEventListView(generics.ListAPIView):
|
class APIObjectEventListView(generics.ListAPIView):
|
||||||
@@ -46,13 +50,72 @@ class APIObjectEventListView(generics.ListAPIView):
|
|||||||
return any_stream(obj)
|
return any_stream(obj)
|
||||||
|
|
||||||
|
|
||||||
|
class APIEventTypeNamespaceDetailView(generics.RetrieveAPIView):
|
||||||
|
"""
|
||||||
|
Returns the details of an event type namespace.
|
||||||
|
"""
|
||||||
|
serializer_class = EventTypeNamespaceSerializer
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
try:
|
||||||
|
return EventTypeNamespace.get(name=self.kwargs['name'])
|
||||||
|
except KeyError:
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
|
||||||
|
class APIEventTypeNamespaceListView(generics.ListAPIView):
|
||||||
|
"""
|
||||||
|
Returns a list of all the available event type namespaces.
|
||||||
|
"""
|
||||||
|
|
||||||
|
serializer_class = EventTypeNamespaceSerializer
|
||||||
|
queryset = EventTypeNamespace.all()
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
return {
|
||||||
|
'format': self.format_kwarg,
|
||||||
|
'request': self.request,
|
||||||
|
'view': self
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class APIEventTypeNamespaceEventTypeListView(generics.ListAPIView):
|
||||||
|
"""
|
||||||
|
Returns a list of all the available event types from a namespaces.
|
||||||
|
"""
|
||||||
|
|
||||||
|
serializer_class = EventTypeSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
try:
|
||||||
|
return EventTypeNamespace.get(
|
||||||
|
name=self.kwargs['name']
|
||||||
|
).get_event_types()
|
||||||
|
except KeyError:
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
return {
|
||||||
|
'format': self.format_kwarg,
|
||||||
|
'request': self.request,
|
||||||
|
'view': self
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class APIEventTypeListView(generics.ListAPIView):
|
class APIEventTypeListView(generics.ListAPIView):
|
||||||
"""
|
"""
|
||||||
Returns a list of all the available event types.
|
Returns a list of all the available event types.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
serializer_class = EventTypeSerializer
|
serializer_class = EventTypeSerializer
|
||||||
queryset = sorted(Event.all(), key=lambda event: event.name)
|
queryset = EventType.all()
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
return {
|
||||||
|
'format': self.format_kwarg,
|
||||||
|
'request': self.request,
|
||||||
|
'view': self
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class APIEventListView(generics.ListAPIView):
|
class APIEventListView(generics.ListAPIView):
|
||||||
@@ -64,3 +127,20 @@ class APIEventListView(generics.ListAPIView):
|
|||||||
permission_classes = (MayanPermission,)
|
permission_classes = (MayanPermission,)
|
||||||
queryset = Action.objects.all()
|
queryset = Action.objects.all()
|
||||||
serializer_class = EventSerializer
|
serializer_class = EventSerializer
|
||||||
|
|
||||||
|
def get_serializer_context(self):
|
||||||
|
return {
|
||||||
|
'format': self.format_kwarg,
|
||||||
|
'request': self.request,
|
||||||
|
'view': self
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class APINotificationListView(generics.ListAPIView):
|
||||||
|
"""
|
||||||
|
Return a list of notifications for the current user.
|
||||||
|
"""
|
||||||
|
serializer_class = NotificationSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Notification.objects.filter(user=self.request.user)
|
||||||
|
|||||||
@@ -3,11 +3,19 @@ from __future__ import unicode_literals
|
|||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from common import MayanAppConfig, menu_tools
|
from common import (
|
||||||
|
MayanAppConfig, menu_main, menu_object, menu_secondary, menu_tools,
|
||||||
|
menu_user
|
||||||
|
)
|
||||||
|
from common.widgets import two_state_template
|
||||||
from navigation import SourceColumn
|
from navigation import SourceColumn
|
||||||
from rest_api.classes import APIEndPoint
|
from rest_api.classes import APIEndPoint
|
||||||
|
|
||||||
from .links import link_events_list
|
from .links import (
|
||||||
|
link_events_list, link_event_types_subscriptions_list,
|
||||||
|
link_notification_mark_read, link_notification_mark_read_all,
|
||||||
|
link_user_notifications_list,
|
||||||
|
)
|
||||||
from .licenses import * # NOQA
|
from .licenses import * # NOQA
|
||||||
from .widgets import event_object_link, event_type_link
|
from .widgets import event_object_link, event_type_link
|
||||||
|
|
||||||
@@ -28,6 +36,8 @@ class EventsApp(MayanAppConfig):
|
|||||||
def ready(self):
|
def ready(self):
|
||||||
super(EventsApp, self).ready()
|
super(EventsApp, self).ready()
|
||||||
Action = apps.get_model(app_label='actstream', model_name='Action')
|
Action = apps.get_model(app_label='actstream', model_name='Action')
|
||||||
|
Notification = self.get_model(model_name='Notification')
|
||||||
|
StoredEventType = self.get_model(model_name='StoredEventType')
|
||||||
|
|
||||||
APIEndPoint(app=self, version_string='1')
|
APIEndPoint(app=self, version_string='1')
|
||||||
|
|
||||||
@@ -39,7 +49,7 @@ class EventsApp(MayanAppConfig):
|
|||||||
func=lambda context: event_actor(context['object'])
|
func=lambda context: event_actor(context['object'])
|
||||||
)
|
)
|
||||||
SourceColumn(
|
SourceColumn(
|
||||||
source=Action, label=_('Verb'),
|
source=Action, label=_('Event'),
|
||||||
func=lambda context: event_type_link(context['object'])
|
func=lambda context: event_type_link(context['object'])
|
||||||
)
|
)
|
||||||
SourceColumn(
|
SourceColumn(
|
||||||
@@ -49,4 +59,44 @@ class EventsApp(MayanAppConfig):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SourceColumn(
|
||||||
|
source=StoredEventType, label=_('Namespace'), attribute='namespace'
|
||||||
|
)
|
||||||
|
SourceColumn(
|
||||||
|
source=StoredEventType, label=_('Label'), attribute='label'
|
||||||
|
)
|
||||||
|
|
||||||
|
SourceColumn(
|
||||||
|
source=Notification, label=_('Timestamp'),
|
||||||
|
attribute='action.timestamp'
|
||||||
|
)
|
||||||
|
SourceColumn(
|
||||||
|
source=Notification, label=_('Actor'), attribute='action.actor'
|
||||||
|
)
|
||||||
|
SourceColumn(
|
||||||
|
source=Notification, label=_('Event'),
|
||||||
|
func=lambda context: event_type_link(context['object'].action)
|
||||||
|
)
|
||||||
|
SourceColumn(
|
||||||
|
source=Notification, label=_('Target'),
|
||||||
|
func=lambda context: event_object_link(context['object'].action)
|
||||||
|
)
|
||||||
|
SourceColumn(
|
||||||
|
source=Notification, label=_('Seen'),
|
||||||
|
func=lambda context: two_state_template(
|
||||||
|
state=context['object'].read
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
menu_main.bind_links(
|
||||||
|
links=(link_user_notifications_list,), position=99
|
||||||
|
)
|
||||||
|
menu_object.bind_links(
|
||||||
|
links=(link_notification_mark_read,), sources=(Notification,)
|
||||||
|
)
|
||||||
|
menu_secondary.bind_links(
|
||||||
|
links=(link_notification_mark_read_all,),
|
||||||
|
sources=('events:user_notifications_list',)
|
||||||
|
)
|
||||||
menu_tools.bind_links(links=(link_events_list,))
|
menu_tools.bind_links(links=(link_events_list,))
|
||||||
|
menu_user.bind_links(links=(link_event_types_subscriptions_list,))
|
||||||
|
|||||||
@@ -1,70 +1,231 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.utils.encoding import force_text
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.utils.encoding import force_text, python_2_unicode_compatible
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from actstream import action
|
from actstream import action
|
||||||
|
|
||||||
|
from .permissions import permission_events_view
|
||||||
|
|
||||||
class Event(object):
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class EventTypeNamespace(object):
|
||||||
_registry = {}
|
_registry = {}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def all(cls):
|
def all(cls):
|
||||||
return Event.sort(event_type_list=cls._registry.values())
|
return sorted(cls._registry.values())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, name):
|
||||||
|
return cls._registry[name]
|
||||||
|
|
||||||
|
def __init__(self, name, label):
|
||||||
|
self.name = name
|
||||||
|
self.label = label
|
||||||
|
self.event_types = []
|
||||||
|
self.__class__._registry[name] = self
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return force_text(self.label)
|
||||||
|
|
||||||
|
def add_event_type(self, name, label):
|
||||||
|
event_type = EventType(namespace=self, name=name, label=label)
|
||||||
|
self.event_types.append(event_type)
|
||||||
|
return event_type
|
||||||
|
|
||||||
|
def get_event_types(self):
|
||||||
|
return EventType.sort(event_type_list=self.event_types)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class EventType(object):
|
||||||
|
_registry = {}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def sort(event_type_list):
|
||||||
|
return sorted(
|
||||||
|
event_type_list, key=lambda x: (x.namespace.label, x.label)
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def all(cls):
|
||||||
|
# Return sorted permisions by namespace.name
|
||||||
|
return EventType.sort(event_type_list=cls._registry.values())
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls, name):
|
def get(cls, name):
|
||||||
try:
|
try:
|
||||||
return cls._registry[name]
|
return cls._registry[name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise KeyError(
|
return _('Unknown or obsolete event type: %s') % name
|
||||||
_('Unknown or obsolete event type: {0}'.format(name))
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_label(cls, name):
|
|
||||||
try:
|
|
||||||
return cls.get(name=name).label
|
|
||||||
except KeyError as exception:
|
|
||||||
return force_text(exception)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def refresh(cls):
|
def refresh(cls):
|
||||||
for event_type in cls.all():
|
for event_type in cls.all():
|
||||||
event_type.get_type()
|
event_type.get_stored_event_type()
|
||||||
|
|
||||||
@staticmethod
|
def __init__(self, namespace, name, label):
|
||||||
def sort(event_type_list):
|
self.namespace = namespace
|
||||||
return sorted(
|
|
||||||
event_type_list, key=lambda x: x.label
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, name, label):
|
|
||||||
self.name = name
|
self.name = name
|
||||||
self.label = label
|
self.label = label
|
||||||
self.event_type = None
|
self.stored_event_type = None
|
||||||
self.__class__._registry[name] = self
|
self.__class__._registry[self.id] = self
|
||||||
|
|
||||||
def get_type(self):
|
def __str__(self):
|
||||||
if not self.event_type:
|
return force_text('{}: {}'.format(self.namespace.label, self.label))
|
||||||
EventType = apps.get_model('events', 'EventType')
|
|
||||||
|
|
||||||
self.event_type, created = EventType.objects.get_or_create(
|
|
||||||
name=self.name
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.event_type
|
|
||||||
|
|
||||||
def commit(self, actor=None, action_object=None, target=None):
|
def commit(self, actor=None, action_object=None, target=None):
|
||||||
if not self.event_type:
|
AccessControlList = apps.get_model(
|
||||||
EventType = apps.get_model('events', 'EventType')
|
app_label='acls', model_name='AccessControlList'
|
||||||
self.event_type, created = EventType.objects.get_or_create(
|
)
|
||||||
name=self.name
|
Action = apps.get_model(
|
||||||
)
|
app_label='actstream', model_name='Action'
|
||||||
|
)
|
||||||
|
ContentType = apps.get_model(
|
||||||
|
app_label='contenttypes', model_name='ContentType'
|
||||||
|
)
|
||||||
|
Notification = apps.get_model(
|
||||||
|
app_label='events', model_name='Notification'
|
||||||
|
)
|
||||||
|
|
||||||
action.send(
|
results = action.send(
|
||||||
actor or target, actor=actor, verb=self.name,
|
actor or target, actor=actor, verb=self.id,
|
||||||
action_object=action_object, target=target
|
action_object=action_object, target=target
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for handler, result in results:
|
||||||
|
if isinstance(result, Action):
|
||||||
|
for user in get_user_model().objects.all():
|
||||||
|
notification = None
|
||||||
|
|
||||||
|
if user.event_subscriptions.filter(stored_event_type__name=result.verb).exists():
|
||||||
|
if result.target:
|
||||||
|
try:
|
||||||
|
AccessControlList.objects.check_access(
|
||||||
|
permissions=permission_events_view,
|
||||||
|
user=user, obj=result.target
|
||||||
|
)
|
||||||
|
except PermissionDenied:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
notification = Notification.objects.create(action=result, user=user)
|
||||||
|
else:
|
||||||
|
notification = Notification.objects.create(action=result, user=user)
|
||||||
|
|
||||||
|
if result.target:
|
||||||
|
content_type = ContentType.objects.get_for_model(model=result.target)
|
||||||
|
|
||||||
|
relationship = user.object_subscriptions.filter(
|
||||||
|
content_type=content_type,
|
||||||
|
object_id=result.target.pk,
|
||||||
|
stored_event_type__name=result.verb
|
||||||
|
)
|
||||||
|
|
||||||
|
if relationship.exists():
|
||||||
|
try:
|
||||||
|
AccessControlList.objects.check_access(
|
||||||
|
permissions=permission_events_view,
|
||||||
|
user=user, obj=result.target
|
||||||
|
)
|
||||||
|
except PermissionDenied:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
notification = Notification.objects.create(action=result, user=user)
|
||||||
|
if not notification and result.action_object:
|
||||||
|
content_type = ContentType.objects.get_for_model(model=result.action_object)
|
||||||
|
|
||||||
|
relationship = user.object_subscriptions.filter(
|
||||||
|
content_type=content_type,
|
||||||
|
object_id=result.action_object.pk,
|
||||||
|
stored_event_type__name=result.verb
|
||||||
|
)
|
||||||
|
|
||||||
|
if relationship.exists():
|
||||||
|
try:
|
||||||
|
AccessControlList.objects.check_access(
|
||||||
|
permissions=permission_events_view,
|
||||||
|
user=user, obj=result.action_object
|
||||||
|
)
|
||||||
|
except PermissionDenied:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
notification = Notification.objects.create(action=result, user=user)
|
||||||
|
|
||||||
|
def get_stored_event_type(self):
|
||||||
|
if not self.stored_event_type:
|
||||||
|
StoredEventType = apps.get_model('events', 'StoredEventType')
|
||||||
|
|
||||||
|
self.stored_event_type, created = StoredEventType.objects.get_or_create(
|
||||||
|
name=self.id
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.stored_event_type
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self):
|
||||||
|
return '%s.%s' % (self.namespace.name, self.name)
|
||||||
|
|
||||||
|
|
||||||
|
class ModelEventType(object):
|
||||||
|
"""
|
||||||
|
Class to allow matching a model to a specific set of events.
|
||||||
|
"""
|
||||||
|
_inheritances = {}
|
||||||
|
_proxies = {}
|
||||||
|
_registry = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_for_class(cls, klass):
|
||||||
|
return cls._registry.get(klass, ())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_for_instance(cls, instance):
|
||||||
|
StoredEventType = apps.get_model(
|
||||||
|
app_label='events', model_name='StoredEventType'
|
||||||
|
)
|
||||||
|
|
||||||
|
events = []
|
||||||
|
|
||||||
|
class_events = cls._registry.get(type(instance))
|
||||||
|
|
||||||
|
if class_events:
|
||||||
|
events.extend(class_events)
|
||||||
|
|
||||||
|
proxy = cls._proxies.get(type(instance))
|
||||||
|
|
||||||
|
if proxy:
|
||||||
|
events.extend(cls._registry.get(proxy))
|
||||||
|
|
||||||
|
pks = [
|
||||||
|
event.id for event in set(events)
|
||||||
|
]
|
||||||
|
|
||||||
|
return EventType.sort(
|
||||||
|
event_type_list=StoredEventType.objects.filter(name__in=pks)
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_inheritance(cls, model):
|
||||||
|
return cls._inheritances[model]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register(cls, model, event_types):
|
||||||
|
cls._registry.setdefault(model, [])
|
||||||
|
for event_type in event_types:
|
||||||
|
cls._registry[model].append(event_type)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register_inheritance(cls, model, related):
|
||||||
|
cls._inheritances[model] = related
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register_proxy(cls, source, model):
|
||||||
|
cls._proxies[model] = source
|
||||||
|
|||||||
122
mayan/apps/events/forms.py
Normal file
122
mayan/apps/events/forms.py
Normal 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
|
||||||
|
)
|
||||||
@@ -26,12 +26,44 @@ def get_kwargs_factory(variable_name):
|
|||||||
return get_kwargs
|
return get_kwargs
|
||||||
|
|
||||||
|
|
||||||
|
def get_notification_count(context):
|
||||||
|
return context['request'].user.notifications.filter(read=False).count()
|
||||||
|
|
||||||
|
|
||||||
link_events_list = Link(
|
link_events_list = Link(
|
||||||
icon='fa fa-list-ol', permissions=(permission_events_view,),
|
icon='fa fa-list-ol', permissions=(permission_events_view,),
|
||||||
text=_('Events'), view='events:events_list'
|
text=_('Events'), view='events:events_list'
|
||||||
)
|
)
|
||||||
|
link_events_details = Link(
|
||||||
|
text=_('Events'), view='events:events_list'
|
||||||
|
)
|
||||||
link_events_for_object = Link(
|
link_events_for_object = Link(
|
||||||
icon='fa fa-list-ol', permissions=(permission_events_view,),
|
icon='fa fa-list-ol', permissions=(permission_events_view,),
|
||||||
text=_('Events'), view='events:events_for_object',
|
text=_('Events'), view='events:events_for_object',
|
||||||
kwargs=get_kwargs_factory('resolved_object')
|
kwargs=get_kwargs_factory('resolved_object')
|
||||||
)
|
)
|
||||||
|
link_event_types_subscriptions_list = Link(
|
||||||
|
icon='fa fa-list-ol', text=_('Event subscriptions'),
|
||||||
|
view='events:event_types_user_subcriptions_list'
|
||||||
|
)
|
||||||
|
link_notification_mark_read = Link(
|
||||||
|
args='object.pk', text=_('Mark as seen'),
|
||||||
|
view='events:notification_mark_read'
|
||||||
|
)
|
||||||
|
link_notification_mark_read_all = Link(
|
||||||
|
text=_('Mark all as seen'), view='events:notification_mark_read_all'
|
||||||
|
)
|
||||||
|
link_object_event_types_user_subcriptions_list = Link(
|
||||||
|
kwargs=get_kwargs_factory('resolved_object'),
|
||||||
|
permissions=(permission_events_view,), text=_('Subscriptions'),
|
||||||
|
view='events:object_event_types_user_subcriptions_list',
|
||||||
|
)
|
||||||
|
link_object_event_types_user_subcriptions_list_with_icon = Link(
|
||||||
|
kwargs=get_kwargs_factory('resolved_object'), icon='fa fa-rss',
|
||||||
|
permissions=(permission_events_view,), text=_('Subscriptions'),
|
||||||
|
view='events:object_event_types_user_subcriptions_list',
|
||||||
|
)
|
||||||
|
link_user_notifications_list = Link(
|
||||||
|
icon='fa fa-bell', text=get_notification_count,
|
||||||
|
view='events:user_notifications_list'
|
||||||
|
)
|
||||||
|
|||||||
34
mayan/apps/events/managers.py
Normal file
34
mayan/apps/events/managers.py
Normal 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
|
||||||
|
)
|
||||||
30
mayan/apps/events/migrations/0002_eventsubscription.py
Normal file
30
mayan/apps/events/migrations/0002_eventsubscription.py
Normal 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',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
32
mayan/apps/events/migrations/0003_notification.py
Normal file
32
mayan/apps/events/migrations/0003_notification.py
Normal 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',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
40
mayan/apps/events/migrations/0004_auto_20170731_0423.py
Normal file
40
mayan/apps/events/migrations/0004_auto_20170731_0423.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
86
mayan/apps/events/migrations/0005_auto_20170731_0452.py
Normal file
86
mayan/apps/events/migrations/0005_auto_20170731_0452.py
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2017-07-31 04:52
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def update_event_types_names(apps, schema_editor):
|
||||||
|
Action = apps.get_model('actstream', 'Action')
|
||||||
|
StoredEventType = apps.get_model('events', 'StoredEventType')
|
||||||
|
|
||||||
|
known_namespaces = {
|
||||||
|
'documents_': 'documents.',
|
||||||
|
'checkouts_': 'checkouts.',
|
||||||
|
'document_comment_': 'document_comments.',
|
||||||
|
'parsing_document_': 'document_parsing.',
|
||||||
|
'ocr_': 'ocr.',
|
||||||
|
'tag_': 'tags.',
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern = re.compile('|'.join(known_namespaces.keys()))
|
||||||
|
|
||||||
|
for event_type in StoredEventType.objects.all():
|
||||||
|
event_type.name = pattern.sub(
|
||||||
|
lambda x: known_namespaces[x.group()], event_type.name
|
||||||
|
)
|
||||||
|
event_type.save()
|
||||||
|
|
||||||
|
for action in Action.objects.all():
|
||||||
|
action.verb = pattern.sub(
|
||||||
|
lambda x: known_namespaces[x.group()], action.verb
|
||||||
|
)
|
||||||
|
action.save()
|
||||||
|
|
||||||
|
|
||||||
|
def revert_event_types_names(apps, schema_editor):
|
||||||
|
Action = apps.get_model('actstream', 'Action')
|
||||||
|
StoredEventType = apps.get_model('events', 'StoredEventType')
|
||||||
|
|
||||||
|
known_namespaces = {
|
||||||
|
'documents\.': 'documents_',
|
||||||
|
'checkouts\.': 'checkouts_',
|
||||||
|
'document_comments\.': 'document_comment_',
|
||||||
|
'document_parsing\.': 'parsing_document_',
|
||||||
|
'ocr\.': 'ocr_',
|
||||||
|
'tags\.': 'tag_',
|
||||||
|
}
|
||||||
|
|
||||||
|
pattern = re.compile('|'.join(known_namespaces.keys()))
|
||||||
|
|
||||||
|
for event_type in StoredEventType.objects.all():
|
||||||
|
old_name = event_type.name
|
||||||
|
new_name = pattern.sub(
|
||||||
|
lambda x: known_namespaces[x.group().replace('.', '\\.')],
|
||||||
|
event_type.name
|
||||||
|
)
|
||||||
|
event_type.name = new_name
|
||||||
|
if old_name == new_name:
|
||||||
|
event_type.delete()
|
||||||
|
else:
|
||||||
|
event_type.save()
|
||||||
|
|
||||||
|
for action in Action.objects.all():
|
||||||
|
new_name = pattern.sub(
|
||||||
|
lambda x: known_namespaces[x.group().replace('.', '\\.')],
|
||||||
|
action.verb
|
||||||
|
)
|
||||||
|
action.verb = new_name
|
||||||
|
action.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('events', '0004_auto_20170731_0423'),
|
||||||
|
('actstream', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(
|
||||||
|
code=update_event_types_names,
|
||||||
|
reverse_code=revert_event_types_names
|
||||||
|
),
|
||||||
|
]
|
||||||
33
mayan/apps/events/migrations/0006_objecteventsubscription.py
Normal file
33
mayan/apps/events/migrations/0006_objecteventsubscription.py
Normal 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',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,28 +1,111 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.encoding import python_2_unicode_compatible
|
from django.utils.encoding import force_text, python_2_unicode_compatible
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from .classes import Event
|
from actstream.models import Action
|
||||||
|
|
||||||
|
from .classes import EventType
|
||||||
|
from .managers import (
|
||||||
|
EventSubscriptionManager, ObjectEventSubscriptionManager
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@python_2_unicode_compatible
|
@python_2_unicode_compatible
|
||||||
class EventType(models.Model):
|
class StoredEventType(models.Model):
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
max_length=64, unique=True, verbose_name=_('Name')
|
max_length=64, unique=True, verbose_name=_('Name')
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _('Event type')
|
verbose_name = _('Stored event type')
|
||||||
verbose_name_plural = _('Event types')
|
verbose_name_plural = _('Stored event types')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.get_class().label
|
return force_text(self.get_class())
|
||||||
|
|
||||||
def get_class(self):
|
def get_class(self):
|
||||||
return Event.get(name=self.name)
|
return EventType.get(name=self.name)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def label(self):
|
def label(self):
|
||||||
return self.get_class().label
|
return self.get_class().label
|
||||||
|
|
||||||
|
@property
|
||||||
|
def namespace(self):
|
||||||
|
return self.get_class().namespace
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class EventSubscription(models.Model):
|
||||||
|
user = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL, db_index=True, on_delete=models.CASCADE,
|
||||||
|
related_name='event_subscriptions', verbose_name=_('User')
|
||||||
|
)
|
||||||
|
stored_event_type = models.ForeignKey(
|
||||||
|
StoredEventType, on_delete=models.CASCADE,
|
||||||
|
related_name='event_subscriptions', verbose_name=_('Event type')
|
||||||
|
)
|
||||||
|
|
||||||
|
objects = EventSubscriptionManager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Event subscription')
|
||||||
|
verbose_name_plural = _('Event subscriptions')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return force_text(self.stored_event_type)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class Notification(models.Model):
|
||||||
|
user = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL, db_index=True, on_delete=models.CASCADE,
|
||||||
|
related_name='notifications', verbose_name=_('User')
|
||||||
|
)
|
||||||
|
action = models.ForeignKey(
|
||||||
|
Action, on_delete=models.CASCADE, related_name='notifications',
|
||||||
|
verbose_name=_('Action')
|
||||||
|
)
|
||||||
|
read = models.BooleanField(default=False, verbose_name=_('Read'))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ('-action__timestamp',)
|
||||||
|
verbose_name = _('Notification')
|
||||||
|
verbose_name_plural = _('Notifications')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return force_text(self.action)
|
||||||
|
|
||||||
|
|
||||||
|
@python_2_unicode_compatible
|
||||||
|
class ObjectEventSubscription(models.Model):
|
||||||
|
content_type = models.ForeignKey(
|
||||||
|
ContentType, on_delete=models.CASCADE,
|
||||||
|
)
|
||||||
|
object_id = models.PositiveIntegerField()
|
||||||
|
content_object = GenericForeignKey(
|
||||||
|
ct_field='content_type',
|
||||||
|
fk_field='object_id',
|
||||||
|
)
|
||||||
|
user = models.ForeignKey(
|
||||||
|
settings.AUTH_USER_MODEL, db_index=True, on_delete=models.CASCADE,
|
||||||
|
related_name='object_subscriptions', verbose_name=_('User')
|
||||||
|
)
|
||||||
|
stored_event_type = models.ForeignKey(
|
||||||
|
StoredEventType, on_delete=models.CASCADE,
|
||||||
|
related_name='object_subscriptions', verbose_name=_('Event type')
|
||||||
|
)
|
||||||
|
|
||||||
|
objects = ObjectEventSubscriptionManager()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _('Object event subscription')
|
||||||
|
verbose_name_plural = _('Object event subscriptions')
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return force_text(self.stored_event_type)
|
||||||
|
|||||||
@@ -4,30 +4,59 @@ from django.utils.six import string_types
|
|||||||
|
|
||||||
from actstream.models import Action
|
from actstream.models import Action
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
from common.serializers import ContentTypeSerializer
|
from common.serializers import ContentTypeSerializer
|
||||||
from rest_api.fields import DynamicSerializerField
|
from rest_api.fields import DynamicSerializerField
|
||||||
|
from user_management.serializers import UserSerializer
|
||||||
|
|
||||||
from .classes import Event
|
from .classes import EventType
|
||||||
from .models import EventType
|
from .models import Notification, StoredEventType
|
||||||
|
|
||||||
|
|
||||||
|
class EventTypeNamespaceSerializer(serializers.Serializer):
|
||||||
|
label = serializers.CharField()
|
||||||
|
name = serializers.CharField()
|
||||||
|
url = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
event_types_url = serializers.HyperlinkedIdentityField(
|
||||||
|
lookup_field='name',
|
||||||
|
view_name='rest_api:event-type-namespace-event-type-list',
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_url(self, instance):
|
||||||
|
return reverse(
|
||||||
|
'rest_api:event-type-namespace-detail', args=(
|
||||||
|
instance.name,
|
||||||
|
), request=self.context['request'], format=self.context['format']
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class EventTypeSerializer(serializers.Serializer):
|
class EventTypeSerializer(serializers.Serializer):
|
||||||
label = serializers.CharField()
|
label = serializers.CharField()
|
||||||
name = serializers.CharField()
|
name = serializers.CharField()
|
||||||
|
id = serializers.CharField()
|
||||||
|
event_type_namespace_url = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
def get_event_type_namespace_url(self, instance):
|
||||||
|
return reverse(
|
||||||
|
'rest_api:event-type-namespace-detail', args=(
|
||||||
|
instance.namespace.name,
|
||||||
|
), request=self.context['request'], format=self.context['format']
|
||||||
|
)
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
if isinstance(instance, Event):
|
if isinstance(instance, EventType):
|
||||||
return super(EventTypeSerializer, self).to_representation(
|
return super(EventTypeSerializer, self).to_representation(
|
||||||
instance
|
instance
|
||||||
)
|
)
|
||||||
elif isinstance(instance, EventType):
|
elif isinstance(instance, StoredEventType):
|
||||||
return super(EventTypeSerializer, self).to_representation(
|
return super(EventTypeSerializer, self).to_representation(
|
||||||
instance.get_class()
|
instance.get_class()
|
||||||
)
|
)
|
||||||
elif isinstance(instance, string_types):
|
elif isinstance(instance, string_types):
|
||||||
return super(EventTypeSerializer, self).to_representation(
|
return super(EventTypeSerializer, self).to_representation(
|
||||||
Event.get(name=instance)
|
EventType.get(name=instance)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -43,3 +72,12 @@ class EventSerializer(serializers.ModelSerializer):
|
|||||||
'action_object_content_type', 'action_object_object_id'
|
'action_object_content_type', 'action_object_object_id'
|
||||||
)
|
)
|
||||||
model = Action
|
model = Action
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationSerializer(serializers.ModelSerializer):
|
||||||
|
user = UserSerializer(read_only=True)
|
||||||
|
action = EventSerializer(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
fields = ('action', 'read', 'user')
|
||||||
|
model = Notification
|
||||||
|
|||||||
@@ -3,9 +3,15 @@ from __future__ import unicode_literals
|
|||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
|
|
||||||
from .api_views import (
|
from .api_views import (
|
||||||
APIEventListView, APIEventTypeListView, APIObjectEventListView
|
APIEventListView, APIEventTypeListView, APIEventTypeNamespaceDetailView,
|
||||||
|
APIEventTypeNamespaceEventTypeListView, APIEventTypeNamespaceListView,
|
||||||
|
APINotificationListView, APIObjectEventListView
|
||||||
|
)
|
||||||
|
from .views import (
|
||||||
|
EventListView, EventTypeSubscriptionListView, NotificationListView,
|
||||||
|
NotificationMarkRead, NotificationMarkReadAll, ObjectEventListView,
|
||||||
|
ObjectEventTypeSubscriptionListView, VerbEventListView
|
||||||
)
|
)
|
||||||
from .views import EventListView, ObjectEventListView, VerbEventListView
|
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^all/$', EventListView.as_view(), name='events_list'),
|
url(r'^all/$', EventListView.as_view(), name='events_list'),
|
||||||
@@ -14,16 +20,60 @@ urlpatterns = [
|
|||||||
ObjectEventListView.as_view(), name='events_for_object'
|
ObjectEventListView.as_view(), name='events_for_object'
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r'^by_verb/(?P<verb>[\w\-]+)/$', VerbEventListView.as_view(),
|
r'^by_verb/(?P<verb>[\w\-\.]+)/$', VerbEventListView.as_view(),
|
||||||
name='events_by_verb'
|
name='events_by_verb'
|
||||||
),
|
),
|
||||||
|
url(
|
||||||
|
r'^notifications/(?P<pk>\d+)/mark_read/$',
|
||||||
|
NotificationMarkRead.as_view(), name='notification_mark_read'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^notifications/all/mark_read/$',
|
||||||
|
NotificationMarkReadAll.as_view(), name='notification_mark_read_all'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^user/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/subscriptions/$',
|
||||||
|
ObjectEventTypeSubscriptionListView.as_view(),
|
||||||
|
name='object_event_types_user_subcriptions_list'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^user/event_types/subscriptions/$',
|
||||||
|
EventTypeSubscriptionListView.as_view(),
|
||||||
|
name='event_types_user_subcriptions_list'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^user/notifications/$',
|
||||||
|
NotificationListView.as_view(),
|
||||||
|
name='user_notifications_list'
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
api_urls = [
|
api_urls = [
|
||||||
url(r'^types/$', APIEventTypeListView.as_view(), name='event-type-list'),
|
url(
|
||||||
|
r'^event_type_namespaces/(?P<name>[-\w]+)/$',
|
||||||
|
APIEventTypeNamespaceDetailView.as_view(),
|
||||||
|
name='event-type-namespace-detail'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^event_type_namespaces/(?P<name>[-\w]+)/event_types/$',
|
||||||
|
APIEventTypeNamespaceEventTypeListView.as_view(),
|
||||||
|
name='event-type-namespace-event-type-list'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^event_type_namespaces/$', APIEventTypeNamespaceListView.as_view(),
|
||||||
|
name='event-type-namespace-list'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^event_types/$', APIEventTypeListView.as_view(),
|
||||||
|
name='event-type-list'
|
||||||
|
),
|
||||||
url(r'^events/$', APIEventListView.as_view(), name='event-list'),
|
url(r'^events/$', APIEventListView.as_view(), name='event-list'),
|
||||||
url(
|
url(
|
||||||
r'^object/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/events/$',
|
r'^notifications/$', APINotificationListView.as_view(),
|
||||||
|
name='notification-list'
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r'^objects/(?P<app_label>[-\w]+)/(?P<model>[-\w]+)/(?P<object_id>\d+)/events/$',
|
||||||
APIObjectEventListView.as_view(), name='object-event-list'
|
APIObjectEventListView.as_view(), name='object-event-list'
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
from __future__ import absolute_import, unicode_literals
|
from __future__ import absolute_import, unicode_literals
|
||||||
|
|
||||||
|
from django.contrib import messages
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.http import Http404
|
from django.http import Http404, HttpResponseRedirect
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from actstream.models import Action, any_stream
|
from actstream.models import Action, any_stream
|
||||||
|
|
||||||
from acls.models import AccessControlList
|
from acls.models import AccessControlList
|
||||||
|
from common.generics import FormView, SimpleView
|
||||||
from common.utils import encapsulate
|
from common.utils import encapsulate
|
||||||
from common.views import SingleObjectListView
|
from common.views import SingleObjectListView
|
||||||
|
|
||||||
from .classes import Event
|
from .classes import EventType, ModelEventType
|
||||||
|
from .forms import (
|
||||||
|
EventTypeUserRelationshipFormSet, ObjectEventTypeUserRelationshipFormSet
|
||||||
|
)
|
||||||
|
from .models import StoredEventType
|
||||||
from .permissions import permission_events_view
|
from .permissions import permission_events_view
|
||||||
from .widgets import event_object_link
|
from .widgets import event_object_link
|
||||||
|
|
||||||
@@ -37,6 +44,96 @@ class EventListView(SingleObjectListView):
|
|||||||
return Action.objects.all()
|
return Action.objects.all()
|
||||||
|
|
||||||
|
|
||||||
|
class EventTypeSubscriptionListView(FormView):
|
||||||
|
form_class = EventTypeUserRelationshipFormSet
|
||||||
|
main_model = 'user'
|
||||||
|
submodel = StoredEventType
|
||||||
|
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
EventType.refresh()
|
||||||
|
return super(EventTypeSubscriptionListView, self).dispatch(*args, **kwargs)
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
try:
|
||||||
|
for instance in form:
|
||||||
|
instance.save()
|
||||||
|
except Exception as exception:
|
||||||
|
messages.error(
|
||||||
|
self.request,
|
||||||
|
_('Error updating event subscription; %s') % exception
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
messages.success(
|
||||||
|
self.request, _('Event subscriptions updated successfully')
|
||||||
|
)
|
||||||
|
|
||||||
|
return super(
|
||||||
|
EventTypeSubscriptionListView, self
|
||||||
|
).form_valid(form=form)
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
return self.request.user
|
||||||
|
|
||||||
|
def get_extra_context(self):
|
||||||
|
return {
|
||||||
|
'form_display_mode_table': True,
|
||||||
|
'object': self.get_object(),
|
||||||
|
'title': _(
|
||||||
|
'Event subscriptions'
|
||||||
|
) % self.get_object()
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
obj = self.get_object()
|
||||||
|
initial = []
|
||||||
|
|
||||||
|
for element in self.get_queryset():
|
||||||
|
initial.append({
|
||||||
|
'user': obj,
|
||||||
|
'main_model': self.main_model,
|
||||||
|
'stored_event_type': element,
|
||||||
|
})
|
||||||
|
return initial
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
# Return the queryset by name from the sorted list of the class
|
||||||
|
event_type_ids = [event_type.id for event_type in EventType.all()]
|
||||||
|
return self.submodel.objects.filter(name__in=event_type_ids)
|
||||||
|
|
||||||
|
def get_post_action_redirect(self):
|
||||||
|
return reverse('common:current_user_details')
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationListView(SingleObjectListView):
|
||||||
|
def get_extra_context(self):
|
||||||
|
return {
|
||||||
|
'hide_object': True,
|
||||||
|
'object': self.request.user,
|
||||||
|
'title': _('Notifications'),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_object_list(self):
|
||||||
|
return self.request.user.notifications.all()
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationMarkRead(SimpleView):
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
self.get_queryset().filter(pk=self.kwargs['pk']).update(read=True)
|
||||||
|
return HttpResponseRedirect(reverse('events:user_notifications_list'))
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.request.user.notifications.all()
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationMarkReadAll(SimpleView):
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
self.get_queryset().update(read=True)
|
||||||
|
return HttpResponseRedirect(reverse('events:user_notifications_list'))
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return self.request.user.notifications.all()
|
||||||
|
|
||||||
|
|
||||||
class ObjectEventListView(EventListView):
|
class ObjectEventListView(EventListView):
|
||||||
view_permissions = None
|
view_permissions = None
|
||||||
|
|
||||||
@@ -73,6 +170,76 @@ class ObjectEventListView(EventListView):
|
|||||||
return any_stream(self.content_object)
|
return any_stream(self.content_object)
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectEventTypeSubscriptionListView(FormView):
|
||||||
|
form_class = ObjectEventTypeUserRelationshipFormSet
|
||||||
|
|
||||||
|
def dispatch(self, *args, **kwargs):
|
||||||
|
EventType.refresh()
|
||||||
|
return super(ObjectEventTypeSubscriptionListView, self).dispatch(*args, **kwargs)
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
try:
|
||||||
|
for instance in form:
|
||||||
|
instance.save()
|
||||||
|
except Exception as exception:
|
||||||
|
messages.error(
|
||||||
|
self.request,
|
||||||
|
_('Error updating object event subscription; %s') % exception
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
messages.success(
|
||||||
|
self.request, _('Object event subscriptions updated successfully')
|
||||||
|
)
|
||||||
|
|
||||||
|
return super(
|
||||||
|
ObjectEventTypeSubscriptionListView, self
|
||||||
|
).form_valid(form=form)
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
object_content_type = get_object_or_404(
|
||||||
|
ContentType, app_label=self.kwargs['app_label'],
|
||||||
|
model=self.kwargs['model']
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
content_object = object_content_type.get_object_for_this_type(
|
||||||
|
pk=self.kwargs['object_id']
|
||||||
|
)
|
||||||
|
except object_content_type.model_class().DoesNotExist:
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
AccessControlList.objects.check_access(
|
||||||
|
permissions=permission_events_view, user=self.request.user,
|
||||||
|
obj=content_object
|
||||||
|
)
|
||||||
|
|
||||||
|
return content_object
|
||||||
|
|
||||||
|
def get_extra_context(self):
|
||||||
|
return {
|
||||||
|
'form_display_mode_table': True,
|
||||||
|
'object': self.get_object(),
|
||||||
|
'title': _(
|
||||||
|
'Event subscriptions for: %s'
|
||||||
|
) % self.get_object()
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_initial(self):
|
||||||
|
obj = self.get_object()
|
||||||
|
initial = []
|
||||||
|
|
||||||
|
for element in self.get_queryset():
|
||||||
|
initial.append({
|
||||||
|
'user': self.request.user,
|
||||||
|
'object': obj,
|
||||||
|
'stored_event_type': element,
|
||||||
|
})
|
||||||
|
return initial
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return ModelEventType.get_for_instance(instance=self.get_object())
|
||||||
|
|
||||||
|
|
||||||
class VerbEventListView(SingleObjectListView):
|
class VerbEventListView(SingleObjectListView):
|
||||||
def get_extra_context(self):
|
def get_extra_context(self):
|
||||||
return {
|
return {
|
||||||
@@ -87,7 +254,7 @@ class VerbEventListView(SingleObjectListView):
|
|||||||
'hide_object': True,
|
'hide_object': True,
|
||||||
'title': _(
|
'title': _(
|
||||||
'Events of type: %s'
|
'Events of type: %s'
|
||||||
) % Event.get_label(self.kwargs['verb']),
|
) % EventType.get(name=self.kwargs['verb']),
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_object_list(self):
|
def get_object_list(self):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import unicode_literals
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
from .classes import Event
|
from .classes import EventType
|
||||||
|
|
||||||
|
|
||||||
def event_object_link(entry, attribute='target'):
|
def event_object_link(entry, attribute='target'):
|
||||||
@@ -26,6 +26,6 @@ def event_type_link(entry):
|
|||||||
return mark_safe(
|
return mark_safe(
|
||||||
'<a href="%(url)s">%(label)s</a>' % {
|
'<a href="%(url)s">%(label)s</a>' % {
|
||||||
'url': reverse('events:events_by_verb', kwargs={'verb': entry.verb}),
|
'url': reverse('events:events_by_verb', kwargs={'verb': entry.verb}),
|
||||||
'label': Event.get_label(entry.verb)
|
'label': EventType.get(name=entry.verb)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,13 +2,15 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from events.classes import Event
|
from events import EventTypeNamespace
|
||||||
|
|
||||||
event_ocr_document_version_submit = Event(
|
namespace = EventTypeNamespace(name='ocr', label=_('OCR'))
|
||||||
name='ocr_document_version_submit',
|
|
||||||
label=_('Document version submitted for OCR')
|
event_ocr_document_version_submit = namespace.add_event_type(
|
||||||
|
label=_('Document version submitted for OCR'),
|
||||||
|
name='document_version_submit'
|
||||||
)
|
)
|
||||||
event_ocr_document_version_finish = Event(
|
event_ocr_document_version_finish = namespace.add_event_type(
|
||||||
name='ocr_document_version_finish',
|
label=_('Document version OCR finished'),
|
||||||
label=_('Document version OCR finished')
|
name='document_version_finish'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ class DynamicSerializerField(serializers.ReadOnlyField):
|
|||||||
for klass, serializer_class in self.serializers.items():
|
for klass, serializer_class in self.serializers.items():
|
||||||
if isinstance(value, klass):
|
if isinstance(value, klass):
|
||||||
return serializer_class(
|
return serializer_class(
|
||||||
context={'request': self.context['request']}
|
context={
|
||||||
|
'format': self.context['format'],
|
||||||
|
'request': self.context['request']
|
||||||
|
}
|
||||||
).to_representation(instance=value)
|
).to_representation(instance=value)
|
||||||
|
|
||||||
return _('Unable to find serializer class for: %s') % value
|
return _('Unable to find serializer class for: %s') % value
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ class Source(models.Model):
|
|||||||
logger.critical(
|
logger.critical(
|
||||||
'Unexpected exception while trying to create version for '
|
'Unexpected exception while trying to create version for '
|
||||||
'new document "%s" from source "%s"; %s',
|
'new document "%s" from source "%s"; %s',
|
||||||
label or file_object.name, self, exception
|
label or file_object.name, self, exception, exc_info=True
|
||||||
)
|
)
|
||||||
document.delete(to_trash=False)
|
document.delete(to_trash=False)
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ from __future__ import absolute_import, unicode_literals
|
|||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from events.classes import Event
|
from events import EventTypeNamespace
|
||||||
|
|
||||||
event_tag_attach = Event(
|
namespace = EventTypeNamespace(name='tags', label=_('Tags'))
|
||||||
name='tag_attach',
|
|
||||||
label=_('Tag attached to document')
|
event_tag_attach = namespace.add_event_type(
|
||||||
|
label=_('Tag attached to document'), name='attach'
|
||||||
)
|
)
|
||||||
event_tag_remove = Event(
|
event_tag_remove = namespace.add_event_type(
|
||||||
name='tag_remove',
|
label=_('Tag removed from document'), name='remove'
|
||||||
label=_('Tag removed from document')
|
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user