Example #1
0
    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(
            db.and_(Event.category_id == self.category_id, ~Event.is_deleted,
                    (Event.visibility.is_(None) | (Event.visibility != 0) |
                     (Event.id == self.id)))).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
Example #2
0
 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))))
Example #3
0
 def is_visible_in(cls, category_id):
     """
     Create a filter that checks whether the event is visible in
     the specified category.
     """
     cte = Category.get_visible_categories_cte(category_id)
     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))))
Example #4
0
 def __eq__(self, other):
     if other.principal_type == PrincipalType.email:
         criteria = [self.cls.email == other.email]
     elif other.principal_type == PrincipalType.local_group:
         criteria = [self.cls.local_group_id == other.id]
     elif other.principal_type == PrincipalType.multipass_group:
         criteria = [self.cls.multipass_group_provider == other.provider,
                     self.cls.multipass_group_name == other.name]
     elif other.principal_type == PrincipalType.user:
         criteria = [self.cls.user_id == other.id]
     else:
         raise ValueError('Unexpected object type {}: {}'.format(type(other), other))
     return db.and_(self.cls.type == other.principal_type, *criteria)
Example #5
0
 def __eq__(self, other):
     if other.principal_type == PrincipalType.email:
         criteria = [self.cls.email == other.email]
     elif other.principal_type == PrincipalType.network:
         criteria = [self.cls.ip_network_group_id == other.id]
     elif other.principal_type == PrincipalType.local_group:
         criteria = [self.cls.local_group_id == other.id]
     elif other.principal_type == PrincipalType.multipass_group:
         criteria = [self.cls.multipass_group_provider == other.provider,
                     self.cls.multipass_group_name == other.name]
     elif other.principal_type == PrincipalType.user:
         criteria = [self.cls.user_id == other.id]
     else:
         raise ValueError('Unexpected object type {}: {}'.format(type(other), other))
     return db.and_(self.cls.type == other.principal_type, *criteria)
Example #6
0
 def __eq__(self, other):
     if other.principal_type == PrincipalType.email:
         criteria = [self.cls.email == other.email]
     elif other.principal_type == PrincipalType.network:
         criteria = [self.cls.ip_network_group_id == other.id]
     elif other.principal_type == PrincipalType.event_role:
         criteria = [self.cls.event_role_id == other.id]
     elif other.principal_type == PrincipalType.category_role:
         criteria = [self.cls.category_role_id == other.id]
     elif other.principal_type == PrincipalType.registration_form:
         criteria = [self.cls.registration_form_id == other.id]
     elif other.principal_type == PrincipalType.local_group:
         criteria = [self.cls.local_group_id == other.id]
     elif other.principal_type == PrincipalType.multipass_group:
         criteria = [
             self.cls.multipass_group_provider == other.provider,
             self.cls.multipass_group_name == other.name
         ]
     elif other.principal_type == PrincipalType.user:
         criteria = [self.cls.user_id == other.id]
     else:
         raise ValueError(f'Unexpected object type {type(other)}: {other}')
     return db.and_(self.cls.type == other.principal_type, *criteria)
Example #7
0
    def get_permissions_for_user(cls, user, allow_admin=True):
        """Get the permissions for all rooms for a user.

        In case of multipass-based groups it will try to get a list of
        all groups the user is in, and if that's not possible check the
        permissions one by one for each room (which may result in many
        group membership lookups).

        It is recommended to not call this in any place where performance
        matters and to memoize the result.
        """
        # XXX: When changing the logic in here, make sure to update can_* as well!
        all_rooms_query = (Room.query.filter(~Room.is_deleted).options(
            load_only('id', 'protection_mode',
                      'reservations_need_confirmation', 'is_reservable',
                      'owner_id'),
            joinedload('owner').load_only('id'), joinedload('acl_entries')))
        is_admin = allow_admin and cls.is_user_admin(user)
        if (is_admin and allow_admin) or not user.can_get_all_multipass_groups:
            # check one by one if we can't get a list of all groups the user is in
            return {
                r.id: {
                    'book': r.can_book(user, allow_admin=allow_admin),
                    'prebook': r.can_prebook(user, allow_admin=allow_admin),
                    'override': r.can_override(user, allow_admin=allow_admin),
                    'moderate': r.can_moderate(user, allow_admin=allow_admin),
                    'manage': r.can_manage(user, allow_admin=allow_admin),
                }
                for r in all_rooms_query
            }

        criteria = [
            db.and_(RoomPrincipal.type == PrincipalType.user,
                    RoomPrincipal.user_id == user.id)
        ]
        for group in user.local_groups:
            criteria.append(
                db.and_(RoomPrincipal.type == PrincipalType.local_group,
                        RoomPrincipal.local_group_id == group.id))
        for group in user.iter_all_multipass_groups():
            criteria.append(
                db.and_(
                    RoomPrincipal.type == PrincipalType.multipass_group,
                    RoomPrincipal.multipass_group_provider ==
                    group.provider.name,
                    db.func.lower(RoomPrincipal.multipass_group_name) ==
                    group.name.lower()))

        data = {}
        permissions = {'book', 'prebook', 'override', 'moderate', 'manage'}
        prebooking_required_rooms = set()
        non_reservable_rooms = set()
        for room in all_rooms_query:
            is_owner = user == room.owner
            data[room.id] = {x: False for x in permissions}
            if room.reservations_need_confirmation:
                prebooking_required_rooms.add(room.id)
            if not room.is_reservable:
                non_reservable_rooms.add(room.id)
            if (room.is_reservable and
                (room.is_public or is_owner)) or (is_admin and allow_admin):
                if not room.reservations_need_confirmation or is_owner or (
                        is_admin and allow_admin):
                    data[room.id]['book'] = True
                if room.reservations_need_confirmation:
                    data[room.id]['prebook'] = True
            if is_owner or (is_admin and allow_admin):
                data[room.id]['override'] = True
                data[room.id]['moderate'] = True
                data[room.id]['manage'] = True
        query = (RoomPrincipal.query.join(Room).filter(
            ~Room.is_deleted, db.or_(*criteria)).options(
                load_only('room_id', 'full_access', 'permissions')))
        for principal in query:
            is_reservable = principal.room_id not in non_reservable_rooms
            for permission in permissions:
                if not is_reservable and not (is_admin and allow_admin
                                              ) and permission in ('book',
                                                                   'prebook'):
                    continue
                explicit = permission == 'prebook' and principal.room_id not in prebooking_required_rooms
                check_permission = None if permission == 'manage' else permission
                if principal.has_management_permission(check_permission,
                                                       explicit=explicit):
                    data[principal.room_id][permission] = True
        return data
Example #8
0
class Event(SearchableTitleMixin, DescriptionMixin, LocationMixin,
            ProtectionManagersMixin, AttachedItemsMixin, AttachedNotesMixin,
            PersonLinkMixin, 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_none_protection_parent = True
    allow_access_key = True
    allow_no_access_contact = True
    person_link_relation_name = 'EventPersonLink'
    person_link_backref_name = 'event'
    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(
                "(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'),
            db.CheckConstraint(
                'is_deleted OR category_id IS NOT NULL OR protection_mode = 1',
                'unlisted_events_always_inherit'), {
                    '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 ID of the label assigned to the event
    label_id = db.Column(db.ForeignKey('events.labels.id'),
                         index=True,
                         nullable=True)
    label_message = db.Column(
        db.Text,
        nullable=False,
        default='',
    )
    #: 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(JSONB, 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(JSONB,
                                    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 url to a map for the event
    own_map_url = db.Column('map_url', db.String, nullable=False, default='')

    #: The ID of the uploaded custom book of abstracts (if available)
    custom_boa_id = db.Column(db.Integer,
                              db.ForeignKey('indico.files.id'),
                              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))
    #: 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',
        ))
    #: The label assigned to the event
    label = db.relationship('EventLabel',
                            lazy=True,
                            backref=db.backref('events', lazy=True))
    #: The custom book of abstracts
    custom_boa = db.relationship('File',
                                 lazy=True,
                                 backref=db.backref('custom_boa_of',
                                                    lazy=True))
    #: The current pending move request
    pending_move_request = db.relationship(
        'EventMoveRequest',
        lazy=True,
        viewonly=True,
        uselist=False,
        primaryjoin=lambda: db.and_(
            EventMoveRequest.event_id == Event.id, EventMoveRequest.state ==
            MoveRequestState.pending))

    # 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_room_reservation_links (ReservationLink.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)
    # - editing_file_types (EditingFileType.event)
    # - editing_review_conditions (EditingReviewCondition.event)
    # - editing_tags (EditingTag.event)
    # - favorite_of (User.favorite_events)
    # - 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)
    # - move_requests (EventMoveRequest.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)
    # - registration_tags (RegistrationTag.event)
    # - registrations (Registration.event)
    # - reminders (EventReminder.event)
    # - requests (Request.event)
    # - roles (EventRole.event)
    # - room_reservation_links (ReservationLink.linked_event)
    # - session_types (SessionType.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)
    # - track_groups (TrackGroup.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')
    public_regform_access = _EventSettingProperty(event_core_settings,
                                                  'public_regform_access')

    @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.

        Warning: This method cannot be used in a negated filter.

        :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_subtree_ids_cte(category_ids)
        return cte.c.id == Event.category_id

    @classmethod
    def is_visible_in(cls, category_id):
        """
        Create a filter that checks whether the event is visible in
        the specified category.
        """
        cte = Category.get_visible_categories_cte(category_id)
        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
    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 {'event_id': self.id}

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

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

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

    @memoize_request
    def get_published_registrations(self, user):
        from indico.modules.events.registration.util import get_published_registrations
        is_participant = self.is_user_registered(user)
        return get_published_registrations(self, is_participant)

    @memoize_request
    def count_hidden_registrations(self, user):
        from indico.modules.events.registration.util import count_hidden_registrations
        is_participant = self.is_user_registered(user)
        return count_hidden_registrations(self, is_participant)

    @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)

    @hybrid_property
    def is_unlisted(self):
        return self.category is None

    @is_unlisted.expression
    def is_unlisted(cls):
        return cls.category_id.is_(None)

    @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', event_id=id_)

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

    @property
    def map_url(self):
        if self.own_map_url:
            return self.own_map_url
        elif not self.room:
            return ''
        return self.room.map_url or ''

    @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
    def editable_types(self):
        from indico.modules.events.editing.settings import editing_settings
        return editing_settings.get(self, 'editable_types')

    @property
    def has_regform_in_acl(self):
        return any(entry.registration_form.is_scheduled
                   for entry in self.acl_entries
                   if entry.type == PrincipalType.registration_form)

    @property
    def has_custom_boa(self):
        return self.custom_boa_id is not None

    @property
    @contextmanager
    def logging_disabled(self):
        """Temporarily disable 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 and self.category.can_manage(user)))

    def can_display(self, user):
        """Check whether the user can display the event in the category."""
        return self.visibility != 0 or self.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``.
        """
        if self.is_unlisted and self.creator == session.user:
            category_filters = [
                Event.category_id.is_(None), Event.creator == session.user
            ]
        elif self.is_unlisted:
            return {'first': None, 'last': None, 'prev': None, 'next': None}
        else:
            category_filters = [Event.category_id == self.category_id]
        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(
            db.and_(*category_filters, ~Event.is_deleted,
                    (Event.visibility.is_(None) | (Event.visibility != 0) |
                     (Event.id == self.id)))).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=str.lower))
            title = f'{speakers}, "{title}"'
        if show_series_pos and self.series and self.series.show_sequence_in_title:
            title = f'{title} ({self.series_pos}/{self.series_count})'
        return title

    def get_label_markup(self, size=''):
        label = self.label
        if not label:
            return ''
        return Markup(
            render_template('events/label.html',
                            label=label,
                            message=self.label_message,
                            size=size))

    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_sorted_tracks(self):
        """Return tracks and track groups in the correct order."""
        track_groups = self.track_groups
        tracks = [track for track in self.tracks if not track.track_group]
        return sorted(tracks + track_groups, key=attrgetter('position'))

    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: A dictionary 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 = {
            email.strip().lower(): f'{name} <{email}>'
            for email, name in emails.items() if email and email.strip()
        }
        own_email = session.user.email if has_request_context(
        ) and session.user else None
        return dict(
            sorted(list(emails.items()),
                   key=lambda x: (x[0] != own_email, x[1].lower())))

    @memoize_request
    def has_feature(self, feature):
        """Check 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,
            meta=None):
        """Create 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:`.LogKind` 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.
        :param meta: JSON-serializable data that won't be displayed.
        :return: The newly created `EventLogEntry`

        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 {}),
                              meta=(meta or {}))
        self.log_entries.append(entry)
        return 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.replace(hour=23, minute=59) -
                    start_dt.replace(hour=0, minute=0)).days
        for offset in range(duration + 1):
            day = (start_dt + timedelta(days=offset)).date()
            if day <= end_dt.date():
                yield day

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

    def move(self, category, *, log_meta=None):
        from indico.modules.events import EventLogRealm
        from indico.modules.logs import LogKind
        if self.pending_move_request:
            self.pending_move_request.withdraw(user=session.user)
        old_category = self.category
        self.category = category
        sep = ' \N{RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK} '
        old_path = sep.join(
            old_category.chain_titles) if old_category else 'Unlisted'
        new_path = sep.join(self.category.chain_titles)
        db.session.flush()
        signals.event.moved.send(self, old_parent=old_category)
        if old_category:
            self.log(EventLogRealm.management,
                     LogKind.change,
                     'Category',
                     'Event moved',
                     session.user,
                     data={
                         'From': old_path,
                         'To': new_path
                     },
                     meta=log_meta)
            old_category.log(CategoryLogRealm.events,
                             LogKind.negative,
                             'Content',
                             f'Event moved out: "{self.title}"',
                             session.user,
                             data={
                                 'ID': self.id,
                                 'To': new_path
                             },
                             meta=log_meta)
            category.log(CategoryLogRealm.events,
                         LogKind.positive,
                         'Content',
                         f'Event moved in: "{self.title}"',
                         session.user,
                         data={'From': old_path},
                         meta=log_meta)
        else:
            notify_event_creation(self)
            self.log(EventLogRealm.management,
                     LogKind.change,
                     'Category',
                     'Event published',
                     session.user,
                     data={'To': new_path},
                     meta=log_meta)
            category.log(CategoryLogRealm.events,
                         LogKind.positive,
                         'Content',
                         f'Event published here: "{self.title}"',
                         session.user,
                         meta=log_meta)

    def delete(self, reason, user=None):
        from indico.modules.events import EventLogRealm, logger
        from indico.modules.logs import LogKind
        self.is_deleted = True
        if self.pending_move_request:
            self.pending_move_request.withdraw(user=user)
        signals.event.deleted.send(self, user=user)
        db.session.flush()
        logger.info('Event %r deleted [%s]', self, reason)
        self.log(EventLogRealm.event,
                 LogKind.negative,
                 'Event',
                 'Event deleted',
                 user,
                 data={'Reason': reason})
        if self.category:
            self.category.log(CategoryLogRealm.events,
                              LogKind.negative,
                              'Content',
                              f'Event deleted: "{self.title}"',
                              user,
                              data={
                                  'ID': self.id,
                                  'Reason': reason
                              })

    def restore(self, reason=None, user=None):
        from indico.modules.events import EventLogRealm, logger
        from indico.modules.logs import LogKind
        if not self.is_deleted:
            return
        self.is_deleted = False
        signals.event.restored.send(self, user=user, reason=reason)
        db.session.flush()
        logger.info('Event %r restored [%s]', self, reason)
        data = {'Reason': reason} if reason else None
        self.log(EventLogRealm.event,
                 LogKind.positive,
                 'Event',
                 'Event restored',
                 user=user,
                 data=data)
        if self.category:
            self.category.log(CategoryLogRealm.events,
                              LogKind.positive,
                              'Content',
                              f'Event restored: "{self.title}"',
                              user,
                              data={
                                  'ID': self.id,
                                  'Reason': reason
                              })

    def refresh_event_persons(self, *, notify=True):
        """Update the data for all EventPersons based on the linked Users.

        :param notify: Whether to trigger the ``person_updated`` signal.
        """
        for person in self.persons.filter(
                EventPerson.user_id.isnot(None)).options(joinedload('user')):
            person.sync_user(notify=notify)

    @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)

    @property
    def reservations(self):
        return [link.reservation for link in self.all_room_reservation_links]

    @property
    def has_ended(self):
        return self.end_dt <= now_utc()

    @property
    def session_block_count(self):
        from indico.modules.events.sessions.models.blocks import SessionBlock
        return (SessionBlock.query.filter(
            SessionBlock.session.has(event=self, is_deleted=False),
            SessionBlock.timetable_entry != None)  # noqa
                .count())

    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))

    def is_user_registered(self, user):
        """Check whether the user is registered in the event.

        This takes both unpaid and complete registrations into account.
        """
        from indico.modules.events.registration.models.forms import RegistrationForm
        from indico.modules.events.registration.models.registrations import Registration, RegistrationState
        if user is None:
            return False
        return (Registration.query.with_parent(self).join(
            Registration.registration_form).filter(
                Registration.user == user,
                Registration.state.in_(
                    [RegistrationState.unpaid,
                     RegistrationState.complete]), ~Registration.is_deleted,
                ~RegistrationForm.is_deleted).has_rows())

    def is_user_speaker(self, user):
        from indico.modules.events.contributions import Contribution
        from indico.modules.events.contributions.models.persons import ContributionPersonLink, SubContributionPersonLink
        from indico.modules.events.contributions.models.subcontributions import SubContribution
        from indico.modules.events.models.persons import EventPerson
        if user is None:
            return False
        return (EventPerson.query.with_parent(self).with_parent(user).filter(
            or_(
                EventPerson.contribution_links.any(
                    and_(
                        ContributionPersonLink.is_speaker,
                        ContributionPersonLink.contribution.has(
                            ~Contribution.is_deleted))),
                EventPerson.subcontribution_links.any(
                    SubContributionPersonLink.subcontribution.has(
                        and_(
                            ~SubContribution.is_deleted,
                            SubContribution.contribution.has(
                                ~Contribution.is_deleted)))),
                EventPerson.event_links.any())).has_rows())
Example #9
0
    def get_permissions_for_user(cls, user, allow_admin=True):
        """Get the permissions for all rooms for a user.

        In case of multipass-based groups it will try to get a list of
        all groups the user is in, and if that's not possible check the
        permissions one by one for each room (which may result in many
        group membership lookups).

        It is recommended to not call this in any place where performance
        matters and to memoize the result.
        """
        # XXX: When changing the logic in here, make sure to update can_* as well!
        all_rooms_query = (Room.query
                           .filter(~Room.is_deleted)
                           .options(load_only('id', 'protection_mode', 'reservations_need_confirmation',
                                              'is_reservable', 'owner_id'),
                                    joinedload('owner').load_only('id'),
                                    joinedload('acl_entries')))
        is_admin = allow_admin and cls.is_user_admin(user)
        if (is_admin and allow_admin) or not user.can_get_all_multipass_groups:
            # check one by one if we can't get a list of all groups the user is in
            return {r.id: {
                'book': r.can_book(user, allow_admin=allow_admin),
                'prebook': r.can_prebook(user, allow_admin=allow_admin),
                'override': r.can_override(user, allow_admin=allow_admin),
                'moderate': r.can_moderate(user, allow_admin=allow_admin),
                'manage': r.can_manage(user, allow_admin=allow_admin),
            } for r in all_rooms_query}

        criteria = [db.and_(RoomPrincipal.type == PrincipalType.user, RoomPrincipal.user_id == user.id)]
        for group in user.local_groups:
            criteria.append(db.and_(RoomPrincipal.type == PrincipalType.local_group,
                                    RoomPrincipal.local_group_id == group.id))
        for group in user.iter_all_multipass_groups():
            criteria.append(db.and_(RoomPrincipal.type == PrincipalType.multipass_group,
                                    RoomPrincipal.multipass_group_provider == group.provider.name,
                                    db.func.lower(RoomPrincipal.multipass_group_name) == group.name.lower()))

        data = {}
        permissions = {'book', 'prebook', 'override', 'moderate', 'manage'}
        prebooking_required_rooms = set()
        non_reservable_rooms = set()
        for room in all_rooms_query:
            is_owner = user == room.owner
            data[room.id] = {x: False for x in permissions}
            if room.reservations_need_confirmation:
                prebooking_required_rooms.add(room.id)
            if not room.is_reservable:
                non_reservable_rooms.add(room.id)
            if (room.is_reservable and (room.is_public or is_owner)) or (is_admin and allow_admin):
                if not room.reservations_need_confirmation or is_owner or (is_admin and allow_admin):
                    data[room.id]['book'] = True
                if room.reservations_need_confirmation:
                    data[room.id]['prebook'] = True
            if is_owner or (is_admin and allow_admin):
                data[room.id]['override'] = True
                data[room.id]['moderate'] = True
                data[room.id]['manage'] = True
        query = (RoomPrincipal.query
                 .join(Room)
                 .filter(~Room.is_deleted, db.or_(*criteria))
                 .options(load_only('room_id', 'full_access', 'permissions')))
        for principal in query:
            is_reservable = principal.room_id not in non_reservable_rooms
            for permission in permissions:
                if not is_reservable and not (is_admin and allow_admin) and permission in ('book', 'prebook'):
                    continue
                explicit = permission == 'prebook' and principal.room_id not in prebooking_required_rooms
                check_permission = None if permission == 'manage' else permission
                if principal.has_management_permission(check_permission, explicit=explicit):
                    data[principal.room_id][permission] = True
        return data