Example #1
0
class Event(SearchableTitleMixin, DescriptionMixin, LocationMixin,
            ProtectionManagersMixin, AttachedItemsMixin, AttachedNotesMixin,
            PersonLinkDataMixin, db.Model):
    """An Indico event

    This model contains the most basic information related to an event.

    Note that the ACL is currently only used for managers but not for
    view access!
    """
    __tablename__ = 'events'
    disallowed_protection_modes = frozenset()
    inheriting_have_acl = True
    allow_access_key = True
    allow_no_access_contact = True
    location_backref_name = 'events'
    allow_location_inheritance = False
    possible_render_modes = {RenderMode.html}
    default_render_mode = RenderMode.html
    __logging_disabled = False

    ATTACHMENT_FOLDER_ID_COLUMN = 'event_id'

    @strict_classproperty
    @classmethod
    def __auto_table_args(cls):
        return (
            db.Index('ix_events_start_dt_desc', cls.start_dt.desc()),
            db.Index('ix_events_end_dt_desc', cls.end_dt.desc()),
            db.Index('ix_events_not_deleted_category', cls.is_deleted,
                     cls.category_id),
            db.Index('ix_events_not_deleted_category_dates', cls.is_deleted,
                     cls.category_id, cls.start_dt, cls.end_dt),
            db.Index('ix_uq_events_url_shortcut',
                     db.func.lower(cls.url_shortcut),
                     unique=True,
                     postgresql_where=db.text('NOT is_deleted')),
            db.CheckConstraint("category_id IS NOT NULL OR is_deleted",
                               'category_data_set'),
            db.CheckConstraint(
                "(logo IS NULL) = (logo_metadata::text = 'null')",
                'valid_logo'),
            db.CheckConstraint(
                "(stylesheet IS NULL) = (stylesheet_metadata::text = 'null')",
                'valid_stylesheet'),
            db.CheckConstraint("end_dt >= start_dt", 'valid_dates'),
            db.CheckConstraint("url_shortcut != ''", 'url_shortcut_not_empty'),
            db.CheckConstraint("cloned_from_id != id", 'not_cloned_from_self'),
            db.CheckConstraint('visibility IS NULL OR visibility >= 0',
                               'valid_visibility'), {
                                   'schema': 'events'
                               })

    @declared_attr
    def __table_args__(cls):
        return auto_table_args(cls)

    #: The ID of the event
    id = db.Column(db.Integer, primary_key=True)
    #: If the event has been deleted
    is_deleted = db.Column(db.Boolean, nullable=False, default=False)
    #: If the event is locked (read-only mode)
    is_locked = db.Column(db.Boolean, nullable=False, default=False)
    #: The ID of the user who created the event
    creator_id = db.Column(db.Integer,
                           db.ForeignKey('users.users.id'),
                           nullable=False,
                           index=True)
    #: The ID of immediate parent category of the event
    category_id = db.Column(db.Integer,
                            db.ForeignKey('categories.categories.id'),
                            nullable=True,
                            index=True)
    #: The ID of the series this events belongs to
    series_id = db.Column(db.Integer,
                          db.ForeignKey('events.series.id'),
                          nullable=True,
                          index=True)
    #: If this event was cloned, the id of the parent event
    cloned_from_id = db.Column(
        db.Integer,
        db.ForeignKey('events.events.id'),
        nullable=True,
        index=True,
    )
    #: The creation date of the event
    created_dt = db.Column(UTCDateTime,
                           nullable=False,
                           index=True,
                           default=now_utc)
    #: The start date of the event
    start_dt = db.Column(UTCDateTime, nullable=False, index=True)
    #: The end date of the event
    end_dt = db.Column(UTCDateTime, nullable=False, index=True)
    #: The timezone of the event
    timezone = db.Column(db.String, nullable=False)
    #: The type of the event
    _type = db.Column('type', PyIntEnum(EventType), nullable=False)
    #: The visibility depth in category overviews
    visibility = db.Column(db.Integer, nullable=True, default=None)
    #: A list of tags/keywords for the event
    keywords = db.Column(
        ARRAY(db.String),
        nullable=False,
        default=[],
    )
    #: The URL shortcut for the event
    url_shortcut = db.Column(db.String, nullable=True)
    #: The metadata of the logo (hash, size, filename, content_type)
    logo_metadata = db.Column(JSON, nullable=False, default=lambda: None)
    #: The logo's raw image data
    logo = db.deferred(db.Column(db.LargeBinary, nullable=True))
    #: The metadata of the stylesheet (hash, size, filename)
    stylesheet_metadata = db.Column(JSON, nullable=False, default=lambda: None)
    #: The stylesheet's raw image data
    stylesheet = db.deferred(db.Column(db.Text, nullable=True))
    #: The ID of the event's default page (conferences only)
    default_page_id = db.Column(db.Integer,
                                db.ForeignKey('events.pages.id'),
                                index=True,
                                nullable=True)
    #: The last user-friendly registration ID
    _last_friendly_registration_id = db.deferred(
        db.Column('last_friendly_registration_id',
                  db.Integer,
                  nullable=False,
                  default=0))
    #: The last user-friendly contribution ID
    _last_friendly_contribution_id = db.deferred(
        db.Column('last_friendly_contribution_id',
                  db.Integer,
                  nullable=False,
                  default=0))
    #: The last user-friendly session ID
    _last_friendly_session_id = db.deferred(
        db.Column('last_friendly_session_id',
                  db.Integer,
                  nullable=False,
                  default=0))

    #: The category containing the event
    category = db.relationship(
        'Category',
        lazy=True,
        backref=db.backref(
            'events',
            primaryjoin=
            '(Category.id == Event.category_id) & ~Event.is_deleted',
            order_by=(start_dt, id),
            lazy=True))
    #: The user who created the event
    creator = db.relationship('User',
                              lazy=True,
                              backref=db.backref('created_events',
                                                 lazy='dynamic'))
    #: The event this one was cloned from
    cloned_from = db.relationship('Event',
                                  lazy=True,
                                  remote_side='Event.id',
                                  backref=db.backref('clones',
                                                     lazy=True,
                                                     order_by=start_dt))
    #: The event's default page (conferences only)
    default_page = db.relationship(
        'EventPage',
        lazy=True,
        foreign_keys=[default_page_id],
        post_update=True,
        # don't use this backref. we just need it so SA properly NULLs
        # this column when deleting the default page
        backref=db.backref('_default_page_of_event', lazy=True))
    #: The ACL entries for the event
    acl_entries = db.relationship('EventPrincipal',
                                  backref='event',
                                  cascade='all, delete-orphan',
                                  collection_class=set)
    #: External references associated with this event
    references = db.relationship('EventReference',
                                 lazy=True,
                                 cascade='all, delete-orphan',
                                 backref=db.backref('event', lazy=True))
    #: Persons associated with this event
    person_links = db.relationship('EventPersonLink',
                                   lazy=True,
                                   cascade='all, delete-orphan',
                                   backref=db.backref('event', lazy=True))
    #: The series this event is part of
    series = db.relationship(
        'EventSeries',
        lazy=True,
        backref=db.backref(
            'events',
            lazy=True,
            order_by=(start_dt, id),
            primaryjoin=
            '(Event.series_id == EventSeries.id) & ~Event.is_deleted',
        ))

    #: Users who can review on all tracks
    global_abstract_reviewers = db.relationship(
        'User',
        secondary='events.track_abstract_reviewers',
        collection_class=set,
        lazy=True,
        backref=db.backref('global_abstract_reviewer_for_events',
                           collection_class=set,
                           lazy=True))

    #: Users who are conveners on all tracks
    global_conveners = db.relationship('User',
                                       secondary='events.track_conveners',
                                       collection_class=set,
                                       lazy=True,
                                       backref=db.backref(
                                           'global_convener_for_events',
                                           collection_class=set,
                                           lazy=True))

    # relationship backrefs:
    # - abstract_email_templates (AbstractEmailTemplate.event)
    # - abstract_review_questions (AbstractReviewQuestion.event)
    # - abstracts (Abstract.event)
    # - agreements (Agreement.event)
    # - all_attachment_folders (AttachmentFolder.event)
    # - all_legacy_attachment_folder_mappings (LegacyAttachmentFolderMapping.event)
    # - all_legacy_attachment_mappings (LegacyAttachmentMapping.event)
    # - all_notes (EventNote.event)
    # - all_vc_room_associations (VCRoomEventAssociation.event)
    # - attachment_folders (AttachmentFolder.linked_event)
    # - clones (Event.cloned_from)
    # - contribution_fields (ContributionField.event)
    # - contribution_types (ContributionType.event)
    # - contributions (Contribution.event)
    # - custom_pages (EventPage.event)
    # - designer_templates (DesignerTemplate.event)
    # - layout_images (ImageFile.event)
    # - legacy_contribution_mappings (LegacyContributionMapping.event)
    # - legacy_mapping (LegacyEventMapping.event)
    # - legacy_session_block_mappings (LegacySessionBlockMapping.event)
    # - legacy_session_mappings (LegacySessionMapping.event)
    # - legacy_subcontribution_mappings (LegacySubContributionMapping.event)
    # - log_entries (EventLogEntry.event)
    # - menu_entries (MenuEntry.event)
    # - note (EventNote.linked_event)
    # - paper_competences (PaperCompetence.event)
    # - paper_review_questions (PaperReviewQuestion.event)
    # - paper_templates (PaperTemplate.event)
    # - persons (EventPerson.event)
    # - registration_forms (RegistrationForm.event)
    # - registrations (Registration.event)
    # - reminders (EventReminder.event)
    # - requests (Request.event)
    # - reservations (Reservation.event)
    # - sessions (Session.event)
    # - settings (EventSetting.event)
    # - settings_principals (EventSettingPrincipal.event)
    # - static_list_links (StaticListLink.event)
    # - static_sites (StaticSite.event)
    # - surveys (Survey.event)
    # - timetable_entries (TimetableEntry.event)
    # - tracks (Track.event)
    # - vc_room_associations (VCRoomEventAssociation.linked_event)

    start_dt_override = _EventSettingProperty(event_core_settings,
                                              'start_dt_override')
    end_dt_override = _EventSettingProperty(event_core_settings,
                                            'end_dt_override')
    organizer_info = _EventSettingProperty(event_core_settings,
                                           'organizer_info')
    additional_info = _EventSettingProperty(event_core_settings,
                                            'additional_info')
    contact_title = _EventSettingProperty(event_contact_settings, 'title')
    contact_emails = _EventSettingProperty(event_contact_settings, 'emails')
    contact_phones = _EventSettingProperty(event_contact_settings, 'phones')

    @classmethod
    def category_chain_overlaps(cls, category_ids):
        """
        Create a filter that checks whether the event has any of the
        provided category ids in its parent chain.

        :param category_ids: A list of category ids or a single
                             category id
        """
        from indico.modules.categories import Category
        if not isinstance(category_ids, (list, tuple, set)):
            category_ids = [category_ids]
        cte = Category.get_tree_cte()
        return (cte.c.id
                == Event.category_id) & cte.c.path.overlap(category_ids)

    @classmethod
    def is_visible_in(cls, category):
        """
        Create a filter that checks whether the event is visible in
        the specified category.
        """
        cte = category.visible_categories_cte
        return (db.exists(db.select([1])).where(
            db.and_(
                cte.c.id == Event.category_id,
                db.or_(Event.visibility.is_(None),
                       Event.visibility > cte.c.level))))

    @property
    @memoize_request
    def as_legacy(self):
        """Return a legacy `Conference` object"""
        from indico.modules.events.legacy import LegacyConference
        return LegacyConference(self)

    @property
    def event(self):
        """Convenience property so all event entities have it"""
        return self

    @property
    def has_logo(self):
        return self.logo_metadata is not None

    @property
    def has_stylesheet(self):
        return self.stylesheet_metadata is not None

    @property
    def theme(self):
        from indico.modules.events.layout import layout_settings, theme_settings
        theme = layout_settings.get(self, 'timetable_theme')
        if theme and theme in theme_settings.get_themes_for(self.type):
            return theme
        else:
            return theme_settings.defaults[self.type]

    @property
    def locator(self):
        return {'confId': self.id}

    @property
    def logo_url(self):
        return url_for('event_images.logo_display',
                       self,
                       slug=self.logo_metadata['hash'])

    @property
    def participation_regform(self):
        return next(
            (form
             for form in self.registration_forms if form.is_participation),
            None)

    @property
    @memoize_request
    def published_registrations(self):
        from indico.modules.events.registration.util import get_published_registrations
        return get_published_registrations(self)

    @property
    def protection_parent(self):
        return self.category

    @property
    def start_dt_local(self):
        return self.start_dt.astimezone(self.tzinfo)

    @property
    def end_dt_local(self):
        return self.end_dt.astimezone(self.tzinfo)

    @property
    def start_dt_display(self):
        """
        The 'displayed start dt', which is usually the actual start dt,
        but may be overridden for a conference.
        """
        if self.type_ == EventType.conference and self.start_dt_override:
            return self.start_dt_override
        else:
            return self.start_dt

    @property
    def end_dt_display(self):
        """
        The 'displayed end dt', which is usually the actual end dt,
        but may be overridden for a conference.
        """
        if self.type_ == EventType.conference and self.end_dt_override:
            return self.end_dt_override
        else:
            return self.end_dt

    @property
    def type(self):
        # XXX: this should eventually be replaced with the type_
        # property returning the enum - but there are too many places
        # right now that rely on the type string
        return self.type_.name

    @hybrid_property
    def type_(self):
        return self._type

    @type_.setter
    def type_(self, value):
        old_type = self._type
        self._type = value
        if old_type is not None and old_type != value:
            signals.event.type_changed.send(self, old_type=old_type)

    @property
    def url(self):
        return url_for('events.display', self)

    @property
    def external_url(self):
        return url_for('events.display', self, _external=True)

    @property
    def short_url(self):
        id_ = self.url_shortcut or self.id
        return url_for('events.shorturl', confId=id_)

    @property
    def short_external_url(self):
        id_ = self.url_shortcut or self.id
        return url_for('events.shorturl', confId=id_, _external=True)

    @property
    def tzinfo(self):
        return pytz.timezone(self.timezone)

    @property
    def display_tzinfo(self):
        """The tzinfo of the event as preferred by the current user"""
        return get_display_tz(self, as_timezone=True)

    @property
    @contextmanager
    def logging_disabled(self):
        """Temporarily disables event logging

        This is useful when performing actions e.g. during event
        creation or at other times where adding entries to the event
        log doesn't make sense.
        """
        self.__logging_disabled = True
        try:
            yield
        finally:
            self.__logging_disabled = False

    @hybrid_method
    def happens_between(self, from_dt=None, to_dt=None):
        """Check whether the event takes place within two dates"""
        if from_dt is not None and to_dt is not None:
            # any event that takes place during the specified range
            return overlaps((self.start_dt, self.end_dt), (from_dt, to_dt),
                            inclusive=True)
        elif from_dt is not None:
            # any event that starts on/after the specified date
            return self.start_dt >= from_dt
        elif to_dt is not None:
            # any event that ends on/before the specifed date
            return self.end_dt <= to_dt
        else:
            return True

    @happens_between.expression
    def happens_between(cls, from_dt=None, to_dt=None):
        if from_dt is not None and to_dt is not None:
            # any event that takes place during the specified range
            return db_dates_overlap(cls,
                                    'start_dt',
                                    from_dt,
                                    'end_dt',
                                    to_dt,
                                    inclusive=True)
        elif from_dt is not None:
            # any event that starts on/after the specified date
            return cls.start_dt >= from_dt
        elif to_dt is not None:
            # any event that ends on/before the specifed date
            return cls.end_dt <= to_dt
        else:
            return True

    @hybrid_method
    def starts_between(self, from_dt=None, to_dt=None):
        """Check whether the event starts within two dates"""
        if from_dt is not None and to_dt is not None:
            return from_dt <= self.start_dt <= to_dt
        elif from_dt is not None:
            return self.start_dt >= from_dt
        elif to_dt is not None:
            return self.start_dt <= to_dt
        else:
            return True

    @starts_between.expression
    def starts_between(cls, from_dt=None, to_dt=None):
        if from_dt is not None and to_dt is not None:
            return cls.start_dt.between(from_dt, to_dt)
        elif from_dt is not None:
            return cls.start_dt >= from_dt
        elif to_dt is not None:
            return cls.start_dt <= to_dt
        else:
            return True

    @hybrid_method
    def ends_after(self, dt):
        """Check whether the event ends on/after the specified date"""
        return self.end_dt >= dt if dt is not None else True

    @ends_after.expression
    def ends_after(cls, dt):
        return cls.end_dt >= dt if dt is not None else True

    @hybrid_property
    def duration(self):
        return self.end_dt - self.start_dt

    def can_lock(self, user):
        """Check whether the user can lock/unlock the event"""
        return user and (user.is_admin or user == self.creator
                         or self.category.can_manage(user))

    def get_relative_event_ids(self):
        """Get the first, last, previous and next event IDs.

        Any of those values may be ``None`` if there is no matching
        event or if it would be the current event.

        :return: A dict containing ``first``, ``last``, ``prev`` and ``next``.
        """
        subquery = (select([
            Event.id,
            db.func.first_value(
                Event.id).over(order_by=(Event.start_dt,
                                         Event.id)).label('first'),
            db.func.last_value(Event.id).over(order_by=(Event.start_dt,
                                                        Event.id),
                                              range_=(None,
                                                      None)).label('last'),
            db.func.lag(Event.id).over(order_by=(Event.start_dt,
                                                 Event.id)).label('prev'),
            db.func.lead(Event.id).over(order_by=(Event.start_dt,
                                                  Event.id)).label('next')
        ]).where((Event.category_id == self.category_id)
                 & ~Event.is_deleted).alias())
        rv = (db.session.query(
            subquery.c.first, subquery.c.last, subquery.c.prev,
            subquery.c.next).filter(subquery.c.id == self.id).one()._asdict())
        if rv['first'] == self.id:
            rv['first'] = None
        if rv['last'] == self.id:
            rv['last'] = None
        return rv

    def get_verbose_title(self, show_speakers=False, show_series_pos=False):
        """Get the event title with some additional information

        :param show_speakers: Whether to prefix the title with the
                              speakers of the event.
        :param show_series_pos: Whether to suffix the title with the
                                position and total count in the event's
                                series.
        """
        title = self.title
        if show_speakers and self.person_links:
            speakers = ', '.join(
                sorted([pl.full_name for pl in self.person_links],
                       key=unicode.lower))
            title = '{}, "{}"'.format(speakers, title)
        if show_series_pos and self.series and self.series.show_sequence_in_title:
            title = '{} ({}/{})'.format(title, self.series_pos,
                                        self.series_count)
        return title

    def get_non_inheriting_objects(self):
        """Get a set of child objects that do not inherit protection"""
        return get_non_inheriting_objects(self)

    def get_contribution(self, id_):
        """Get a contribution of the event"""
        return get_related_object(self, 'contributions', {'id': id_})

    def get_session(self, id_=None, friendly_id=None):
        """Get a session of the event"""
        if friendly_id is None and id_ is not None:
            criteria = {'id': id_}
        elif id_ is None and friendly_id is not None:
            criteria = {'friendly_id': friendly_id}
        else:
            raise ValueError('Exactly one kind of id must be specified')
        return get_related_object(self, 'sessions', criteria)

    def get_session_block(self, id_, scheduled_only=False):
        """Get a session block of the event"""
        from indico.modules.events.sessions.models.blocks import SessionBlock
        query = SessionBlock.query.filter(
            SessionBlock.id == id_,
            SessionBlock.session.has(event=self, is_deleted=False))
        if scheduled_only:
            query.filter(SessionBlock.timetable_entry != None)  # noqa
        return query.first()

    def get_allowed_sender_emails(self,
                                  include_current_user=True,
                                  include_creator=True,
                                  include_managers=True,
                                  include_contact=True,
                                  include_chairs=True,
                                  extra=None):
        """
        Return the emails of people who can be used as senders (or
        rather Reply-to contacts) in emails sent from within an event.

        :param include_current_user: Whether to include the email of
                                     the currently logged-in user
        :param include_creator: Whether to include the email of the
                                event creator
        :param include_managers: Whether to include the email of all
                                 event managers
        :param include_contact: Whether to include the "event contact"
                                emails
        :param include_chairs: Whether to include the emails of event
                               chairpersons (or lecture speakers)
        :param extra: An email address that is always included, even
                      if it is not in any of the included lists.
        :return: An OrderedDict mapping emails to pretty names
        """
        emails = {}
        # Contact/Support
        if include_contact:
            for email in self.contact_emails:
                emails[email] = self.contact_title
        # Current user
        if include_current_user and has_request_context() and session.user:
            emails[session.user.email] = session.user.full_name
        # Creator
        if include_creator:
            emails[self.creator.email] = self.creator.full_name
        # Managers
        if include_managers:
            emails.update((p.principal.email, p.principal.full_name)
                          for p in self.acl_entries
                          if p.type == PrincipalType.user and p.full_access)
        # Chairs
        if include_chairs:
            emails.update((pl.email, pl.full_name) for pl in self.person_links
                          if pl.email)
        # Extra email (e.g. the current value in an object from the DB)
        if extra:
            emails.setdefault(extra, extra)
        # Sanitize and format emails
        emails = {
            to_unicode(email.strip().lower()):
            '{} <{}>'.format(to_unicode(name), to_unicode(email))
            for email, name in emails.iteritems() if email and email.strip()
        }
        own_email = session.user.email if has_request_context(
        ) and session.user else None
        return OrderedDict(
            sorted(emails.items(),
                   key=lambda x: (x[0] != own_email, x[1].lower())))

    @memoize_request
    def has_feature(self, feature):
        """Checks if a feature is enabled for the event"""
        from indico.modules.events.features.util import is_feature_enabled
        return is_feature_enabled(self, feature)

    @property
    @memoize_request
    def scheduled_notes(self):
        from indico.modules.events.notes.util import get_scheduled_notes
        return get_scheduled_notes(self)

    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 summary: 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.
        """
        if self.__logging_disabled:
            return
        entry = EventLogEntry(user=user,
                              realm=realm,
                              kind=kind,
                              module=module,
                              type=type_,
                              summary=summary,
                              data=data or {})
        self.log_entries.append(entry)

    def get_contribution_field(self, field_id):
        return next((v for v in self.contribution_fields if v.id == field_id),
                    '')

    def move_start_dt(self, start_dt):
        """Set event start_dt and adjust its timetable entries"""
        diff = start_dt - self.start_dt
        for entry in self.timetable_entries.filter(
                TimetableEntry.parent_id.is_(None)):
            new_dt = entry.start_dt + diff
            entry.move(new_dt)
        self.start_dt = start_dt

    def iter_days(self, tzinfo=None):
        start_dt = self.start_dt
        end_dt = self.end_dt
        if tzinfo:
            start_dt = start_dt.astimezone(tzinfo)
            end_dt = end_dt.astimezone(tzinfo)
        duration = (end_dt - start_dt).days
        for offset in xrange(duration + 1):
            yield (start_dt + timedelta(days=offset)).date()

    def preload_all_acl_entries(self):
        db.m.Contribution.preload_acl_entries(self)
        db.m.Session.preload_acl_entries(self)

    def move(self, category):
        old_category = self.category
        self.category = category
        db.session.flush()
        signals.event.moved.send(self, old_parent=old_category)

    def delete(self, reason, user=None):
        from indico.modules.events import logger, EventLogRealm, EventLogKind
        self.is_deleted = True
        signals.event.deleted.send(self, user=user)
        db.session.flush()
        logger.info('Event %r deleted [%s]', self, reason)
        self.log(EventLogRealm.event,
                 EventLogKind.negative,
                 'Event',
                 'Event deleted',
                 user,
                 data={'Reason': reason})

    @property
    @memoize_request
    def cfa(self):
        from indico.modules.events.abstracts.models.call_for_abstracts import CallForAbstracts
        return CallForAbstracts(self)

    @property
    @memoize_request
    def cfp(self):
        from indico.modules.events.papers.models.call_for_papers import CallForPapers
        return CallForPapers(self)

    @return_ascii
    def __repr__(self):
        return format_repr(self,
                           'id',
                           'start_dt',
                           'end_dt',
                           is_deleted=False,
                           is_locked=False,
                           _text=text_to_repr(self.title, max_length=75))
Example #2
0
class Event(ProtectionManagersMixin, db.Model):
    """An Indico event

    This model contains the most basic information related to an event.

    Note that the ACL is currently only used for managers but not for
    view access!
    """
    __tablename__ = 'events'
    __table_args__ = (
        db.CheckConstraint("(logo IS NULL) = (logo_metadata::text = 'null')",
                           'valid_logo'),
        db.CheckConstraint(
            "(stylesheet IS NULL) = (stylesheet_metadata::text = 'null')",
            'valid_stylesheet'), {
                'schema': 'events'
            })
    disallowed_protection_modes = frozenset()
    inheriting_have_acl = True
    __logging_disabled = False

    #: The ID of the event
    id = db.Column(db.Integer, primary_key=True)
    #: If the event has been deleted
    is_deleted = db.Column(db.Boolean, nullable=False, default=False)
    #: The ID of the user who created the event
    creator_id = db.Column(db.Integer,
                           db.ForeignKey('users.users.id'),
                           nullable=False,
                           index=True)
    #: The metadata of the logo (hash, size, filename, content_type)
    logo_metadata = db.Column(JSON, nullable=False, default=None)
    #: The logo's raw image data
    logo = db.deferred(db.Column(db.LargeBinary, nullable=True))
    #: The metadata of the stylesheet (hash, size, filename)
    stylesheet_metadata = db.Column(JSON, nullable=False, default=None)
    #: The stylesheet's raw image data
    stylesheet = db.deferred(db.Column(db.Text, nullable=True))
    #: The ID of the event's default page (conferences only)
    default_page_id = db.Column(db.Integer,
                                db.ForeignKey('events.pages.id'),
                                index=True,
                                nullable=True)
    #: The last user-friendly registration ID
    _last_friendly_registration_id = db.deferred(
        db.Column('last_friendly_registration_id',
                  db.Integer,
                  nullable=False,
                  default=0))

    #: The user who created the event
    creator = db.relationship('User',
                              lazy=True,
                              backref=db.backref('created_events',
                                                 lazy='dynamic'))
    #: The event's default page (conferences only)
    default_page = db.relationship(
        'EventPage',
        lazy=True,
        foreign_keys=[default_page_id],
        # don't use this backref. we just need it so SA properly NULLs
        # this column when deleting the default page
        backref=db.backref('_default_page_of_event', lazy=True))
    #: The ACL entries for the event
    acl_entries = db.relationship('EventPrincipal',
                                  backref='event_new',
                                  cascade='all, delete-orphan',
                                  collection_class=set)

    # relationship backrefs:
    # - agreements (Agreement.event_new)
    # - attachment_folders (AttachmentFolder.event_new)
    # - custom_pages (EventPage.event_new)
    # - layout_images (ImageFile.event_new)
    # - legacy_attachment_folder_mappings (LegacyAttachmentFolderMapping.event_new)
    # - legacy_attachment_mappings (LegacyAttachmentMapping.event_new)
    # - legacy_mapping (LegacyEventMapping.event_new)
    # - log_entries (EventLogEntry.event_new)
    # - menu_entries (MenuEntry.event_new)
    # - notes (EventNote.event_new)
    # - registration_forms (RegistrationForm.event_new)
    # - registrations (Registration.event_new)
    # - reminders (EventReminder.event_new)
    # - requests (Request.event_new)
    # - reservations (Reservation.event_new)
    # - settings (EventSetting.event_new)
    # - settings_principals (EventSettingPrincipal.event_new)
    # - static_sites (StaticSite.event_new)
    # - surveys (Survey.event_new)
    # - vc_room_associations (VCRoomEventAssociation.event_new)

    @property
    @memoize_request
    def as_legacy(self):
        """Returns a legacy `Conference` object (ZODB)"""
        from MaKaC.conference import ConferenceHolder
        return ConferenceHolder().getById(self.id, True)

    @property
    def protection_parent(self):
        return self.as_legacy.getOwner()

    @property
    def has_logo(self):
        return self.logo_metadata is not None

    @property
    def logo_url(self):
        return url_for('event_images.logo_display',
                       self,
                       slug=self.logo_metadata['hash'])

    @property
    def has_stylesheet(self):
        return self.stylesheet_metadata is not None

    @property
    def locator(self):
        return {'confId': self.id}

    @property
    def participation_regform(self):
        return self.registration_forms.filter_by(is_participation=True,
                                                 is_deleted=False).first()

    @property
    def title(self):
        return to_unicode(self.as_legacy.getTitle())

    @property
    def type(self):
        event_type = self.as_legacy.getType()
        if event_type == 'simple_event':
            event_type = 'lecture'
        return event_type

    @property
    @contextmanager
    def logging_disabled(self):
        """Temporarily disables event logging

        This is useful when performing actions e.g. during event
        creation or at other times where adding entries to the event
        log doesn't make sense.
        """
        self.__logging_disabled = True
        try:
            yield
        finally:
            self.__logging_disabled = False

    def can_access(self, user, allow_admin=True):
        if not allow_admin:
            raise NotImplementedError(
                'can_access(..., allow_admin=False) is unsupported until ACLs are migrated'
            )
        from MaKaC.accessControl import AccessWrapper
        return self.as_legacy.canAccess(
            AccessWrapper(user.as_avatar if user else None))

    def can_manage(self, user, role=None, allow_key=False, *args, **kwargs):
        # XXX: Remove this method once modification keys are gone!
        return (super(Event, self).can_manage(user, role, *args, **kwargs)
                or (allow_key and self.as_legacy.canKeyModify()))

    @memoize_request
    def has_feature(self, feature):
        """Checks if a feature is enabled for the event"""
        from indico.modules.events.features.util import is_feature_enabled
        return is_feature_enabled(self, feature)

    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 summary: 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.
        """
        if self.__logging_disabled:
            return
        entry = EventLogEntry(user=user,
                              realm=realm,
                              kind=kind,
                              module=module,
                              type=type_,
                              summary=summary,
                              data=data or {})
        self.log_entries.append(entry)

    @return_ascii
    def __repr__(self):
        # TODO: add self.protection_repr once we use it and the title once we store it here
        return format_repr(self, 'id', is_deleted=False)

    # TODO: Remove the next block of code once event acls (read access) are migrated
    def _fail(self, *args, **kwargs):
        raise NotImplementedError(
            'These properties are not usable until event ACLs are in the new DB'
        )

    is_public = classproperty(classmethod(_fail))
    is_inheriting = classproperty(classmethod(_fail))
    is_protected = classproperty(classmethod(_fail))
    protection_repr = property(_fail)
    del _fail
Example #3
0
class Event(DescriptionMixin, LocationMixin, ProtectionManagersMixin,
            AttachedItemsMixin, AttachedNotesMixin, PersonLinkDataMixin,
            db.Model):
    """An Indico event

    This model contains the most basic information related to an event.

    Note that the ACL is currently only used for managers but not for
    view access!
    """
    __tablename__ = 'events'
    disallowed_protection_modes = frozenset()
    inheriting_have_acl = True
    location_backref_name = 'events'
    allow_location_inheritance = False
    description_wrapper = RichMarkup
    __logging_disabled = False

    ATTACHMENT_FOLDER_ID_COLUMN = 'event_id'

    @strict_classproperty
    @classmethod
    def __auto_table_args(cls):
        return (
            db.Index(None, 'category_chain', postgresql_using='gin'),
            db.Index('ix_events_title_fts',
                     db.func.to_tsvector('simple', cls.title),
                     postgresql_using='gin'),
            db.Index('ix_events_start_dt_desc', cls.start_dt.desc()),
            db.Index('ix_events_end_dt_desc', cls.end_dt.desc()),
            db.CheckConstraint(
                "(category_id IS NOT NULL AND category_chain IS NOT NULL) OR is_deleted",
                'category_data_set'),
            db.CheckConstraint("category_id = category_chain[1]",
                               'category_id_matches_chain'),
            db.CheckConstraint(
                "category_chain[array_length(category_chain, 1)] = 0",
                'category_chain_has_root'),
            db.CheckConstraint(
                "(logo IS NULL) = (logo_metadata::text = 'null')",
                'valid_logo'),
            db.CheckConstraint(
                "(stylesheet IS NULL) = (stylesheet_metadata::text = 'null')",
                'valid_stylesheet'),
            db.CheckConstraint("end_dt >= start_dt", 'valid_dates'),
            db.CheckConstraint("title != ''", 'valid_title'),
            db.CheckConstraint("cloned_from_id != id",
                               'not_cloned_from_self'), {
                                   'schema': 'events'
                               })

    @declared_attr
    def __table_args__(cls):
        return auto_table_args(cls)

    #: The ID of the event
    id = db.Column(db.Integer, primary_key=True)
    #: If the event has been deleted
    is_deleted = db.Column(db.Boolean, nullable=False, default=False)
    #: The ID of the user who created the event
    creator_id = db.Column(db.Integer,
                           db.ForeignKey('users.users.id'),
                           nullable=False,
                           index=True)
    #: The ID of immediate parent category of the event
    category_id = db.Column(db.Integer, nullable=True, index=True)
    #: The category chain of the event (from immediate parent to root)
    category_chain = db.Column(ARRAY(db.Integer), nullable=True)
    #: If this event was cloned, the id of the parent event
    cloned_from_id = db.Column(
        db.Integer,
        db.ForeignKey('events.events.id'),
        nullable=True,
        index=True,
    )
    #: The start date of the event
    start_dt = db.Column(UTCDateTime, nullable=False, index=True)
    #: The end date of the event
    end_dt = db.Column(UTCDateTime, nullable=False, index=True)
    #: The timezone of the event
    timezone = db.Column(db.String, nullable=False)
    #: The title of the event
    title = db.Column(db.String, nullable=False)
    #: The metadata of the logo (hash, size, filename, content_type)
    logo_metadata = db.Column(JSON, nullable=False, default=None)
    #: The logo's raw image data
    logo = db.deferred(db.Column(db.LargeBinary, nullable=True))
    #: The metadata of the stylesheet (hash, size, filename)
    stylesheet_metadata = db.Column(JSON, nullable=False, default=None)
    #: The stylesheet's raw image data
    stylesheet = db.deferred(db.Column(db.Text, nullable=True))
    #: The ID of the event's default page (conferences only)
    default_page_id = db.Column(db.Integer,
                                db.ForeignKey('events.pages.id'),
                                index=True,
                                nullable=True)
    #: The last user-friendly registration ID
    _last_friendly_registration_id = db.deferred(
        db.Column('last_friendly_registration_id',
                  db.Integer,
                  nullable=False,
                  default=0))
    #: The last user-friendly contribution ID
    _last_friendly_contribution_id = db.deferred(
        db.Column('last_friendly_contribution_id',
                  db.Integer,
                  nullable=False,
                  default=0))
    #: The last user-friendly session ID
    _last_friendly_session_id = db.deferred(
        db.Column('last_friendly_session_id',
                  db.Integer,
                  nullable=False,
                  default=0))

    #: The user who created the event
    creator = db.relationship('User',
                              lazy=True,
                              backref=db.backref('created_events',
                                                 lazy='dynamic'))
    #: The event this one was cloned from
    cloned_from = db.relationship('Event',
                                  lazy=True,
                                  remote_side='Event.id',
                                  backref=db.backref('clones',
                                                     lazy=True,
                                                     order_by=start_dt))

    #: The event's default page (conferences only)
    default_page = db.relationship(
        'EventPage',
        lazy=True,
        foreign_keys=[default_page_id],
        # don't use this backref. we just need it so SA properly NULLs
        # this column when deleting the default page
        backref=db.backref('_default_page_of_event', lazy=True))
    #: The ACL entries for the event
    acl_entries = db.relationship('EventPrincipal',
                                  backref='event_new',
                                  cascade='all, delete-orphan',
                                  collection_class=set)
    #: External references associated with this event
    references = db.relationship('EventReference',
                                 lazy=True,
                                 cascade='all, delete-orphan',
                                 backref=db.backref('event_new', lazy=True))
    #: Persons associated with this event
    person_links = db.relationship('EventPersonLink',
                                   lazy=True,
                                   cascade='all, delete-orphan',
                                   backref=db.backref('event', lazy=True))

    # relationship backrefs:
    # - abstracts (Abstract.event_new)
    # - agreements (Agreement.event_new)
    # - all_attachment_folders (AttachmentFolder.event_new)
    # - all_legacy_attachment_folder_mappings (LegacyAttachmentFolderMapping.event_new)
    # - all_legacy_attachment_mappings (LegacyAttachmentMapping.event_new)
    # - all_notes (EventNote.event_new)
    # - all_vc_room_associations (VCRoomEventAssociation.event_new)
    # - attachment_folders (AttachmentFolder.linked_event)
    # - clones (Event.cloned_from)
    # - contribution_fields (ContributionField.event_new)
    # - contribution_types (ContributionType.event_new)
    # - contributions (Contribution.event_new)
    # - custom_pages (EventPage.event_new)
    # - layout_images (ImageFile.event_new)
    # - legacy_contribution_mappings (LegacyContributionMapping.event_new)
    # - legacy_mapping (LegacyEventMapping.event_new)
    # - legacy_session_block_mappings (LegacySessionBlockMapping.event_new)
    # - legacy_session_mappings (LegacySessionMapping.event_new)
    # - legacy_subcontribution_mappings (LegacySubContributionMapping.event_new)
    # - log_entries (EventLogEntry.event_new)
    # - menu_entries (MenuEntry.event_new)
    # - note (EventNote.linked_event)
    # - persons (EventPerson.event_new)
    # - registration_forms (RegistrationForm.event_new)
    # - registrations (Registration.event_new)
    # - reminders (EventReminder.event_new)
    # - report_links (ReportLink.event_new)
    # - requests (Request.event_new)
    # - reservations (Reservation.event_new)
    # - sessions (Session.event_new)
    # - settings (EventSetting.event_new)
    # - settings_principals (EventSettingPrincipal.event_new)
    # - static_sites (StaticSite.event_new)
    # - surveys (Survey.event_new)
    # - timetable_entries (TimetableEntry.event_new)
    # - vc_room_associations (VCRoomEventAssociation.linked_event)

    @property
    @memoize_request
    def as_legacy(self):
        """Returns a legacy `Conference` object (ZODB)"""
        from MaKaC.conference import ConferenceHolder
        return ConferenceHolder().getById(self.id, True)

    @property
    def event_new(self):
        """Convenience property so all event entities have it"""
        return self

    @property
    def category(self):
        from MaKaC.conference import CategoryManager
        return CategoryManager().getById(str(self.category_id),
                                         True) if self.category_id else None

    @property
    def has_logo(self):
        return self.logo_metadata is not None

    @property
    def has_stylesheet(self):
        return self.stylesheet_metadata is not None

    @property
    def theme(self):
        from indico.modules.events.layout import layout_settings, theme_settings
        return (layout_settings.get(self, 'timetable_theme')
                or theme_settings.defaults[self.type])

    @property
    def is_protected(self):
        return self.as_legacy.isProtected()

    @property
    def locator(self):
        return {'confId': self.id}

    @property
    def logo_url(self):
        return url_for('event_images.logo_display',
                       self,
                       slug=self.logo_metadata['hash'])

    @property
    def participation_regform(self):
        return self.registration_forms.filter_by(is_participation=True,
                                                 is_deleted=False).first()

    @property
    @memoize_request
    def published_registrations(self):
        from indico.modules.events.registration.util import get_published_registrations
        return get_published_registrations(self)

    @property
    def protection_parent(self):
        return self.as_legacy.getOwner()

    @property
    def start_dt_local(self):
        return self.start_dt.astimezone(self.tzinfo)

    @property
    def end_dt_local(self):
        return self.end_dt.astimezone(self.tzinfo)

    @property
    def type(self):
        event_type = self.as_legacy.getType()
        if event_type == 'simple_event':
            event_type = 'lecture'
        return event_type

    @property
    def tzinfo(self):
        return pytz.timezone(self.timezone)

    @property
    def display_tzinfo(self):
        """The tzinfo of the event as preferred by the current user"""
        from MaKaC.common.timezoneUtils import DisplayTZ
        return DisplayTZ(conf=self).getDisplayTZ(as_timezone=True)

    @property
    @contextmanager
    def logging_disabled(self):
        """Temporarily disables event logging

        This is useful when performing actions e.g. during event
        creation or at other times where adding entries to the event
        log doesn't make sense.
        """
        self.__logging_disabled = True
        try:
            yield
        finally:
            self.__logging_disabled = False

    @classmethod
    def title_matches(cls, search_string, exact=False):
        """Check whether the title matches a search string.

        To be used in a SQLAlchemy `filter` call.

        :param search_string: A string to search for
        :param exact: Whether to search for the exact string
        """
        crit = db.func.to_tsvector('simple', cls.title).match(
            preprocess_ts_string(search_string), postgresql_regconfig='simple')
        if exact:
            crit = crit & cls.title.ilike('%{}%'.format(
                escape_like(search_string)))
        return crit

    @hybrid_method
    def happens_between(self, from_dt=None, to_dt=None):
        """Check whether the event takes place within two dates"""
        if from_dt is not None and to_dt is not None:
            # any event that takes place during the specified range
            return overlaps((self.start_dt, self.end_dt), (from_dt, to_dt),
                            inclusive=True)
        elif from_dt is not None:
            # any event that starts on/after the specified date
            return self.start_dt >= from_dt
        elif to_dt is not None:
            # any event that ends on/before the specifed date
            return self.end_dt <= to_dt
        else:
            return True

    @happens_between.expression
    def happens_between(cls, from_dt=None, to_dt=None):
        if from_dt is not None and to_dt is not None:
            # any event that takes place during the specified range
            return db_dates_overlap(cls,
                                    'start_dt',
                                    from_dt,
                                    'end_dt',
                                    to_dt,
                                    inclusive=True)
        elif from_dt is not None:
            # any event that starts on/after the specified date
            return cls.start_dt >= from_dt
        elif to_dt is not None:
            # any event that ends on/before the specifed date
            return cls.end_dt <= to_dt
        else:
            return True

    @hybrid_method
    def starts_between(self, from_dt=None, to_dt=None):
        """Check whether the event starts within two dates"""
        if from_dt is not None and to_dt is not None:
            return from_dt <= self.start_dt <= to_dt
        elif from_dt is not None:
            return self.start_dt >= from_dt
        elif to_dt is not None:
            return self.start_dt <= to_dt
        else:
            return True

    @starts_between.expression
    def starts_between(cls, from_dt=None, to_dt=None):
        if from_dt is not None and to_dt is not None:
            return cls.start_dt.between(from_dt, to_dt)
        elif from_dt is not None:
            return cls.start_dt >= from_dt
        elif to_dt is not None:
            return cls.start_dt <= to_dt
        else:
            return True

    @hybrid_property
    def duration(self):
        return self.end_dt - self.start_dt

    def can_access(self, user, allow_admin=True):
        if not allow_admin:
            raise NotImplementedError(
                'can_access(..., allow_admin=False) is unsupported until ACLs are migrated'
            )
        from MaKaC.accessControl import AccessWrapper
        return self.as_legacy.canAccess(
            AccessWrapper(user.as_avatar if user else None))

    def can_manage(self, user, role=None, allow_key=False, *args, **kwargs):
        # XXX: Remove this method once modification keys are gone!
        return (super(Event, self).can_manage(user, role, *args, **kwargs)
                or bool(allow_key and user and self.as_legacy.canKeyModify()))

    def get_non_inheriting_objects(self):
        """Get a set of child objects that do not inherit protection"""
        return get_non_inheriting_objects(self)

    def get_contribution(self, id_):
        """Get a contribution of the event"""
        return get_related_object(self, 'contributions', {'id': id_})

    def get_session(self, id_=None, friendly_id=None):
        """Get a session of the event"""
        if friendly_id is None and id_ is not None:
            criteria = {'id': id_}
        elif id_ is None and friendly_id is not None:
            criteria = {'friendly_id': friendly_id}
        else:
            raise ValueError('Exactly one kind of id must be specified')
        return get_related_object(self, 'sessions', criteria)

    def get_session_block(self, id_, scheduled_only=False):
        """Get a session block of the event"""
        from indico.modules.events.sessions.models.blocks import SessionBlock
        query = SessionBlock.query.filter(
            SessionBlock.id == id_,
            SessionBlock.session.has(event_new=self.event_new,
                                     is_deleted=False))
        if scheduled_only:
            query.filter(SessionBlock.timetable_entry != None)  # noqa
        return query.first()

    @memoize_request
    def has_feature(self, feature):
        """Checks if a feature is enabled for the event"""
        from indico.modules.events.features.util import is_feature_enabled
        return is_feature_enabled(self, feature)

    @property
    @memoize_request
    def scheduled_notes(self):
        from indico.modules.events.notes.util import get_scheduled_notes
        return get_scheduled_notes(self)

    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 summary: 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.
        """
        if self.__logging_disabled:
            return
        entry = EventLogEntry(user=user,
                              realm=realm,
                              kind=kind,
                              module=module,
                              type=type_,
                              summary=summary,
                              data=data or {})
        self.log_entries.append(entry)

    # XXX: Delete once event ACLs are in the new DB
    def get_access_list(self, skip_managers=False, skip_self_acl=False):
        return {
            x.as_new
            for x in self.as_legacy.getRecursiveAllowedToAccessList(
                skip_managers, skip_self_acl)
        }

    def get_contribution_field(self, field_id):
        return next((v for v in self.contribution_fields if v.id == field_id),
                    '')

    def move_start_dt(self, start_dt):
        """Set event start_dt and adjust its timetable entries"""
        diff = start_dt - self.start_dt
        for entry in self.timetable_entries.filter(
                TimetableEntry.parent_id.is_(None)):
            new_dt = entry.start_dt + diff
            entry.move(new_dt)
        self.start_dt = start_dt

    def preload_all_acl_entries(self):
        db.m.Contribution.preload_acl_entries(self)
        db.m.Session.preload_acl_entries(self)

    @return_ascii
    def __repr__(self):
        # TODO: add self.protection_repr once we use it
        return format_repr(self,
                           'id',
                           'start_dt',
                           'end_dt',
                           is_deleted=False,
                           _text=text_to_repr(self.title, max_length=75))

    # TODO: Remove the next block of code once event acls (read access) are migrated
    def _fail(self, *args, **kwargs):
        raise NotImplementedError(
            'These properties are not usable until event ACLs are in the new DB'
        )

    is_public = classproperty(classmethod(_fail))
    is_inheriting = classproperty(classmethod(_fail))
    is_self_protected = classproperty(classmethod(_fail))
    protection_repr = property(_fail)
    del _fail
Example #4
0
class Event(SearchableTitleMixin, DescriptionMixin, LocationMixin, ProtectionManagersMixin, AttachedItemsMixin,
            AttachedNotesMixin, PersonLinkDataMixin, db.Model):
    """An Indico event

    This model contains the most basic information related to an event.

    Note that the ACL is currently only used for managers but not for
    view access!
    """
    __tablename__ = 'events'
    disallowed_protection_modes = frozenset()
    inheriting_have_acl = True
    allow_access_key = True
    allow_no_access_contact = True
    location_backref_name = 'events'
    allow_location_inheritance = False
    possible_render_modes = {RenderMode.html}
    default_render_mode = RenderMode.html
    __logging_disabled = False

    ATTACHMENT_FOLDER_ID_COLUMN = 'event_id'

    @strict_classproperty
    @classmethod
    def __auto_table_args(cls):
        return (db.Index('ix_events_start_dt_desc', cls.start_dt.desc()),
                db.Index('ix_events_end_dt_desc', cls.end_dt.desc()),
                db.Index('ix_events_not_deleted_category', cls.is_deleted, cls.category_id),
                db.Index('ix_events_not_deleted_category_dates',
                         cls.is_deleted, cls.category_id, cls.start_dt, cls.end_dt),
                db.CheckConstraint("category_id IS NOT NULL OR is_deleted", 'category_data_set'),
                db.CheckConstraint("(logo IS NULL) = (logo_metadata::text = 'null')", 'valid_logo'),
                db.CheckConstraint("(stylesheet IS NULL) = (stylesheet_metadata::text = 'null')",
                                   'valid_stylesheet'),
                db.CheckConstraint("end_dt >= start_dt", 'valid_dates'),
                db.CheckConstraint("cloned_from_id != id", 'not_cloned_from_self'),
                db.CheckConstraint('visibility IS NULL OR visibility >= 0', 'valid_visibility'),
                {'schema': 'events'})

    @declared_attr
    def __table_args__(cls):
        return auto_table_args(cls)

    #: The ID of the event
    id = db.Column(
        db.Integer,
        primary_key=True
    )
    #: If the event has been deleted
    is_deleted = db.Column(
        db.Boolean,
        nullable=False,
        default=False
    )
    #: The ID of the user who created the event
    creator_id = db.Column(
        db.Integer,
        db.ForeignKey('users.users.id'),
        nullable=False,
        index=True
    )
    #: The ID of immediate parent category of the event
    category_id = db.Column(
        db.Integer,
        db.ForeignKey('categories.categories.id'),
        nullable=True,
        index=True
    )
    #: The ID of the series this events belongs to
    series_id = db.Column(
        db.Integer,
        db.ForeignKey('events.series.id'),
        nullable=True,
        index=True
    )
    #: If this event was cloned, the id of the parent event
    cloned_from_id = db.Column(
        db.Integer,
        db.ForeignKey('events.events.id'),
        nullable=True,
        index=True,
    )
    #: The creation date of the event
    created_dt = db.Column(
        UTCDateTime,
        nullable=False,
        index=True,
        default=now_utc
    )
    #: The start date of the event
    start_dt = db.Column(
        UTCDateTime,
        nullable=False,
        index=True
    )
    #: The end date of the event
    end_dt = db.Column(
        UTCDateTime,
        nullable=False,
        index=True
    )
    #: The timezone of the event
    timezone = db.Column(
        db.String,
        nullable=False
    )
    #: The type of the event
    _type = db.Column(
        'type',
        PyIntEnum(EventType),
        nullable=False
    )
    #: The visibility depth in category overviews
    visibility = db.Column(
        db.Integer,
        nullable=True,
        default=None
    )
    #: A list of tags/keywords for the event
    keywords = db.Column(
        ARRAY(db.String),
        nullable=False,
        default=[],
    )
    #: The metadata of the logo (hash, size, filename, content_type)
    logo_metadata = db.Column(
        JSON,
        nullable=False,
        default=None
    )
    #: The logo's raw image data
    logo = db.deferred(db.Column(
        db.LargeBinary,
        nullable=True
    ))
    #: The metadata of the stylesheet (hash, size, filename)
    stylesheet_metadata = db.Column(
        JSON,
        nullable=False,
        default=None
    )
    #: The stylesheet's raw image data
    stylesheet = db.deferred(db.Column(
        db.Text,
        nullable=True
    ))
    #: The ID of the event's default page (conferences only)
    default_page_id = db.Column(
        db.Integer,
        db.ForeignKey('events.pages.id'),
        index=True,
        nullable=True
    )
    #: The last user-friendly registration ID
    _last_friendly_registration_id = db.deferred(db.Column(
        'last_friendly_registration_id',
        db.Integer,
        nullable=False,
        default=0
    ))
    #: The last user-friendly contribution ID
    _last_friendly_contribution_id = db.deferred(db.Column(
        'last_friendly_contribution_id',
        db.Integer,
        nullable=False,
        default=0
    ))
    #: The last user-friendly session ID
    _last_friendly_session_id = db.deferred(db.Column(
        'last_friendly_session_id',
        db.Integer,
        nullable=False,
        default=0
    ))

    #: The category containing the event
    category = db.relationship(
        'Category',
        lazy=True,
        backref=db.backref(
            'events',
            primaryjoin='(Category.id == Event.category_id) & ~Event.is_deleted',
            order_by=(start_dt, id),
            lazy=True
        )
    )
    #: The user who created the event
    creator = db.relationship(
        'User',
        lazy=True,
        backref=db.backref(
            'created_events',
            lazy='dynamic'
        )
    )
    #: The event this one was cloned from
    cloned_from = db.relationship(
        'Event',
        lazy=True,
        remote_side='Event.id',
        backref=db.backref(
            'clones',
            lazy=True,
            order_by=start_dt
        )
    )
    #: The event's default page (conferences only)
    default_page = db.relationship(
        'EventPage',
        lazy=True,
        foreign_keys=[default_page_id],
        post_update=True,
        # don't use this backref. we just need it so SA properly NULLs
        # this column when deleting the default page
        backref=db.backref('_default_page_of_event', lazy=True)
    )
    #: The ACL entries for the event
    acl_entries = db.relationship(
        'EventPrincipal',
        backref='event_new',
        cascade='all, delete-orphan',
        collection_class=set
    )
    #: External references associated with this event
    references = db.relationship(
        'EventReference',
        lazy=True,
        cascade='all, delete-orphan',
        backref=db.backref(
            'event_new',
            lazy=True
        )
    )
    #: Persons associated with this event
    person_links = db.relationship(
        'EventPersonLink',
        lazy=True,
        cascade='all, delete-orphan',
        backref=db.backref(
            'event',
            lazy=True
        )
    )
    #: The series this event is part of
    series = db.relationship(
        'EventSeries',
        lazy=True,
        backref=db.backref(
            'events',
            lazy=True,
            order_by=(start_dt, id),
            primaryjoin='(Event.series_id == EventSeries.id) & ~Event.is_deleted',
        )
    )

    # relationship backrefs:
    # - abstracts (Abstract.event_new)
    # - agreements (Agreement.event_new)
    # - all_attachment_folders (AttachmentFolder.event_new)
    # - all_legacy_attachment_folder_mappings (LegacyAttachmentFolderMapping.event_new)
    # - all_legacy_attachment_mappings (LegacyAttachmentMapping.event_new)
    # - all_notes (EventNote.event_new)
    # - all_vc_room_associations (VCRoomEventAssociation.event_new)
    # - attachment_folders (AttachmentFolder.linked_event)
    # - clones (Event.cloned_from)
    # - contribution_fields (ContributionField.event_new)
    # - contribution_types (ContributionType.event_new)
    # - contributions (Contribution.event_new)
    # - custom_pages (EventPage.event_new)
    # - layout_images (ImageFile.event_new)
    # - legacy_contribution_mappings (LegacyContributionMapping.event_new)
    # - legacy_mapping (LegacyEventMapping.event_new)
    # - legacy_session_block_mappings (LegacySessionBlockMapping.event_new)
    # - legacy_session_mappings (LegacySessionMapping.event_new)
    # - legacy_subcontribution_mappings (LegacySubContributionMapping.event_new)
    # - log_entries (EventLogEntry.event_new)
    # - menu_entries (MenuEntry.event_new)
    # - note (EventNote.linked_event)
    # - persons (EventPerson.event_new)
    # - registration_forms (RegistrationForm.event_new)
    # - registrations (Registration.event_new)
    # - reminders (EventReminder.event_new)
    # - requests (Request.event_new)
    # - reservations (Reservation.event_new)
    # - sessions (Session.event_new)
    # - settings (EventSetting.event_new)
    # - settings_principals (EventSettingPrincipal.event_new)
    # - static_list_links (StaticListLink.event_new)
    # - static_sites (StaticSite.event_new)
    # - surveys (Survey.event_new)
    # - timetable_entries (TimetableEntry.event_new)
    # - vc_room_associations (VCRoomEventAssociation.linked_event)

    @classmethod
    def category_chain_overlaps(cls, category_ids):
        """
        Create a filter that checks whether the event has any of the
        provided category ids in its parent chain.

        :param category_ids: A list of category ids or a single
                             category id
        """
        from indico.modules.categories import Category
        if not isinstance(category_ids, (list, tuple, set)):
            category_ids = [category_ids]
        cte = Category.get_tree_cte()
        return (cte.c.id == Event.category_id) & cte.c.path.overlap(category_ids)

    @classmethod
    def is_visible_in(cls, category):
        """
        Create a filter that checks whether the event is visible in
        the specified category.
        """
        cte = category.visible_categories_cte
        return (db.exists(db.select([1]))
                .where(db.and_(cte.c.id == Event.category_id,
                               db.or_(Event.visibility.is_(None), Event.visibility > cte.c.level))))

    @property
    @memoize_request
    def as_legacy(self):
        """Returns a legacy `Conference` object (ZODB)"""
        from MaKaC.conference import ConferenceHolder
        return ConferenceHolder().getById(self.id, True)

    @property
    def event_new(self):
        """Convenience property so all event entities have it"""
        return self

    @property
    def has_logo(self):
        return self.logo_metadata is not None

    @property
    def has_stylesheet(self):
        return self.stylesheet_metadata is not None

    @property
    def theme(self):
        from indico.modules.events.layout import layout_settings, theme_settings
        return (layout_settings.get(self, 'timetable_theme') or
                theme_settings.defaults[self.type])

    @property
    def locator(self):
        return {'confId': self.id}

    @property
    def logo_url(self):
        return url_for('event_images.logo_display', self, slug=self.logo_metadata['hash'])

    @property
    def participation_regform(self):
        return self.registration_forms.filter_by(is_participation=True, is_deleted=False).first()

    @property
    @memoize_request
    def published_registrations(self):
        from indico.modules.events.registration.util import get_published_registrations
        return get_published_registrations(self)

    @property
    def protection_parent(self):
        return self.category

    @property
    def start_dt_local(self):
        return self.start_dt.astimezone(self.tzinfo)

    @property
    def end_dt_local(self):
        return self.end_dt.astimezone(self.tzinfo)

    @property
    def type(self):
        # XXX: this should eventually be replaced with the type_
        # property returning the enum - but there are too many places
        # right now that rely on the type string
        return self.type_.name

    @hybrid_property
    def type_(self):
        return self._type

    @type_.setter
    def type_(self, value):
        old_type = self._type
        self._type = value
        if old_type is not None and old_type != value:
            signals.event.type_changed.send(self, old_type=old_type)

    @property
    def url(self):
        return url_for('event.conferenceDisplay', self)

    @property
    def web_factory(self):
        return self.type_.web_factory

    @property
    def tzinfo(self):
        return pytz.timezone(self.timezone)

    @property
    def display_tzinfo(self):
        """The tzinfo of the event as preferred by the current user"""
        from MaKaC.common.timezoneUtils import DisplayTZ
        return DisplayTZ(conf=self).getDisplayTZ(as_timezone=True)

    @property
    @contextmanager
    def logging_disabled(self):
        """Temporarily disables event logging

        This is useful when performing actions e.g. during event
        creation or at other times where adding entries to the event
        log doesn't make sense.
        """
        self.__logging_disabled = True
        try:
            yield
        finally:
            self.__logging_disabled = False

    @hybrid_method
    def happens_between(self, from_dt=None, to_dt=None):
        """Check whether the event takes place within two dates"""
        if from_dt is not None and to_dt is not None:
            # any event that takes place during the specified range
            return overlaps((self.start_dt, self.end_dt), (from_dt, to_dt), inclusive=True)
        elif from_dt is not None:
            # any event that starts on/after the specified date
            return self.start_dt >= from_dt
        elif to_dt is not None:
            # any event that ends on/before the specifed date
            return self.end_dt <= to_dt
        else:
            return True

    @happens_between.expression
    def happens_between(cls, from_dt=None, to_dt=None):
        if from_dt is not None and to_dt is not None:
            # any event that takes place during the specified range
            return db_dates_overlap(cls, 'start_dt', from_dt, 'end_dt', to_dt, inclusive=True)
        elif from_dt is not None:
            # any event that starts on/after the specified date
            return cls.start_dt >= from_dt
        elif to_dt is not None:
            # any event that ends on/before the specifed date
            return cls.end_dt <= to_dt
        else:
            return True

    @hybrid_method
    def starts_between(self, from_dt=None, to_dt=None):
        """Check whether the event starts within two dates"""
        if from_dt is not None and to_dt is not None:
            return from_dt <= self.start_dt <= to_dt
        elif from_dt is not None:
            return self.start_dt >= from_dt
        elif to_dt is not None:
            return self.start_dt <= to_dt
        else:
            return True

    @starts_between.expression
    def starts_between(cls, from_dt=None, to_dt=None):
        if from_dt is not None and to_dt is not None:
            return cls.start_dt.between(from_dt, to_dt)
        elif from_dt is not None:
            return cls.start_dt >= from_dt
        elif to_dt is not None:
            return cls.start_dt <= to_dt
        else:
            return True

    @hybrid_property
    def duration(self):
        return self.end_dt - self.start_dt

    def get_relative_event_ids(self):
        """Get the first, last, previous and next event IDs.

        Any of those values may be ``None`` if there is no matching
        event or if it would be the current event.

        :return: A dict containing ``first``, ``last``, ``prev`` and ``next``.
        """
        subquery = (select([Event.id,
                            db.func.first_value(Event.id).over(order_by=(Event.start_dt, Event.id)).label('first'),
                            # XXX, use this after updating to sqlalchemy 1.1:
                            # db.func.last_value(Event.id).over(order_by=(Event.start_dt, Event.id),
                            #                                range_=(None, None)).label('last')
                            db.literal_column('last_value(id) OVER (ORDER BY start_dt ASC, id ASC RANGE '
                                              'BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)').label('last'),
                            db.func.lag(Event.id).over(order_by=(Event.start_dt, Event.id)).label('prev'),
                            db.func.lead(Event.id).over(order_by=(Event.start_dt, Event.id)).label('next')])
                    .where((Event.category_id == self.category_id) & ~Event.is_deleted)
                    .alias())
        rv = (db.session.query(subquery.c.first, subquery.c.last, subquery.c.prev, subquery.c.next)
              .filter(subquery.c.id == self.id)
              .one()
              ._asdict())
        if rv['first'] == self.id:
            rv['first'] = None
        if rv['last'] == self.id:
            rv['last'] = None
        return rv

    def get_verbose_title(self, show_speakers=False, show_series_pos=False):
        """Get the event title with some additional information

        :param show_speakers: Whether to prefix the title with the
                              speakers of the event.
        :param show_series_pos: Whether to suffix the title with the
                                position and total count in the event's
                                series.
        """
        title = self.title
        if show_speakers and self.person_links:
            speakers = ', '.join(sorted([pl.full_name for pl in self.person_links], key=unicode.lower))
            title = '{}, "{}"'.format(speakers, title)
        if show_series_pos and self.series and self.series.show_sequence_in_title:
            title = '{} ({}/{})'.format(title, self.series_pos, self.series_count)
        return title

    def get_non_inheriting_objects(self):
        """Get a set of child objects that do not inherit protection"""
        return get_non_inheriting_objects(self)

    def get_contribution(self, id_):
        """Get a contribution of the event"""
        return get_related_object(self, 'contributions', {'id': id_})

    def get_session(self, id_=None, friendly_id=None):
        """Get a session of the event"""
        if friendly_id is None and id_ is not None:
            criteria = {'id': id_}
        elif id_ is None and friendly_id is not None:
            criteria = {'friendly_id': friendly_id}
        else:
            raise ValueError('Exactly one kind of id must be specified')
        return get_related_object(self, 'sessions', criteria)

    def get_session_block(self, id_, scheduled_only=False):
        """Get a session block of the event"""
        from indico.modules.events.sessions.models.blocks import SessionBlock
        query = SessionBlock.query.filter(SessionBlock.id == id_,
                                          SessionBlock.session.has(event_new=self.event_new, is_deleted=False))
        if scheduled_only:
            query.filter(SessionBlock.timetable_entry != None)  # noqa
        return query.first()

    @memoize_request
    def has_feature(self, feature):
        """Checks if a feature is enabled for the event"""
        from indico.modules.events.features.util import is_feature_enabled
        return is_feature_enabled(self, feature)

    @property
    @memoize_request
    def scheduled_notes(self):
        from indico.modules.events.notes.util import get_scheduled_notes
        return get_scheduled_notes(self)

    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 summary: 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.
        """
        if self.__logging_disabled:
            return
        entry = EventLogEntry(user=user, realm=realm, kind=kind, module=module, type=type_, summary=summary,
                              data=data or {})
        self.log_entries.append(entry)

    def get_contribution_field(self, field_id):
        return next((v for v in self.contribution_fields if v.id == field_id), '')

    def move_start_dt(self, start_dt):
        """Set event start_dt and adjust its timetable entries"""
        diff = start_dt - self.start_dt
        for entry in self.timetable_entries.filter(TimetableEntry.parent_id.is_(None)):
            new_dt = entry.start_dt + diff
            entry.move(new_dt)
        self.start_dt = start_dt

    def preload_all_acl_entries(self):
        db.m.Contribution.preload_acl_entries(self)
        db.m.Session.preload_acl_entries(self)

    def move(self, category):
        old_category = self.category
        self.category = category
        db.session.flush()
        signals.event.moved.send(self, old_parent=old_category)

    @return_ascii
    def __repr__(self):
        # TODO: add self.protection_repr once we use it
        return format_repr(self, 'id', 'start_dt', 'end_dt', is_deleted=False,
                           _text=text_to_repr(self.title, max_length=75))