From cec324127672b3fc7babfea088c4026bbc304a8b Mon Sep 17 00:00:00 2001 From: Adrian Moennich Date: Thu, 11 Jun 2015 15:50:40 +0200 Subject: [PATCH] Add basic event log functionality --- indico/MaKaC/conference.py | 28 +++- indico/core/signals/event/core.py | 4 + indico/modules/events/logs/__init__.py | 29 ++++- indico/modules/events/logs/models/entries.py | 145 +++++++++++++++++++++ indico/modules/events/logs/renderers.py | 67 ++++++++++ .../logs/templates/events/logs/entry_email.html | 25 ++++ .../logs/templates/events/logs/entry_simple.html | 9 ++ .../modules/events/logs/{__init__.py => util.py} | 12 +- indico/modules/users/models/users.py | 1 + ...151732_9f0a44f8035_add_event_log_entry_table.py | 39 ++++++ 10 files changed, 346 insertions(+), 13 deletions(-) create mode 100644 indico/modules/events/logs/models/entries.py create mode 100644 indico/modules/events/logs/renderers.py create mode 100644 indico/modules/events/logs/templates/events/logs/entry_email.html create mode 100644 indico/modules/events/logs/templates/events/logs/entry_simple.html copy indico/modules/events/logs/{__init__.py => util.py} (67%) create mode 100644 migrations/versions/201506151732_9f0a44f8035_add_event_log_entry_table.py diff --git a/indico/MaKaC/conference.py b/indico/MaKaC/conference.py index 4a802fbed..939efc95b 100644 --- a/indico/MaKaC/conference.py +++ b/indico/MaKaC/conference.py @@ -103,7 +103,7 @@ import zope.interface from indico.core import signals from indico.util.date_time import utc_timestamp, format_datetime from indico.core.index import IIndexableByStartDateTime, IUniqueIdProvider, Catalog -from indico.core.db import DBMgr +from indico.core.db import DBMgr, db from indico.core.db.event import SupportInfo from indico.core.config import Config from indico.util.date_time import utc_timestamp @@ -2181,6 +2181,32 @@ class Conference(CommonObjectBase, Locatable): emails = {self.getCreator().getEmail()} | {u.getEmail() for u in self.getManagerList()} return {e for e in emails if e} + def log(self, realm, kind, module, summary, user=None, type_='simple', data=None): + """Creates a new log entry for the event + + :param realm: A value from :class:`.EventLogRealm` indicating + the realm of the action. + :param kind: A value from :class:`.EventLogKind` indicating + the kind of the action that was performed. + :param module: A human-friendly string describing the module + related to the action. + :param summmary: A one-line summary describing the logged action. + :param user: The user who performed the action. + :param type_: The type of the log entry. This is used for custom + rendering of the log message/data + :param data: JSON-serializable data specific to the log type. + + In most cases the ``simple`` log type is fine. For this type, + any items from data will be shown in the detailed view of the + log entry. You may either use a dict (which will be sorted) + alphabetically or a list of ``key, value`` pairs which will + be displayed in the given order. + """ + from indico.modules.events.logs import EventLogEntry + db.session.add(EventLogEntry(event_id=int(self.id), user=user, realm=realm, kind=kind, module=module, + type=type_, summary=summary, data=data or {})) + + @staticmethod def _cmpByDate(self, toCmp): res = cmp(self.getStartDate(), toCmp.getStartDate()) diff --git a/indico/core/signals/event/core.py b/indico/core/signals/event/core.py index c3c6e3fa3..89291fbbb 100644 --- a/indico/core/signals/event/core.py +++ b/indico/core/signals/event/core.py @@ -118,3 +118,7 @@ and the following parameters are available: Should return ``True`` or ``False``. """) + +get_log_renderers = _signals.signal('get-log-renderers', """ +Expected to return `EventLogRenderer` classes. +""") diff --git a/indico/modules/events/logs/__init__.py b/indico/modules/events/logs/__init__.py index 070269118..8f91c7941 100644 --- a/indico/modules/events/logs/__init__.py +++ b/indico/modules/events/logs/__init__.py @@ -19,12 +19,35 @@ from __future__ import unicode_literals from flask import session from indico.core import signals +from indico.modules.events.logs.models.entries import EventLogRealm, EventLogKind, EventLogEntry +from indico.modules.events.logs.renderers import SimpleRenderer, EmailRenderer +from indico.modules.events.logs.util import get_log_renderers from indico.web.flask.util import url_for -from MaKaC.webinterface.wcomponents import SideMenuItem @signals.event_management.sidemenu_advanced.connect def _extend_event_management_menu(event, **kwargs): - if event.has_legacy_id: - return + from MaKaC.webinterface.wcomponents import SideMenuItem return 'logs', SideMenuItem('Logs', url_for('event_logs.index', event), visible=event.canModify(session.user)) + + +@signals.event.deleted.connect +def _event_deleted(event, **kwargs): + EventLogEntry.find(event_id=int(event.id)).delete() + + +@signals.users.merged.connect +def _merge_users(target, source, **kwargs): + EventLogEntry.find(user_id=source.id).update({EventLogEntry.user_id: target.id}) + + +@signals.event.get_log_renderers.connect +def _get_log_renderers(sender, **kwargs): + yield SimpleRenderer + yield EmailRenderer + + +@signals.app_created.connect +def _check_agreement_definitions(app, **kwargs): + # This will raise RuntimeError if the log renderer types are not unique + get_log_renderers() diff --git a/indico/modules/events/logs/models/entries.py b/indico/modules/events/logs/models/entries.py new file mode 100644 index 000000000..ddb89e19e --- /dev/null +++ b/indico/modules/events/logs/models/entries.py @@ -0,0 +1,145 @@ +# This file is part of Indico. +# Copyright (C) 2002 - 2015 European Organization for Nuclear Research (CERN). +# +# Indico is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# Indico is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Indico; if not, see . + +from __future__ import unicode_literals + +from sqlalchemy.dialects.postgresql import JSON + +from indico.core.db import db +from indico.core.db.sqlalchemy import UTCDateTime, PyIntEnum +from indico.util.date_time import now_utc +from indico.util.i18n import _ +from indico.util.string import return_ascii +from indico.util.struct.enum import TitledIntEnum, IndicoEnum + + +class EventLogRealm(TitledIntEnum): + __titles__ = (None, _('Event'), _('Management'), _('Participants'), _('Reviewing'), _('Emails')) + event = 1 + management = 2 + participants = 3 + reviewing = 4 + emails = 5 + + +class EventLogKind(int, IndicoEnum): + other = 1 + positive = 2 + change = 3 + negative = 4 + + +class EventLogEntry(db.Model): + """Log entries for events""" + __tablename__ = 'logs' + __table_args__ = {'schema': 'events'} + + #: The ID of the log entry + id = db.Column( + db.Integer, + primary_key=True + ) + #: The ID of the event + event_id = db.Column( + db.Integer, + nullable=False, + index=True + ) + #: The ID of the user associated with the entry + user_id = db.Column( + db.Integer, + db.ForeignKey('users.users.id'), + index=True, + nullable=True + ) + #: The date/time when the reminder was created + logged_dt = db.Column( + UTCDateTime, + nullable=False, + default=now_utc + ) + #: The general area of the event the entry comes from + realm = db.Column( + PyIntEnum(EventLogRealm), + nullable=False + ) + #: The general kind of operation that was performed + kind = db.Column( + PyIntEnum(EventLogKind), + nullable=False + ) + #: The module the operation was related to (does not need to match + #: something in indico.modules and should be human-friendly but not + #: translated). + module = db.Column( + db.String, + nullable=False + ) + #: The type of the log entry. This needs to match the name of a log renderer. + type = db.Column( + db.String, + nullable=False + ) + #: A short one-line description of the logged action. + #: Should not be translated! + summary = db.Column( + db.String, + nullable=False + ) + #: Type-specific data + data = db.Column( + JSON, + nullable=False + ) + + #: The user associated with the log entry + user = db.relationship( + 'User', + lazy=False, + backref=db.backref( + 'event_log_entries', + lazy='dynamic' + ) + ) + + @property + def event(self): + from MaKaC.conference import ConferenceHolder + return ConferenceHolder().getById(str(self.event_id), True) + + @event.setter + def event(self, event): + self.event_id = int(event.getId()) + + @property + def renderer(self): + from indico.modules.events.logs.util import get_log_renderers + return get_log_renderers().get(self.type) + + def render(self): + """Renders the log entry to be displayed. + + If the renderer is not available anymore, e.g. because of a + disabled plugin, ``None`` is returned. + """ + renderer = self.renderer + return renderer.render_entry(self) if renderer else None + + @return_ascii + def __repr__(self): + realm = self.realm.name if self.realm is not None else None + return ''.format(self.id, self.event_id, self.logged_dt, realm, + self.module, self.summary) diff --git a/indico/modules/events/logs/renderers.py b/indico/modules/events/logs/renderers.py new file mode 100644 index 000000000..c7a267717 --- /dev/null +++ b/indico/modules/events/logs/renderers.py @@ -0,0 +1,67 @@ +# This file is part of Indico. +# Copyright (C) 2002 - 2015 European Organization for Nuclear Research (CERN). +# +# Indico is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# Indico is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Indico; if not, see . + +from __future__ import unicode_literals + +from flask import render_template + + +class EventLogRendererBase(object): + """Base class for event log renderers.""" + + #: unique name of the log renderer (matches EventLogEntry.type) + name = None + #: plugin containing this renderer - assigned automatically + plugin = None + #: template used to render the log entry + template_name = None + + @classmethod + def render_entry(cls, entry): + """Renders the log entry row + + :param entry: A :class:`.EventLogEntry` + """ + template = '{}:{}'.format(cls.plugin.name, cls.template_name) if cls.plugin is not None else cls.template_name + return render_template(template, entry=entry, data=cls.get_data(entry)) + + @classmethod + def get_data(cls, entry): + """Returns the entry data in a format suitable for the template. + + This method may be overridden if the entry's data needs to be + preprocessed before being passed to the template. + + It MUST NOT modify `entry.data` directly. + """ + return entry.data + + +class SimpleRenderer(EventLogRendererBase): + name = 'simple' + template_name = 'events/logs/entry_simple.html' + + @classmethod + def get_data(cls, entry): + data = entry.data + if isinstance(entry.data, dict): + data = sorted(entry.data.items()) + return data + + +class EmailRenderer(EventLogRendererBase): + name = 'email' + template_name = 'events/logs/entry_email.html' diff --git a/indico/modules/events/logs/templates/events/logs/entry_email.html b/indico/modules/events/logs/templates/events/logs/entry_email.html new file mode 100644 index 000000000..4b8d5f958 --- /dev/null +++ b/indico/modules/events/logs/templates/events/logs/entry_email.html @@ -0,0 +1,25 @@ +{{ entry.summary }} +
+ {% if data.to %} +
{% trans %}To{% endtrans %}
+
{{ data.to|join(', ') }}
+ {% endif %} + {% if data.cc %} +
{% trans %}CC{% endtrans %}
+
{{ data.cc|join(', ') }}
+ {% endif %} + {% if data.bcc %} +
{% trans %}BCC{% endtrans %}
+
{{ data.bcc|join(', ') }}
+ {% endif %} +
{% trans %}Subject{% endtrans %}
+
{{ data.subject }}
+
{% trans %}Body{% endtrans %}
+
+ {% if data.content_type == 'text/plain' %} +
{{ data.body }}
+ {% else %} + {{ data.body | safe }} + {% endif %} +
+
diff --git a/indico/modules/events/logs/templates/events/logs/entry_simple.html b/indico/modules/events/logs/templates/events/logs/entry_simple.html new file mode 100644 index 000000000..efec4e214 --- /dev/null +++ b/indico/modules/events/logs/templates/events/logs/entry_simple.html @@ -0,0 +1,9 @@ +{{ entry.summary }} +{% if data %} +
+ {% for key, value in data %} +
{{ key }}
+
{{ value }}
+ {% endfor %} +
+{% endif %} diff --git a/indico/modules/events/logs/__init__.py b/indico/modules/events/logs/util.py similarity index 67% copy from indico/modules/events/logs/__init__.py copy to indico/modules/events/logs/util.py index 070269118..8266d848e 100644 --- a/indico/modules/events/logs/__init__.py +++ b/indico/modules/events/logs/util.py @@ -16,15 +16,9 @@ from __future__ import unicode_literals -from flask import session - from indico.core import signals -from indico.web.flask.util import url_for -from MaKaC.webinterface.wcomponents import SideMenuItem +from indico.util.signals import named_objects_from_signal -@signals.event_management.sidemenu_advanced.connect -def _extend_event_management_menu(event, **kwargs): - if event.has_legacy_id: - return - return 'logs', SideMenuItem('Logs', url_for('event_logs.index', event), visible=event.canModify(session.user)) +def get_log_renderers(): + return named_objects_from_signal(signals.event.get_log_renderers.send(), plugin_attr='plugin') diff --git a/indico/modules/users/models/users.py b/indico/modules/users/models/users.py index bdb7ab782..2665a8899 100644 --- a/indico/modules/users/models/users.py +++ b/indico/modules/users/models/users.py @@ -252,6 +252,7 @@ class User(db.Model): # - static_sites (StaticSite.user) # - event_reminders (EventReminder.creator) # - oauth_tokens (OAuthToken.user) + # - event_log_entries (EventLogEntry.user) @property def as_principal(self): diff --git a/migrations/versions/201506151732_9f0a44f8035_add_event_log_entry_table.py b/migrations/versions/201506151732_9f0a44f8035_add_event_log_entry_table.py new file mode 100644 index 000000000..91f1fd615 --- /dev/null +++ b/migrations/versions/201506151732_9f0a44f8035_add_event_log_entry_table.py @@ -0,0 +1,39 @@ +"""Add event log entry table + +Revision ID: 9f0a44f8035 +Revises: 48177e1c4aa4 +Create Date: 2015-06-11 14:36:32.203307 +""" + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +from indico.core.db.sqlalchemy import PyIntEnum, UTCDateTime +from indico.modules.events.logs.models.entries import EventLogKind, EventLogRealm + + +# revision identifiers, used by Alembic. +revision = '9f0a44f8035' +down_revision = '48177e1c4aa4' + + +def upgrade(): + op.create_table('logs', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('event_id', sa.Integer(), nullable=False, index=True), + sa.Column('user_id', sa.Integer(), nullable=True, index=True), + sa.Column('logged_dt', UTCDateTime, nullable=False), + sa.Column('realm', PyIntEnum(EventLogRealm), nullable=False), + sa.Column('kind', PyIntEnum(EventLogKind), nullable=False), + sa.Column('module', sa.String(), nullable=False), + sa.Column('type', sa.String(), nullable=False), + sa.Column('summary', sa.String(), nullable=False), + sa.Column('data', postgresql.JSON(), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['users.users.id']), + sa.PrimaryKeyConstraint('id'), + schema='events') + + +def downgrade(): + op.drop_table('logs', schema='events') -- 2.11.4.GIT