2021-07-13 14:02:44 +02:00

237 lines
9.8 KiB
Python

# This file is part of the Indico plugins.
# Copyright (C) 2002 - 2021 CERN
#
# The Indico plugins are free software; you can redistribute
# them and/or modify them under the terms of the MIT License;
# see the LICENSE file for more details.
from collections import defaultdict
from flask import g
from sqlalchemy import inspect
from indico.core import signals
from indico.core.db.sqlalchemy.links import LinkType
from indico.core.db.sqlalchemy.protection import ProtectionMode
from indico.modules.attachments.models.attachments import Attachment
from indico.modules.attachments.models.folders import AttachmentFolder
from indico.modules.categories.models.categories import Category
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.notes.models.notes import EventNote
from indico.modules.events.sessions import Session
from indico.modules.events.sessions.models.blocks import SessionBlock
from indico.modules.events.timetable.models.entries import TimetableEntryType
from indico_livesync.models.queue import ChangeType, LiveSyncQueueEntry
from indico_livesync.util import get_excluded_categories, obj_ref
def connect_signals(plugin):
# request
plugin.connect(signals.core.after_process, _apply_changes)
# moved
plugin.connect(signals.category.moved, _moved)
plugin.connect(signals.event.moved, _moved)
# created
plugin.connect(signals.event.created, _created)
plugin.connect(signals.event.restored, _restored)
plugin.connect(signals.event.contribution_created, _created)
plugin.connect(signals.event.subcontribution_created, _created)
# deleted
plugin.connect(signals.event.deleted, _deleted)
plugin.connect(signals.event.contribution_deleted, _deleted)
plugin.connect(signals.event.subcontribution_deleted, _deleted)
# updated
plugin.connect(signals.event.updated, _updated)
plugin.connect(signals.event.contribution_updated, _updated)
plugin.connect(signals.event.subcontribution_updated, _updated)
# event times
plugin.connect(signals.event.times_changed, _event_times_changed, sender=Event)
plugin.connect(signals.event.times_changed, _event_times_changed, sender=Contribution)
# location
plugin.connect(signals.event.location_changed, _location_changed, sender=Event)
plugin.connect(signals.event.location_changed, _location_changed, sender=Contribution)
plugin.connect(signals.event.location_changed, _location_changed, sender=Session)
plugin.connect(signals.event.location_changed, _session_block_location_changed, sender=SessionBlock)
# 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.acl.protection_changed, _category_protection_changed, sender=Category)
plugin.connect(signals.acl.protection_changed, _protection_changed, sender=Event)
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.entry_changed, _acl_entry_changed, sender=Category)
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)
# notes
plugin.connect(signals.event.notes.note_added, _created)
plugin.connect(signals.event.notes.note_restored, _restored)
plugin.connect(signals.event.notes.note_deleted, _deleted)
plugin.connect(signals.event.notes.note_modified, _updated)
# attachments
plugin.connect(signals.attachments.folder_deleted, _attachment_folder_deleted)
plugin.connect(signals.attachments.attachment_created, _created)
plugin.connect(signals.attachments.attachment_deleted, _attachment_deleted)
plugin.connect(signals.attachments.attachment_updated, _updated)
plugin.connect(signals.acl.protection_changed, _attachment_folder_protection_changed, sender=AttachmentFolder)
plugin.connect(signals.acl.protection_changed, _protection_changed, sender=Attachment)
plugin.connect(signals.acl.entry_changed, _attachment_folder_acl_entry_changed, sender=AttachmentFolder)
plugin.connect(signals.acl.entry_changed, _acl_entry_changed, sender=Attachment)
def _is_category_excluded(category):
excluded_categories = get_excluded_categories()
return any(c.id in excluded_categories for c in category.chain_query)
def _moved(obj, old_parent, **kwargs):
_register_change(obj, ChangeType.moved)
new_category = obj if isinstance(obj, Category) else obj.category
old_excluded = _is_category_excluded(old_parent)
new_excluded = _is_category_excluded(new_category)
if old_excluded != new_excluded:
_register_change(obj, ChangeType.unpublished if new_excluded else ChangeType.published)
if obj.is_inheriting:
# If protection is inherited, check whether it changed
category_protection = old_parent.effective_protection_mode
new_category_protection = obj.protection_parent.effective_protection_mode
# Protection of new parent is different
if category_protection != new_category_protection:
_register_change(obj, ChangeType.protection_changed)
def _created(obj, **kwargs):
if not isinstance(obj, (Event, EventNote, Attachment, Contribution, SubContribution)):
raise TypeError(f'Unexpected object: {type(obj).__name__}')
_register_change(obj, ChangeType.created)
def _restored(obj, **kwargs):
_register_change(obj, ChangeType.undeleted)
def _deleted(obj, **kwargs):
_register_deletion(obj)
def _updated(obj, **kwargs):
_register_change(obj, ChangeType.data_changed)
def _event_times_changed(sender, obj, **kwargs):
_register_change(obj, ChangeType.data_changed)
def _session_block_location_changed(sender, obj, **kwargs):
for contrib in obj.contributions:
_register_change(contrib, ChangeType.location_changed)
def _location_changed(sender, obj, **kwargs):
_register_change(obj, ChangeType.location_changed)
def _timetable_changed(entry, **kwargs):
if entry.type == TimetableEntryType.CONTRIBUTION:
_register_change(entry.object, ChangeType.data_changed)
def _category_protection_changed(sender, obj, mode, old_mode, **kwargs):
parent_mode = obj.protection_parent.effective_protection_mode if obj.protection_parent else None
if ((old_mode == ProtectionMode.inheriting and parent_mode == mode) or
(old_mode == parent_mode and mode == ProtectionMode.inheriting)):
return
_protection_changed(sender, obj, mode=mode, old_mode=old_mode, **kwargs)
def _protection_changed(sender, obj, **kwargs):
if not inspect(obj).persistent:
return
_register_change(obj, ChangeType.protection_changed)
def _acl_entry_changed(sender, obj, entry, old_data, **kwargs):
if not inspect(obj).persistent:
return
register = False
# entry deleted
if entry is None and old_data is not None:
register = True
# entry added
elif entry is not None and old_data is None:
register = True
# entry updated
elif entry is not None and old_data is not None:
old_access = bool(old_data['read_access'] or old_data['full_access'] or old_data['permissions'])
new_access = bool(entry.full_access or entry.read_access or entry.permissions)
register = old_access != new_access
if register:
_register_change(obj, ChangeType.protection_changed)
def _attachment_folder_deleted(folder, **kwargs):
if folder.link_type not in (LinkType.event, LinkType.contribution, LinkType.subcontribution):
return
for attachment in folder.attachments:
_register_deletion(attachment)
def _attachment_deleted(attachment, **kwargs):
if attachment.folder.link_type not in (LinkType.event, LinkType.contribution, LinkType.subcontribution):
return
_register_deletion(attachment)
def _attachment_folder_protection_changed(sender, obj, **kwargs):
if not inspect(obj).persistent:
return
if obj.link_type not in (LinkType.event, LinkType.contribution, LinkType.subcontribution):
return
for attachment in obj.attachments:
_register_change(attachment, ChangeType.protection_changed)
def _attachment_folder_acl_entry_changed(sender, obj, entry, old_data, **kwargs):
if not inspect(obj).persistent:
return
if obj.link_type not in (LinkType.event, LinkType.contribution, LinkType.subcontribution):
return
for attachment in obj.attachments:
_acl_entry_changed(type(attachment), attachment, entry, old_data)
def _apply_changes(sender, **kwargs):
if not hasattr(g, 'livesync_changes'):
return
excluded_categories = get_excluded_categories()
for ref, changes in g.livesync_changes.items():
LiveSyncQueueEntry.create(changes, ref, excluded_categories=excluded_categories)
def _register_deletion(obj):
_init_livesync_g()
g.livesync_changes[obj_ref(obj)].add(ChangeType.deleted)
def _register_change(obj, action):
if not isinstance(obj, Category):
event = obj.folder.event if isinstance(obj, Attachment) else obj.event
if event is None or event.is_deleted:
# When deleting an event we get data change signals afterwards. We can simple ignore them.
# Also, ACL changes during user merges might involve deleted objects which we also don't care about
return
_init_livesync_g()
g.livesync_changes[obj_ref(obj)].add(action)
def _init_livesync_g():
g.setdefault('livesync_changes', defaultdict(set))