diff --git a/livesync/indico_livesync/handler.py b/livesync/indico_livesync/handler.py index 812385c..bf82539 100644 --- a/livesync/indico_livesync/handler.py +++ b/livesync/indico_livesync/handler.py @@ -21,9 +21,14 @@ from collections import defaultdict from flask import g from indico.core import signals +from indico.core.db.sqlalchemy.protection import ProtectionMode from indico.modules.events import Event +from indico.modules.events.contributions.models.contributions import Contribution +from indico.modules.events.contributions.models.subcontributions import SubContribution +from indico.modules.events.sessions import Session +from indico.util.event import unify_event_args from MaKaC.accessControl import AccessController -from MaKaC.conference import ConferenceHolder, Conference, Contribution, SubContribution, Category, Session +from MaKaC.conference import Category, ConferenceHolder, Conference from indico_livesync.models.queue import LiveSyncQueueEntry, ChangeType from indico_livesync.util import obj_ref, is_ref_excluded @@ -37,30 +42,34 @@ def connect_signals(plugin): plugin.connect(signals.category.moved, _moved) plugin.connect(signals.event.moved, _moved) # created - plugin.connect(signals.category.created, _created) plugin.connect(signals.event.created, _created) plugin.connect(signals.event.contribution_created, _created) plugin.connect(signals.event.subcontribution_created, _created) # deleted - plugin.connect(signals.category.deleted, _deleted) plugin.connect(signals.event.deleted, _deleted) plugin.connect(signals.event.contribution_deleted, _deleted) plugin.connect(signals.event.subcontribution_deleted, _deleted) - # data - plugin.connect(signals.category.data_changed, _data_changed) - plugin.connect(signals.event.data_changed, _data_changed) - plugin.connect(signals.event.contribution_data_changed, _data_changed) - plugin.connect(signals.event.subcontribution_data_changed, _data_changed) + # updated + plugin.connect(signals.event.data_changed, _updated) + plugin.connect(signals.event.contribution_updated, _updated) + plugin.connect(signals.event.subcontribution_updated, _updated) + # timetable + plugin.connect(signals.event.timetable_entry_created, _timetable_changed) + plugin.connect(signals.event.timetable_entry_updated, _timetable_changed) + plugin.connect(signals.event.timetable_entry_deleted, _timetable_changed) # protection - plugin.connect(signals.category.protection_changed, _protection_changed) - plugin.connect(signals.event.protection_changed, _protection_changed) - plugin.connect(signals.event.contribution_protection_changed, _protection_changed) + plugin.connect(signals.category.protection_changed, _protection_changed_legacy) + plugin.connect(signals.event.protection_changed, _protection_changed_legacy) + plugin.connect(signals.acl.protection_changed, _protection_changed, sender=Session) + plugin.connect(signals.acl.protection_changed, _protection_changed, sender=Contribution) # ACLs - plugin.connect(signals.acl.access_granted, _acl_changed) - plugin.connect(signals.acl.access_revoked, _acl_changed) - plugin.connect(signals.acl.modification_granted, _acl_changed) - plugin.connect(signals.acl.modification_revoked, _acl_changed) - plugin.connect(signals.acl.entry_changed, _event_acl_changed, sender=Event) + plugin.connect(signals.acl.access_granted, _acl_changed_legacy) + plugin.connect(signals.acl.access_revoked, _acl_changed_legacy) + plugin.connect(signals.acl.modification_granted, _acl_changed_legacy) + plugin.connect(signals.acl.modification_revoked, _acl_changed_legacy) + plugin.connect(signals.acl.entry_changed, _acl_entry_changed, sender=Event) + plugin.connect(signals.acl.entry_changed, _acl_entry_changed, sender=Session) + plugin.connect(signals.acl.entry_changed, _acl_entry_changed, sender=Contribution) # domain access plugin.connect(signals.category.domain_access_granted, _domain_changed) plugin.connect(signals.category.domain_access_revoked, _domain_changed) @@ -86,33 +95,59 @@ def _moved(obj, old_parent, new_parent, **kwargs): _register_change(obj, ChangeType.protection_changed) -def _created(obj, parent, **kwargs): - _register_change(parent, ChangeType.data_changed) +def _created(obj, **kwargs): + if isinstance(obj, Event): + parent = None + elif isinstance(obj, Contribution): + parent = obj.event_new + elif isinstance(obj, SubContribution): + parent = obj.contribution + else: + raise TypeError('Unexpected object: {}'.format(type(obj).__name__)) + if parent: + _register_change(parent, ChangeType.data_changed) _register_change(obj, ChangeType.created) def _deleted(obj, **kwargs): - parent = kwargs.pop('parent', None) - _register_deletion(obj, parent) + _register_deletion(obj) -def _data_changed(obj, **kwargs): +def _updated(obj, **kwargs): _register_change(obj, ChangeType.data_changed) -def _protection_changed(obj, old, new, **kwargs): +def _timetable_changed(entry, **kwargs): + _register_change(entry.event_new, ChangeType.data_changed) + + +def _protection_changed_legacy(obj, old, new, **kwargs): if new == 0: # inheriting new = 1 if obj.isProtected() else -1 if old != new: _register_change(obj, ChangeType.protection_changed) -def _acl_changed(obj, principal, **kwargs): +def _protection_changed(sender, obj, **kwargs): + if isinstance(obj, Session): + _register_change(obj.event_new, ChangeType.protection_changed) + else: + _register_change(obj, ChangeType.protection_changed) + + +def _acl_changed_legacy(obj, **kwargs): _handle_acl_change(obj) -def _event_acl_changed(sender, obj, **kwargs): - _register_change(obj.as_legacy, ChangeType.protection_changed) +def _acl_entry_changed(sender, obj, **kwargs): + if isinstance(obj, Session): + # if a session acl is changed we need to update all inheriting + # contributions in that session + for contrib in obj.contributions: + if contrib.protection_mode == ProtectionMode.inheriting: + _register_change(contrib, ChangeType.protection_changed) + else: + _register_change(obj, ChangeType.protection_changed) def _domain_changed(obj, **kwargs): @@ -120,18 +155,13 @@ def _domain_changed(obj, **kwargs): def _note_changed(note, **kwargs): - obj = note.linked_object - if isinstance(obj, Session): - obj = ConferenceHolder().getById(note.event_id) - _register_change(obj, ChangeType.data_changed) + _register_change(note.object, ChangeType.data_changed) def _attachment_changed(attachment_or_folder, **kwargs): folder = getattr(attachment_or_folder, 'folder', attachment_or_folder) - obj = folder.linked_object - if isinstance(obj, Session): - obj = ConferenceHolder().getById(folder.event_id) - _register_change(obj, ChangeType.data_changed) + if not isinstance(folder.object, Category): + _register_change(folder.object.event_new, ChangeType.data_changed) def _apply_changes(sender, **kwargs): @@ -149,32 +179,28 @@ def _clear_changes(sender, **kwargs): del g.livesync_changes -def _handle_acl_change(obj, child=False): - if isinstance(obj, (Conference, Contribution, SubContribution, Category)): - # if it was a child, emit data_changed instead - if child: +def _handle_acl_change(obj): + if isinstance(obj, Category): + _register_change(obj, ChangeType.protection_changed) + elif isinstance(obj, Conference): + if obj.getOwner(): _register_change(obj, ChangeType.data_changed) - else: - _register_change(obj, ChangeType.protection_changed) - elif isinstance(obj, Session): - owner = obj.getOwner() - if owner: - _register_change(owner, ChangeType.data_changed) elif isinstance(obj, AccessController): - _handle_acl_change(obj.getOwner(), child=False) + _handle_acl_change(obj.getOwner()) else: - _handle_acl_change(obj.getOwner(), child=True) + raise TypeError('Unexpected object: {}'.format(type(obj).__name__)) -def _register_deletion(obj, parent): +def _register_deletion(obj): _init_livesync_g() - g.livesync_changes[obj_ref(obj, parent)].add(ChangeType.deleted) + g.livesync_changes[obj_ref(obj)].add(ChangeType.deleted) +@unify_event_args def _register_change(obj, action): if not isinstance(obj, Category): - event = obj.getConference() - if event is None or ConferenceHolder().getById(event.id, True) is None or event.getOwner() is None: + event = obj.event_new + if event is None or event.is_deleted or event.as_legacy is None or event.as_legacy.getOwner() is None: # When deleting an event we get data change signals afterwards. We can simple ignore them. # When moving an event it's even worse, we get a data change notification in the middle of the move while # the event has no category... @@ -185,5 +211,4 @@ def _register_change(obj, action): def _init_livesync_g(): - if not hasattr(g, 'livesync_changes'): - g.livesync_changes = defaultdict(set) + g.setdefault('livesync_changes', defaultdict(set)) diff --git a/livesync/indico_livesync/marcxml.py b/livesync/indico_livesync/marcxml.py index be99d72..73d045d 100644 --- a/livesync/indico_livesync/marcxml.py +++ b/livesync/indico_livesync/marcxml.py @@ -18,12 +18,14 @@ from __future__ import unicode_literals from flask_pluginengine import current_plugin +from indico.modules.users import User from MaKaC.accessControl import AccessWrapper from MaKaC.common.output import outputGenerator from MaKaC.common.xmlGen import XMLGen -from indico.modules.users import User +from MaKaC.conference import Category from indico_livesync import SimpleChange +from indico_livesync.models.queue import EntryType from indico_livesync.util import make_compound_id, obj_deref, obj_ref @@ -74,19 +76,19 @@ class MARCXMLGenerator: xg.closeTag(b'datafield') xg.closeTag(b'record') self.xml_generator.xml += xg.xml - elif ref['type'] in {'event', 'contribution', 'subcontribution'}: + elif ref['type'] in {EntryType.event, EntryType.contribution, EntryType.subcontribution}: obj = obj_deref(ref) if obj is None: raise ValueError('Cannot add deleted object') - elif not obj.getOwner(): + elif isinstance(obj, Category) and not obj.getOwner(): raise ValueError('Cannot add object without owner: {}'.format(obj)) - if ref['type'] == 'event': + if ref['type'] == EntryType.event: self.xml_generator.xml += self._event_to_marcxml(obj) - elif ref['type'] == 'contribution': + elif ref['type'] == EntryType.contribution: self.xml_generator.xml += self._contrib_to_marcxml(obj) - elif ref['type'] == 'subcontribution': + elif ref['type'] == EntryType.subcontribution: self.xml_generator.xml += self._subcontrib_to_marcxml(obj) - elif ref['type'] == 'category': + elif ref['type'] == EntryType.category: pass # we don't send category updates else: raise ValueError('unknown object ref: {}'.format(ref['type'])) diff --git a/livesync/indico_livesync/models/queue.py b/livesync/indico_livesync/models/queue.py index 97df892..5c5eccd 100644 --- a/livesync/indico_livesync/models/queue.py +++ b/livesync/indico_livesync/models/queue.py @@ -25,7 +25,7 @@ from indico.util.struct.enum import IndicoEnum from MaKaC.conference import CategoryManager from indico_livesync.models.agents import LiveSyncAgent -from indico_livesync.util import obj_deref +from indico_livesync.util import obj_deref, obj_ref class ChangeType(int, IndicoEnum): @@ -191,7 +191,7 @@ class LiveSyncQueueEntry(db.Model): @property def object_ref(self): """Returns the reference of the changed object""" - return ImmutableDict(type=self.type.name, category_id=self.category_id, event_id=self.event_id, + return ImmutableDict(type=self.type, category_id=self.category_id, event_id=self.event_id, contrib_id=self.contrib_id, subcontrib_id=self.subcontrib_id) @return_ascii @@ -203,18 +203,18 @@ class LiveSyncQueueEntry(db.Model): return format_repr(self, 'agent', 'id', 'type', 'change', _text=ref_repr) @classmethod - def create(cls, changes, obj_ref): + def create(cls, changes, ref): """Creates a new change in all queues :param changes: the change types, an iterable containing :class:`ChangeType` - :param obj_ref: the object reference (returned by `obj_ref`) + :param ref: the object reference (returned by `obj_ref`) of the changed object """ - obj_ref = dict(obj_ref) + ref = dict(ref) for agent in LiveSyncAgent.find(): for change in changes: - entry = cls(agent=agent, change=change, **obj_ref) + entry = cls(agent=agent, change=change, **ref) db.session.add(entry) db.session.flush() @@ -224,26 +224,14 @@ class LiveSyncQueueEntry(db.Model): The only field of the yielded items that should be used are `type`, `object` and `object_ref`. """ - if self.type not in {'category', 'event', 'contribution'}: + if self.type not in {EntryType.category, EntryType.event, EntryType.contribution}: return - data = {'change': self.change, - 'category_id': self.category_id, 'event_id': self.event_id, - 'contrib_id': self.contrib_id, 'subcontrib_id': self.subcontrib_id} - if self.type == 'category': + if self.type == EntryType.category: for event in self.object.iterAllConferences(): - new_data = dict(data) - new_data['type'] = 'event' - new_data['event_id'] = event.getId() - yield LiveSyncQueueEntry(**new_data) - elif self.type == 'event': - for contrib in self.object.iterContributions(): - new_data = dict(data) - new_data['type'] = 'contribution' - new_data['contrib_id'] = contrib.getId() - yield LiveSyncQueueEntry(**new_data) - elif self.type == 'contribution': - for subcontrib in self.object.iterSubContributions(): - new_data = dict(data) - new_data['type'] = 'subcontribution' - new_data['subcontrib_id'] = subcontrib.getId() - yield LiveSyncQueueEntry(**new_data) + yield LiveSyncQueueEntry(change=self.change, **obj_ref(event)) + elif self.type == EntryType.event: + for contrib in self.object.contributions: + yield LiveSyncQueueEntry(change=self.change, **obj_ref(contrib)) + elif self.type == EntryType.contribution: + for subcontrib in self.object.subcontributions: + yield LiveSyncQueueEntry(change=self.change, **obj_ref(subcontrib)) diff --git a/livesync/indico_livesync/simplify.py b/livesync/indico_livesync/simplify.py index d7f108a..4c7c4ee 100644 --- a/livesync/indico_livesync/simplify.py +++ b/livesync/indico_livesync/simplify.py @@ -20,7 +20,7 @@ from collections import defaultdict from indico.util.struct.enum import IndicoEnum -from indico_livesync.models.queue import ChangeType +from indico_livesync.models.queue import ChangeType, EntryType class SimpleChange(int, IndicoEnum): @@ -43,17 +43,17 @@ def process_records(records): # Probably they are updates for objects that got deleted afterwards. continue if record.change == ChangeType.created: - if record.type != 'category': - changes[record.object_ref] |= SimpleChange.created + assert record.type != EntryType.category + changes[record.object_ref] |= SimpleChange.created elif record.change == ChangeType.deleted: - if record.type != 'category': - changes[record.object_ref] |= SimpleChange.deleted + assert record.type != EntryType.category + changes[record.object_ref] |= SimpleChange.deleted elif record.change in {ChangeType.moved, ChangeType.protection_changed}: for ref in _cascade(record): changes[ref] |= SimpleChange.updated elif record.change == ChangeType.data_changed: - if record.type != 'category': - changes[record.object_ref] |= SimpleChange.updated + assert record.type != EntryType.category + changes[record.object_ref] |= SimpleChange.updated return changes diff --git a/livesync/indico_livesync/util.py b/livesync/indico_livesync/util.py index abcd92c..d1aee1a 100644 --- a/livesync/indico_livesync/util.py +++ b/livesync/indico_livesync/util.py @@ -20,26 +20,27 @@ from datetime import timedelta from werkzeug.datastructures import ImmutableDict +from indico.modules.events import Event +from indico.modules.events.contributions.models.contributions import Contribution +from indico.modules.events.contributions.models.subcontributions import SubContribution from indico.util.caching import memoize_request from indico.util.date_time import now_utc -from MaKaC.conference import Conference, Contribution, SubContribution, Category, CategoryManager, ConferenceHolder +from MaKaC.conference import Category, CategoryManager, Conference -def obj_ref(obj, parent=None): +def obj_ref(obj): """Returns a tuple identifying a category/event/contrib/subcontrib""" + from indico_livesync.models.queue import EntryType if isinstance(obj, Category): - ref = {'type': 'category', 'category_id': obj.id} + ref = {'type': EntryType.category, 'category_id': obj.id} + elif isinstance(obj, Event): + ref = {'type': EntryType.event, 'event_id': obj.id} elif isinstance(obj, Conference): - ref = {'type': 'event', 'category_id': obj.getOwner().id, 'event_id': obj.id} + ref = {'type': EntryType.event, 'event_id': int(obj.id)} elif isinstance(obj, Contribution): - event = parent or obj.getConference() - ref = {'type': 'contribution', 'category_id': event.getOwner().id, 'event_id': event.id, 'contrib_id': obj.id} + ref = {'type': EntryType.contribution, 'contrib_id': obj.id} elif isinstance(obj, SubContribution): - contrib = parent or obj.getContribution() - event = contrib.getConference() - ref = {'type': 'subcontribution', - 'category_id': event.getOwner().id, - 'event_id': event.id, 'contrib_id': contrib.id, 'subcontrib_id': obj.id} + ref = {'type': EntryType.subcontribution, 'subcontrib_id': obj.id} else: raise ValueError('Unexpected object: {}'.format(obj.__class__.__name__)) return ImmutableDict(ref) @@ -47,35 +48,32 @@ def obj_ref(obj, parent=None): def obj_deref(ref): """Returns the object identified by `ref`""" - if ref['type'] == 'category': - try: - return CategoryManager().getById(ref['category_id']) - except KeyError: - return None - elif ref['type'] in {'event', 'contribution', 'subcontribution'}: - event = ConferenceHolder().getById(ref['event_id'], quiet=True) - if ref['type'] == 'event' or not event: - return event - contrib = event.getContributionById(ref['contrib_id']) - if ref['type'] == 'contribution' or not contrib: - return contrib - return contrib.getSubContributionById(ref['subcontrib_id']) + from indico_livesync.models.queue import EntryType + if ref['type'] == EntryType.category: + return CategoryManager().getById(ref['category_id'], True) + elif ref['type'] == EntryType.event: + return Event.get(ref['event_id']) + elif ref['type'] == EntryType.contribution: + return Contribution.get(ref['contrib_id']) + elif ref['type'] == EntryType.subcontribution: + return SubContribution.get(ref['subcontrib_id']) else: raise ValueError('Unexpected object type: {}'.format(ref['type'])) def make_compound_id(ref): """Returns the compound ID for the referenced object""" - if ref['type'] == 'category': + from indico_livesync.models.queue import EntryType + if ref['type'] == EntryType.category: raise ValueError('Compound IDs are not supported for categories') - elif ref['type'] == 'event': - return ref['event_id'] - elif ref['type'] == 'contribution': - return '{}.{}'.format(ref['event_id'], ref['contrib_id']) - elif ref['type'] == 'subcontribution': - return '{}.{}.{}'.format(ref['event_id'], ref['contrib_id'], ref['subcontrib_id']) - else: - raise ValueError('Unexpected object type: {}'.format(ref['type'])) + obj = obj_deref(ref) + if isinstance(obj, Event): + return unicode(obj.id) + elif isinstance(obj, Contribution): + return '{}.{}'.format(obj.event_id, obj.id) + elif isinstance(obj, SubContribution): + return '{}.{}.{}'.format(obj.contribution.event_id, obj.contribution_id, obj.id) + raise ValueError('Unexpected object type: {}'.format(ref['type'])) def clean_old_entries(): @@ -109,4 +107,9 @@ def get_excluded_categories(): def is_ref_excluded(ref): - return ref['category_id'] in get_excluded_categories() + from indico_livesync.models.queue import EntryType + if ref['type'] == EntryType.category: + return ref['category_id'] in get_excluded_categories() + else: + obj = obj_deref(ref) + return unicode(obj.event_new.category_id) in {unicode(x) for x in get_excluded_categories()}