Example #1
0
 def registration_form(cls):
     if not cls.allow_registration_forms:
         return
     assert cls.principal_backref_name
     return db.relationship('RegistrationForm',
                            lazy=False,
                            backref=db.backref(cls.principal_backref_name,
                                               cascade='all, delete',
                                               lazy='dynamic'))
Example #2
0
 def category_role(cls):
     if not cls.allow_category_roles:
         return
     assert cls.principal_backref_name
     return db.relationship('CategoryRole',
                            lazy=False,
                            backref=db.backref(cls.principal_backref_name,
                                               cascade='all, delete',
                                               lazy='dynamic'))
Example #3
0
 def ip_network_group(cls):
     if not cls.allow_networks:
         return
     assert cls.principal_backref_name
     return db.relationship('IPNetworkGroup',
                            lazy=False,
                            backref=db.backref(cls.principal_backref_name,
                                               cascade='all, delete',
                                               lazy='dynamic'))
Example #4
0
 def _affiliation_link(cls):
     return db.relationship(
         'Affiliation',
         lazy=False,
         backref=db.backref(
             cls.person_link_backref_name,
             cascade_backrefs=False,
             lazy='dynamic',
         )
     )
Example #5
0
 def person_links(cls):
     return db.relationship(
         cls.person_link_relation_name,
         lazy=True,
         cascade='all, delete-orphan',
         backref=db.backref(
             cls.person_link_backref_name,
             lazy=True
         )
     )
Example #6
0
 def local_group(cls):
     assert cls.principal_backref_name
     return db.relationship(
         'LocalGroup',
         lazy=False,
         backref=db.backref(
             cls.principal_backref_name,
             cascade='all, delete-orphan',
             lazy='dynamic'
         )
     )
Example #7
0
 def user(cls):
     assert cls.principal_backref_name
     return db.relationship(
         'User',
         lazy=False,
         backref=db.backref(
             cls.principal_backref_name,
             cascade='all, delete-orphan',
             lazy='dynamic'
         )
     )
Example #8
0
 def person(cls):
     return db.relationship(
         'EventPerson',
         lazy=False,
         backref=db.backref(
             cls.person_link_backref_name,
             cascade='all, delete-orphan',
             cascade_backrefs=False,
             lazy=True
         )
     )
Example #9
0
 def person(cls):
     return db.relationship(
         'EventPerson',
         lazy=False,
         backref=db.backref(
             cls.person_link_backref_name,
             cascade='all, delete-orphan',
             cascade_backrefs=False,
             lazy=True
         )
     )
Example #10
0
 def local_group(cls):
     assert cls.principal_backref_name
     return db.relationship(
         'LocalGroup',
         lazy=False,
         backref=db.backref(
             cls.principal_backref_name,
             cascade='all, delete',
             lazy='dynamic'
         )
     )
Example #11
0
 def user(cls):
     assert cls.principal_backref_name
     return db.relationship(
         'User',
         lazy=False,
         backref=db.backref(
             cls.principal_backref_name,
             cascade='all, delete',
             lazy='dynamic'
         )
     )
Example #12
0
class VidyoExtension(db.Model):
    __tablename__ = 'vidyo_extensions'
    __table_args__ = {'schema': 'plugin_vc_vidyo'}

    #: ID of the videoconference room
    vc_room_id = db.Column(db.Integer,
                           db.ForeignKey('events.vc_rooms.id'),
                           primary_key=True)
    extension = db.Column(db.BigInteger, index=True)
    owned_by_id = db.Column(db.Integer,
                            db.ForeignKey('users.users.id'),
                            index=True,
                            nullable=False)
    vc_room = db.relationship('VCRoom',
                              lazy=False,
                              backref=db.backref('vidyo_extension',
                                                 cascade='all, delete-orphan',
                                                 uselist=False,
                                                 lazy=False))

    #: The user who owns the Vidyo room
    owned_by_user = db.relationship('User',
                                    lazy=True,
                                    backref=db.backref('vc_rooms_vidyo',
                                                       lazy='dynamic'))

    @property
    def join_url(self):
        from indico_vc_vidyo.plugin import VidyoPlugin
        url = self.vc_room.data['url']
        custom_url_tpl = VidyoPlugin.settings.get('client_chooser_url')
        if custom_url_tpl:
            return custom_url_tpl + '?' + urllib.urlencode({'url': url})
        return url

    @return_ascii
    def __repr__(self):
        return '<VidyoExtension({}, {}, {})>'.format(self.vc_room,
                                                     self.extension,
                                                     self.owned_by_user)
Example #13
0
 def event_role(cls):
     if not cls.allow_event_roles:
         return
     assert cls.principal_backref_name
     return db.relationship(
         'EventRole',
         lazy=False,
         backref=db.backref(
             cls.principal_backref_name,
             cascade='all, delete',
             lazy='dynamic'
         )
     )
Example #14
0
 def ip_network_group(cls):
     if not cls.allow_networks:
         return
     assert cls.principal_backref_name
     return db.relationship(
         'IPNetworkGroup',
         lazy=False,
         backref=db.backref(
             cls.principal_backref_name,
             cascade='all, delete',
             lazy='dynamic'
         )
     )
Example #15
0
class AbstractReviewQuestion(db.Model):
    __tablename__ = 'abstract_review_questions'
    __table_args__ = {'schema': 'event_abstracts'}

    id = db.Column(db.Integer, primary_key=True)
    event_id = db.Column(db.Integer,
                         db.ForeignKey('events.events.id'),
                         index=True,
                         nullable=False)
    text = db.Column(db.Text, nullable=False)
    no_score = db.Column(db.Boolean, nullable=False, default=False)
    position = db.Column(db.Integer,
                         nullable=False,
                         default=_get_next_position)
    is_deleted = db.Column(db.Boolean, nullable=False, default=False)
    event_new = db.relationship(
        'Event',
        lazy=True,
        backref=db.backref(
            'abstract_review_questions',
            primaryjoin=
            '(AbstractReviewQuestion.event_id == Event.id) & ~AbstractReviewQuestion.is_deleted',
            order_by=position,
            cascade='all, delete-orphan',
            lazy=True))

    # relationship backrefs:
    # - ratings (AbstractReviewRating.question)

    @return_ascii
    def __repr__(self):
        return format_repr(self,
                           'id',
                           'event_id',
                           no_score=False,
                           is_deleted=False,
                           _text=self.text)

    def get_review_rating(self, review, allow_create=False):
        """Get the rating given in particular review.

        :param review: the review object
        :param allow_create: if there is not rating for that review a new one is created
        """
        results = [
            rating for rating in review.ratings if rating.question == self
        ]
        rating = results[0] if results else None
        if rating is None and allow_create:
            rating = AbstractReviewRating(question=self, review=review)
        return rating
class ZoomMeeting(db.Model):
    __tablename__ = 'zoom_meetings'
    __table_args__ = {'schema': 'plugin_vc_zoom'}

    #: ID of the videoconference room
    vc_room_id = db.Column(db.Integer,
                           db.ForeignKey('events.vc_rooms.id'),
                           primary_key=True)
    meeting = db.Column(db.BigInteger, index=True)
    url_zoom = db.Column(db.Text, index=True, nullable=False)
    owned_by_id = db.Column(db.Integer,
                            db.ForeignKey('users.users.id'),
                            index=True,
                            nullable=False)
    vc_room = db.relationship('VCRoom',
                              lazy=False,
                              backref=db.backref('zoom_meeting',
                                                 cascade='all, delete-orphan',
                                                 uselist=False,
                                                 lazy=False))

    #: The user who owns the Zoom room
    owned_by_user = db.relationship('User',
                                    lazy=True,
                                    backref=db.backref('vc_rooms_zoom',
                                                       lazy='dynamic'))

    @property
    def join_url(self):
        from indico_vc_zoom.plugin import ZoomPlugin
        url = self.vc_room.data['url']
        return url

    @return_ascii
    def __repr__(self):
        return '<ZoomMeeting({}, {}, {})>'.format(self.vc_room, self.meeting,
                                                  self.owned_by_user)
class CERNAccessRequest(db.Model):
    __tablename__ = 'access_requests'
    __table_args__ = {'schema': 'plugin_cern_access'}

    registration_id = db.Column(
        db.ForeignKey('event_registration.registrations.id'), primary_key=True)
    request_state = db.Column(PyIntEnum(CERNAccessRequestState),
                              nullable=False,
                              default=CERNAccessRequestState.not_requested)
    reservation_code = db.Column(db.String, nullable=False)
    birth_date = db.Column(db.Date, nullable=True)
    nationality = db.Column(db.String, nullable=True)
    birth_place = db.Column(db.String, nullable=True)
    license_plate = db.Column(db.String, nullable=True)

    registration = db.relationship('Registration',
                                   uselist=False,
                                   lazy=True,
                                   backref=db.backref('cern_access_request',
                                                      uselist=False,
                                                      lazy=False))

    @hybrid_property
    def is_not_requested(self):
        return self.request_state == CERNAccessRequestState.not_requested

    @hybrid_property
    def is_withdrawn(self):
        return self.request_state == CERNAccessRequestState.withdrawn

    @hybrid_property
    def is_active(self):
        return self.request_state == CERNAccessRequestState.active

    @hybrid_property
    def has_identity_info(self):
        return bool(self.birth_place) and bool(
            self.nationality) and self.birth_date is not None

    @has_identity_info.expression
    def has_identity_info(cls):
        return cls.birth_place.isnot(None) & cls.nationality.isnot(
            None) & cls.birth_date.isnot(None)

    def clear_identity_data(self):
        self.birth_date = None
        self.nationality = None
        self.birth_place = None
        self.license_plate = None
Example #18
0
class TrackGroup(DescriptionMixin, db.Model):
    __tablename__ = 'track_groups'
    __table_args__ = {'schema': 'events'}

    is_track_group = True

    possible_render_modes = {RenderMode.markdown}
    default_render_mode = RenderMode.markdown

    id = db.Column(
        db.Integer,
        primary_key=True
    )
    title = db.Column(
        db.String,
        nullable=False
    )
    position = db.Column(
        db.Integer,
        nullable=False,
        default=get_next_position
    )
    event_id = db.Column(
        db.Integer,
        db.ForeignKey('events.events.id'),
        index=True,
        nullable=False
    )
    event = db.relationship(
        'Event',
        lazy=True,
        backref=db.backref(
            'track_groups',
            cascade='all, delete-orphan',
            lazy=True,
            order_by=id
        )
    )

    # relationship backrefs:
    # - tracks (Track.track_group)

    @locator_property
    def locator(self):
        return dict(self.event.locator, track_group_id=self.id)

    @return_ascii
    def __repr__(self):
        return format_repr(self, 'id', _text=text_to_repr(self.title))
Example #19
0
class Foo(db.Model):
    __tablename__ = 'foo'
    __table_args__ = {'schema': 'plugin_example'}

    id = db.Column(db.Integer, primary_key=True)
    bar = db.Column(db.String, default='')
    location_id = db.Column(db.Integer,
                            db.ForeignKey('roombooking.locations.id'),
                            nullable=False)
    location = db.relationship(
        'Location',
        backref=db.backref('example_foo',
                           cascade='all, delete-orphan',
                           lazy='dynamic'),
    )

    @return_ascii
    def __repr__(self):
        return u'<Foo({}, {}, {})>'.format(self.id, self.bar, self.location)
class CERNAccessRequestRegForm(db.Model):
    __tablename__ = 'access_request_regforms'
    __table_args__ = {'schema': 'plugin_cern_access'}

    form_id = db.Column(db.ForeignKey('event_registration.forms.id'),
                        primary_key=True)
    request_state = db.Column(PyIntEnum(CERNAccessRequestState),
                              nullable=False,
                              default=CERNAccessRequestState.not_requested)

    registration_form = db.relationship('RegistrationForm',
                                        uselist=False,
                                        lazy=False,
                                        backref=db.backref(
                                            'cern_access_request',
                                            uselist=False))

    @hybrid_property
    def is_active(self):
        return self.request_state != CERNAccessRequestState.withdrawn
Example #21
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 #22
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))
Example #23
0
class EventPerson(PersonMixin, db.Model):
    """A person inside an event, e.g. a speaker/author etc."""

    __tablename__ = 'persons'
    __table_args__ = (db.UniqueConstraint('event_id', 'user_id'),
                      db.CheckConstraint('email = lower(email)',
                                         'lowercase_email'),
                      db.Index(None,
                               'event_id',
                               'email',
                               unique=True,
                               postgresql_where=db.text("email != ''")), {
                                   'schema': 'events'
                               })

    id = db.Column(db.Integer, primary_key=True)
    event_id = db.Column(db.Integer,
                         db.ForeignKey('events.events.id'),
                         nullable=False,
                         index=True)
    user_id = db.Column(db.Integer,
                        db.ForeignKey('users.users.id'),
                        nullable=True,
                        index=True)
    first_name = db.Column(db.String, nullable=False, default='')
    last_name = db.Column(db.String, nullable=False)
    email = db.Column(db.String, nullable=False, index=True, default='')
    # the title of the user - you usually want the `title` property!
    _title = db.Column('title',
                       PyIntEnum(UserTitle),
                       nullable=False,
                       default=UserTitle.none)
    affiliation = db.Column(db.String, nullable=False, default='')
    address = db.Column(db.Text, nullable=False, default='')
    phone = db.Column(db.String, nullable=False, default='')
    invited_dt = db.Column(UTCDateTime, nullable=True)
    is_untrusted = db.Column(db.Boolean, nullable=False, default=False)

    event = db.relationship('Event',
                            lazy=True,
                            backref=db.backref('persons',
                                               cascade='all, delete-orphan',
                                               cascade_backrefs=False,
                                               lazy='dynamic'))
    user = db.relationship('User',
                           lazy=True,
                           backref=db.backref('event_persons',
                                              cascade_backrefs=False,
                                              lazy='dynamic'))

    # relationship backrefs:
    # - abstract_links (AbstractPersonLink.person)
    # - contribution_links (ContributionPersonLink.person)
    # - event_links (EventPersonLink.person)
    # - session_block_links (SessionBlockPersonLink.person)
    # - subcontribution_links (SubContributionPersonLink.person)

    @locator_property
    def locator(self):
        return dict(self.event.locator, person_id=self.id)

    @return_ascii
    def __repr__(self):
        return format_repr(self,
                           'id',
                           is_untrusted=False,
                           _text=self.full_name)

    @property
    def principal(self):
        if self.user is not None:
            return self.user
        elif self.email:
            return EmailPrincipal(self.email)
        return None

    @classmethod
    def create_from_user(cls, user, event=None, is_untrusted=False):
        return EventPerson(user=user,
                           event=event,
                           first_name=user.first_name,
                           last_name=user.last_name,
                           email=user.email,
                           affiliation=user.affiliation,
                           address=user.address,
                           phone=user.phone,
                           is_untrusted=is_untrusted)

    @classmethod
    def for_user(cls, user, event=None, is_untrusted=False):
        """Return EventPerson for a matching User in Event creating if needed"""
        person = event.persons.filter_by(user=user).first() if event else None
        return person or cls.create_from_user(
            user, event, is_untrusted=is_untrusted)

    @classmethod
    def merge_users(cls, target, source):
        """Merge the EventPersons of two users.

        :param target: The target user of the merge
        :param source: The user that is being merged into `target`
        """
        existing_persons = {ep.event_id: ep for ep in target.event_persons}
        for event_person in source.event_persons:
            existing = existing_persons.get(event_person.event_id)
            if existing is None:
                event_person.user = target
            else:
                existing.merge_person_info(event_person)
                db.session.delete(event_person)
        db.session.flush()

    @classmethod
    def link_user_by_email(cls, user):
        """
        Links all email-based persons matching the user's
        email addresses with the user.

        :param user: A User object.
        """
        from indico.modules.events.models.events import Event
        query = (cls.query.join(EventPerson.event).filter(
            ~Event.is_deleted, cls.email.in_(user.all_emails),
            cls.user_id.is_(None)))
        for event_person in query:
            existing = (cls.query.filter_by(
                user_id=user.id, event_id=event_person.event_id).one_or_none())
            if existing is None:
                event_person.user = user
            else:
                existing.merge_person_info(event_person)
                db.session.delete(event_person)
        db.session.flush()

    @no_autoflush
    def merge_person_info(self, other):
        from indico.modules.events.contributions.models.persons import AuthorType
        for column_name in {
                '_title', 'affiliation', 'address', 'phone', 'first_name',
                'last_name'
        }:
            value = getattr(self, column_name) or getattr(other, column_name)
            setattr(self, column_name, value)

        for event_link in other.event_links:
            existing_event_link = next(
                (link for link in self.event_links
                 if link.event_id == event_link.event_id), None)
            if existing_event_link is None:
                event_link.person = self
            else:
                other.event_links.remove(event_link)

        for abstract_link in other.abstract_links:
            existing_abstract_link = next(
                (link for link in self.abstract_links
                 if link.abstract_id == abstract_link.abstract_id), None)

            if existing_abstract_link is None:
                abstract_link.person = self
            else:
                existing_abstract_link.is_speaker |= abstract_link.is_speaker
                existing_abstract_link.author_type = AuthorType.get_highest(
                    existing_abstract_link.author_type,
                    abstract_link.author_type)
                other.abstract_links.remove(abstract_link)

        for contribution_link in other.contribution_links:
            existing_contribution_link = next(
                (link for link in self.contribution_links
                 if link.contribution_id == contribution_link.contribution_id),
                None)

            if existing_contribution_link is None:
                contribution_link.person = self
            else:
                existing_contribution_link.is_speaker |= contribution_link.is_speaker
                existing_contribution_link.author_type = AuthorType.get_highest(
                    existing_contribution_link.author_type,
                    contribution_link.author_type)
                other.contribution_links.remove(contribution_link)

        for subcontribution_link in other.subcontribution_links:
            existing_subcontribution_link = next(
                (link for link in self.subcontribution_links
                 if link.subcontribution_id ==
                 subcontribution_link.subcontribution_id), None)
            if existing_subcontribution_link is None:
                subcontribution_link.person = self
            else:
                other.subcontribution_links.remove(subcontribution_link)

        for session_block_link in other.session_block_links:
            existing_session_block_link = next(
                (link
                 for link in self.session_block_links if link.session_block_id
                 == session_block_link.session_block_id), None)
            if existing_session_block_link is None:
                session_block_link.person = self
            else:
                other.session_block_links.remove(session_block_link)

        db.session.flush()

    def has_role(self, role, obj):
        """Whether the person has a role in the ACL list of a given object"""
        principals = [
            x for x in obj.acl_entries
            if x.has_management_permission(role, explicit=True)
        ]
        return any(
            x for x in principals
            if ((self.user_id is not None and self.user_id == x.user_id) or (
                self.email is not None and self.email == x.email)))
Example #24
0
class Track(DescriptionMixin, ProtectionManagersMixin, db.Model):
    __tablename__ = 'tracks'
    __table_args__ = {'schema': 'events'}

    disable_protection_mode = True
    is_track_group = False

    possible_render_modes = {RenderMode.markdown}
    default_render_mode = RenderMode.markdown

    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String, nullable=False)
    code = db.Column(db.String, nullable=False, default='')
    event_id = db.Column(db.Integer,
                         db.ForeignKey('events.events.id'),
                         index=True,
                         nullable=False)
    position = db.Column(db.Integer, nullable=False, default=get_next_position)
    default_session_id = db.Column(db.Integer,
                                   db.ForeignKey('events.sessions.id'),
                                   index=True,
                                   nullable=True)
    track_group_id = db.Column(db.Integer,
                               db.ForeignKey('events.track_groups.id',
                                             ondelete='SET NULL'),
                               index=True,
                               nullable=True)
    event = db.relationship('Event',
                            lazy=True,
                            backref=db.backref('tracks',
                                               cascade='all, delete-orphan',
                                               lazy=True,
                                               order_by=position))
    acl_entries = db.relationship('TrackPrincipal',
                                  lazy=True,
                                  cascade='all, delete-orphan',
                                  collection_class=set,
                                  backref='track')
    default_session = db.relationship('Session',
                                      lazy=True,
                                      backref='default_for_tracks')
    track_group = db.relationship('TrackGroup',
                                  lazy=True,
                                  backref=db.backref('tracks',
                                                     order_by=position,
                                                     lazy=True,
                                                     passive_deletes=True))

    # relationship backrefs:
    # - abstract_reviews (AbstractReview.track)
    # - abstracts_accepted (Abstract.accepted_track)
    # - abstracts_reviewed (Abstract.reviewed_for_tracks)
    # - abstracts_submitted (Abstract.submitted_for_tracks)
    # - contributions (Contribution.track)
    # - proposed_abstract_reviews (AbstractReview.proposed_tracks)

    @property
    def short_title(self):
        return self.code if self.code else self.title

    @property
    def full_title(self):
        return f'{self.code} - {self.title}' if self.code else self.title

    @property
    def title_with_group(self):
        return f'{self.track_group.title}: {self.title}' if self.track_group else self.title

    @property
    def short_title_with_group(self):
        return f'{self.track_group.title}: {self.short_title}' if self.track_group else self.short_title

    @property
    def full_title_with_group(self):
        return f'{self.track_group.title}: {self.full_title}' if self.track_group else self.full_title

    @locator_property
    def locator(self):
        return dict(self.event.locator, track_id=self.id)

    def __repr__(self):
        return format_repr(self, 'id', _text=text_to_repr(self.title))

    def can_delete(self, user):
        return self.event.can_manage(user) and not self.abstracts_accepted

    def can_review_abstracts(self, user):
        if not user:
            return False
        elif not self.event.can_manage(user,
                                       permission='abstract_reviewer',
                                       explicit_permission=True):
            return False
        elif self.event.can_manage(user,
                                   permission='review_all_abstracts',
                                   explicit_permission=True):
            return True
        return self.can_manage(user,
                               permission='review',
                               explicit_permission=True)

    def can_convene(self, user):
        if not user:
            return False
        elif not self.event.can_manage(
                user, permission='track_convener', explicit_permission=True):
            return False
        elif self.event.can_manage(user,
                                   permission='convene_all_abstracts',
                                   explicit_permission=True):
            return True
        return self.can_manage(user,
                               permission='convene',
                               explicit_permission=True)
Example #25
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 #26
0
class AbstractEmailLogEntry(db.Model):
    __tablename__ = 'email_logs'
    __table_args__ = {'schema': 'event_abstracts'}

    id = db.Column(db.Integer, primary_key=True)
    abstract_id = db.Column(db.Integer,
                            db.ForeignKey('event_abstracts.abstracts.id'),
                            index=True,
                            nullable=False)
    email_template_id = db.Column(
        db.Integer,
        db.ForeignKey('event_abstracts.email_templates.id'),
        index=True,
        nullable=True)
    user_id = db.Column(db.Integer,
                        db.ForeignKey('users.users.id'),
                        index=True,
                        nullable=True)
    sent_dt = db.Column(UTCDateTime, nullable=False, default=now_utc)
    recipients = db.Column(ARRAY(db.String), nullable=False)
    subject = db.Column(db.String, nullable=False)
    body = db.Column(db.Text, nullable=False)
    data = db.Column(JSON, nullable=False)

    abstract = db.relationship('Abstract',
                               lazy=True,
                               backref=db.backref('email_logs',
                                                  order_by=sent_dt,
                                                  lazy=True))
    email_template = db.relationship('AbstractEmailTemplate',
                                     lazy=True,
                                     backref=db.backref('logs',
                                                        lazy='dynamic'))
    user = db.relationship('User',
                           lazy=True,
                           backref=db.backref('abstract_email_log_entries',
                                              lazy='dynamic'))

    @return_ascii
    def __repr__(self):
        return format_repr(self, 'id', 'abstract_id', _text=self.subject)

    @classmethod
    def create_from_email(cls, email_data, email_tpl, user=None):
        """Create a new log entry from the data used to send an email

        :param email_data: email data as returned from `make_email`
        :param email_tpl: the abstract email template that created the
                          email
        :param user: the user who performed the action causing the
                     notification
        """
        recipients = sorted(email_data['toList'] | email_data['ccList']
                            | email_data['bccList'])
        data = {'template_name': email_tpl.title}
        return cls(email_template=email_tpl,
                   user=user,
                   recipients=recipients,
                   subject=email_data['subject'],
                   body=email_data['body'],
                   data=data)
Example #27
0
class Room(versioned_cache(_cache, 'id'), db.Model, Serializer):
    __tablename__ = 'rooms'
    __table_args__ = (db.UniqueConstraint('id', 'location_id'),  # useless but needed for the LocationMixin fkey
                      {'schema': 'roombooking'})

    __public__ = [
        'id', 'name', 'location_name', 'floor', 'number', 'building',
        'booking_url', 'capacity', 'comments', 'owner_id', 'details_url',
        'large_photo_url', 'small_photo_url', 'has_photo', 'is_active',
        'is_reservable', 'is_auto_confirm', 'marker_description', 'kind',
        'booking_limit_days'
    ]

    __public_exhaustive__ = __public__ + [
        'has_webcast_recording', 'has_vc', 'has_projector', 'is_public', 'has_booking_groups'
    ]

    __calendar_public__ = [
        'id', 'building', 'name', 'floor', 'number', 'kind', 'booking_url', 'details_url', 'location_name',
        'max_advance_days'
    ]

    __api_public__ = (
        'id', 'building', 'name', 'floor', 'longitude', 'latitude', ('number', 'roomNr'), ('location_name', 'location'),
        ('full_name', 'fullName'), ('booking_url', 'bookingUrl')
    )

    __api_minimal_public__ = (
        'id', ('full_name', 'fullName')
    )

    id = db.Column(
        db.Integer,
        primary_key=True
    )
    location_id = db.Column(
        db.Integer,
        db.ForeignKey('roombooking.locations.id'),
        nullable=False
    )
    photo_id = db.Column(
        db.Integer,
        db.ForeignKey('roombooking.photos.id')
    )
    name = db.Column(
        db.String,
        nullable=False
    )
    site = db.Column(
        db.String,
        default=''
    )
    division = db.Column(
        db.String
    )
    building = db.Column(
        db.String,
        nullable=False
    )
    floor = db.Column(
        db.String,
        default='',
        nullable=False
    )
    number = db.Column(
        db.String,
        default='',
        nullable=False
    )
    notification_before_days = db.Column(
        db.Integer
    )
    notification_before_days_weekly = db.Column(
        db.Integer
    )
    notification_before_days_monthly = db.Column(
        db.Integer
    )
    notification_for_assistance = db.Column(
        db.Boolean,
        nullable=False,
        default=False
    )
    reservations_need_confirmation = db.Column(
        db.Boolean,
        nullable=False,
        default=False
    )
    notifications_enabled = db.Column(
        db.Boolean,
        nullable=False,
        default=True
    )
    telephone = db.Column(
        db.String
    )
    key_location = db.Column(
        db.String
    )
    capacity = db.Column(
        db.Integer,
        default=20
    )
    surface_area = db.Column(
        db.Integer
    )
    latitude = db.Column(
        db.String
    )
    longitude = db.Column(
        db.String
    )
    comments = db.Column(
        db.String
    )
    owner_id = db.Column(
        db.Integer,
        db.ForeignKey('users.users.id'),
        index=True,
        nullable=False
    )
    is_active = db.Column(
        db.Boolean,
        nullable=False,
        default=True,
        index=True
    )
    is_reservable = db.Column(
        db.Boolean,
        nullable=False,
        default=True
    )
    max_advance_days = db.Column(
        db.Integer
    )
    booking_limit_days = db.Column(
        db.Integer
    )

    attributes = db.relationship(
        'RoomAttributeAssociation',
        backref='room',
        cascade='all, delete-orphan',
        lazy='dynamic'
    )

    blocked_rooms = db.relationship(
        'BlockedRoom',
        backref='room',
        cascade='all, delete-orphan',
        lazy='dynamic'
    )

    bookable_hours = db.relationship(
        'BookableHours',
        backref='room',
        order_by=BookableHours.start_time,
        cascade='all, delete-orphan',
        lazy='dynamic'
    )

    available_equipment = db.relationship(
        'EquipmentType',
        secondary=RoomEquipmentAssociation,
        backref='rooms',
        lazy='dynamic'
    )

    nonbookable_periods = db.relationship(
        'NonBookablePeriod',
        backref='room',
        order_by=NonBookablePeriod.end_dt.desc(),
        cascade='all, delete-orphan',
        lazy='dynamic'
    )

    photo = db.relationship(
        'Photo',
        backref='room',
        cascade='all, delete-orphan',
        single_parent=True,
        lazy=True
    )

    reservations = db.relationship(
        'Reservation',
        backref='room',
        cascade='all, delete-orphan',
        lazy='dynamic'
    )

    #: The owner of the room. If the room has the `manager-group`
    #: attribute set, any users in that group are also considered
    #: owners when it comes to management privileges.
    #: Use :meth:`is_owned_by` for ownership checks that should
    #: also check against the management group.
    owner = db.relationship(
        'User',
        # subquery load since a normal joinedload breaks `get_with_data`
        lazy='subquery',
        backref=db.backref(
            'owned_rooms',
            lazy='dynamic'
        )
    )

    # relationship backrefs:
    # - breaks (Break.own_room)
    # - contributions (Contribution.own_room)
    # - events (Event.own_room)
    # - location (Location.rooms)
    # - session_blocks (SessionBlock.own_room)
    # - sessions (Session.own_room)

    @hybrid_property
    def is_auto_confirm(self):
        return not self.reservations_need_confirmation

    @is_auto_confirm.expression
    def is_auto_confirm(self):
        return ~self.reservations_need_confirmation

    @property
    def booking_url(self):
        if self.id is None:
            return None
        return url_for('rooms.room_book', self)

    @property
    def details_url(self):
        if self.id is None:
            return None
        return url_for('rooms.roomBooking-roomDetails', self)

    @property
    def large_photo_url(self):
        if self.id is None:
            return None
        return url_for('rooms.photo', self, size='large')

    @property
    def small_photo_url(self):
        if self.id is None:
            return None
        return url_for('rooms.photo', self, size='small')

    @property
    def map_url(self):
        if self.location.map_url_template:
            return self.location.map_url_template.format(
                building=self.building,
                floor=self.floor,
                number=self.number
            )
        else:
            return None

    @property
    def has_photo(self):
        return self.photo_id is not None

    @property
    def full_name(self):
        if self.has_special_name:
            return u'{} - {}'.format(self.generate_name(), self.name)
        else:
            return u'{}'.format(self.generate_name())

    @property
    def has_special_name(self):
        return self.name and self.name != self.generate_name()

    @property
    @cached(_cache)
    def has_booking_groups(self):
        return self.has_attribute('allowed-booking-group')

    @property
    @cached(_cache)
    def has_projector(self):
        return self.has_equipment(u'Computer Projector', u'Video projector 4:3', u'Video projector 16:9')

    @property
    @cached(_cache)
    def has_webcast_recording(self):
        return self.has_equipment('Webcast/Recording')

    @property
    @cached(_cache)
    def has_vc(self):
        return self.has_equipment('Video conference')

    @property
    @cached(_cache)
    def is_public(self):
        return self.is_reservable and not self.has_booking_groups

    @property
    def kind(self):
        if not self.is_reservable or self.has_booking_groups:
            return 'privateRoom'
        elif self.reservations_need_confirmation:
            return 'moderatedRoom'
        else:
            return 'basicRoom'

    @property
    def location_name(self):
        return self.location.name

    @property
    def marker_description(self):
        infos = []

        infos.append(u'{capacity} {label}'.format(capacity=self.capacity,
                                                  label=_(u'person') if self.capacity == 1 else _(u'people')))
        infos.append(_(u'public') if self.is_public else _(u'private'))
        infos.append(_(u'auto-confirmation') if self.is_auto_confirm else _(u'needs confirmation'))
        if self.has_vc:
            infos.append(_(u'videoconference'))

        return u', '.join(map(unicode, infos))

    @property
    def manager_emails(self):
        manager_group = self.get_attribute_value('manager-group')
        if not manager_group:
            return set()
        group = GroupProxy.get_named_default_group(manager_group)
        return {u.email for u in group.get_members()}

    @property
    def notification_emails(self):
        return set(filter(None, map(unicode.strip, self.get_attribute_value(u'notification-email', u'').split(u','))))

    @return_ascii
    def __repr__(self):
        return u'<Room({0}, {1}, {2})>'.format(
            self.id,
            self.location_id,
            self.name
        )

    @cached(_cache)
    def has_equipment(self, *names):
        return self.available_equipment.filter(EquipmentType.name.in_(names)).count() > 0

    def find_available_vc_equipment(self):
        vc_equipment = (self.available_equipment
                        .correlate(Room)
                        .with_entities(EquipmentType.id)
                        .filter_by(name='Video conference')
                        .as_scalar())
        return self.available_equipment.filter(EquipmentType.parent_id == vc_equipment)

    def get_attribute_by_name(self, attribute_name):
        return (self.attributes
                .join(RoomAttribute)
                .filter(RoomAttribute.name == attribute_name)
                .first())

    def has_attribute(self, attribute_name):
        return self.get_attribute_by_name(attribute_name) is not None

    @cached(_cache)
    def get_attribute_value(self, name, default=None):
        attr = self.get_attribute_by_name(name)
        return attr.value if attr else default

    def set_attribute_value(self, name, value):
        attr = self.get_attribute_by_name(name)
        if attr:
            if value:
                attr.value = value
            else:
                self.attributes.filter(RoomAttributeAssociation.attribute_id == attr.attribute_id) \
                    .delete(synchronize_session='fetch')
        elif value:
            attr = self.location.get_attribute_by_name(name)
            if not attr:
                raise ValueError("Attribute {} not supported in location {}".format(name, self.location_name))
            attr_assoc = RoomAttributeAssociation()
            attr_assoc.value = value
            attr_assoc.attribute = attr
            self.attributes.append(attr_assoc)
        db.session.flush()

    @locator_property
    def locator(self):
        return {'roomLocation': self.location_name, 'roomID': self.id}

    def generate_name(self):
        return u'{}-{}-{}'.format(
            self.building,
            self.floor,
            self.number
        )

    def update_name(self):
        if not self.has_special_name and self.building and self.floor and self.number:
            self.name = self.generate_name()

    @classmethod
    def find_all(cls, *args, **kwargs):
        """Retrieves rooms, sorted by location and full name"""
        rooms = super(Room, cls).find_all(*args, **kwargs)
        rooms.sort(key=lambda r: natural_sort_key(r.location_name + r.full_name))
        return rooms

    @classmethod
    def find_with_attribute(cls, attribute):
        """Search rooms which have a specific attribute"""
        return (Room.query
                .with_entities(Room, RoomAttributeAssociation.value)
                .join(Room.attributes, RoomAttributeAssociation.attribute)
                .filter(RoomAttribute.name == attribute)
                .all())

    @staticmethod
    def get_with_data(*args, **kwargs):
        from indico.modules.rb.models.locations import Location

        only_active = kwargs.pop('only_active', True)
        filters = kwargs.pop('filters', None)
        order = kwargs.pop('order', [Location.name, Room.building, Room.floor, Room.number, Room.name])
        if kwargs:
            raise ValueError('Unexpected kwargs: {}'.format(kwargs))

        query = Room.query
        entities = [Room]

        if 'equipment' in args:
            entities.append(static_array.array_agg(EquipmentType.name))
            query = query.outerjoin(RoomEquipmentAssociation).outerjoin(EquipmentType)
        if 'vc_equipment' in args or 'non_vc_equipment' in args:
            vc_id_subquery = db.session.query(EquipmentType.id) \
                                       .correlate(Room) \
                                       .filter_by(name='Video conference') \
                                       .join(RoomEquipmentAssociation) \
                                       .filter(RoomEquipmentAssociation.c.room_id == Room.id) \
                                       .as_scalar()

            if 'vc_equipment' in args:
                # noinspection PyTypeChecker
                entities.append(static_array.array(
                    db.session.query(EquipmentType.name)
                    .join(RoomEquipmentAssociation)
                    .filter(
                        RoomEquipmentAssociation.c.room_id == Room.id,
                        EquipmentType.parent_id == vc_id_subquery
                    )
                    .order_by(EquipmentType.name)
                    .as_scalar()
                ))
            if 'non_vc_equipment' in args:
                # noinspection PyTypeChecker
                entities.append(static_array.array(
                    db.session.query(EquipmentType.name)
                    .join(RoomEquipmentAssociation)
                    .filter(
                        RoomEquipmentAssociation.c.room_id == Room.id,
                        (EquipmentType.parent_id == None) | (EquipmentType.parent_id != vc_id_subquery)
                    )
                    .order_by(EquipmentType.name)
                    .as_scalar()
                ))

        query = (query.with_entities(*entities)
                 .outerjoin(Location, Location.id == Room.location_id)
                 .group_by(Location.name, Room.id))

        if only_active:
            query = query.filter(Room.is_active)
        if filters:  # pragma: no cover
            query = query.filter(*filters)
        if order:  # pragma: no cover
            query = query.order_by(*order)

        keys = ('room',) + tuple(args)
        return (dict(zip(keys, row if args else [row])) for row in query)

    @classproperty
    @staticmethod
    def max_capacity():
        return db.session.query(db.func.max(Room.capacity)).scalar() or 0

    @staticmethod
    def filter_available(start_dt, end_dt, repetition, include_pre_bookings=True, include_pending_blockings=True):
        """Returns a SQLAlchemy filter criterion ensuring that the room is available during the given time."""
        # Check availability against reservation occurrences
        dummy_occurrences = ReservationOccurrence.create_series(start_dt, end_dt, repetition)
        overlap_criteria = ReservationOccurrence.filter_overlap(dummy_occurrences)
        reservation_criteria = [Reservation.room_id == Room.id,
                                ReservationOccurrence.is_valid,
                                overlap_criteria]
        if not include_pre_bookings:
            reservation_criteria.append(Reservation.is_accepted)
        occurrences_filter = Reservation.occurrences.any(and_(*reservation_criteria))
        # Check availability against blockings
        if include_pending_blockings:
            valid_states = (BlockedRoom.State.accepted, BlockedRoom.State.pending)
        else:
            valid_states = (BlockedRoom.State.accepted,)
        blocking_criteria = [BlockedRoom.blocking_id == Blocking.id,
                             BlockedRoom.state.in_(valid_states),
                             Blocking.start_date <= start_dt.date(),
                             Blocking.end_date >= end_dt.date()]
        blockings_filter = Room.blocked_rooms.any(and_(*blocking_criteria))
        return ~occurrences_filter & ~blockings_filter

    @staticmethod
    def find_with_filters(filters, user=None):
        from indico.modules.rb.models.locations import Location

        equipment_count = len(filters.get('available_equipment', ()))
        equipment_subquery = None
        if equipment_count:
            equipment_subquery = (
                db.session.query(RoomEquipmentAssociation)
                .with_entities(func.count(RoomEquipmentAssociation.c.room_id))
                .filter(
                    RoomEquipmentAssociation.c.room_id == Room.id,
                    RoomEquipmentAssociation.c.equipment_id.in_(eq.id for eq in filters['available_equipment'])
                )
                .correlate(Room)
                .as_scalar()
            )

        capacity = filters.get('capacity')
        q = (
            Room.query
            .join(Location.rooms)
            .filter(
                Location.id == filters['location'].id if filters.get('location') else True,
                ((Room.capacity >= (capacity * 0.8)) | (Room.capacity == None)) if capacity else True,
                Room.is_reservable if filters.get('is_only_public') else True,
                Room.is_auto_confirm if filters.get('is_auto_confirm') else True,
                Room.is_active if filters.get('is_only_active', False) else True,
                (equipment_subquery == equipment_count) if equipment_subquery is not None else True)
        )

        if filters.get('available', -1) != -1:
            repetition = RepeatMapping.convert_legacy_repeatability(ast.literal_eval(filters['repeatability']))
            is_available = Room.filter_available(filters['start_dt'], filters['end_dt'], repetition,
                                                 include_pre_bookings=filters.get('include_pre_bookings', True),
                                                 include_pending_blockings=filters.get('include_pending_blockings',
                                                                                       True))
            # Filter the search results
            if filters['available'] == 0:  # booked/unavailable
                q = q.filter(~is_available)
            elif filters['available'] == 1:  # available
                q = q.filter(is_available)
            else:
                raise ValueError('Unexpected availability value')

        free_search_columns = (
            'name', 'site', 'division', 'building', 'floor', 'number', 'telephone', 'key_location', 'comments'
        )
        if filters.get('details'):
            # Attributes are stored JSON-encoded, so we need to JSON-encode the provided string and remove the quotes
            # afterwards since PostgreSQL currently does not expose a function to decode a JSON string:
            # http://www.postgresql.org/message-id/[email protected]
            details = filters['details'].lower()
            details_str = u'%{}%'.format(escape_like(details))
            details_json = u'%{}%'.format(escape_like(json.dumps(details)[1:-1]))
            free_search_criteria = [getattr(Room, c).ilike(details_str) for c in free_search_columns]
            free_search_criteria.append(Room.attributes.any(cast(RoomAttributeAssociation.value, db.String)
                                                            .ilike(details_json)))
            q = q.filter(or_(*free_search_criteria))

        q = q.order_by(Room.capacity)
        rooms = q.all()
        # Apply a bunch of filters which are *much* easier to do here than in SQL!
        if filters.get('is_only_public'):
            # This may trigger additional SQL queries but is_public is cached and doing this check here is *much* easier
            rooms = [r for r in rooms if r.is_public]
        if filters.get('is_only_my_rooms'):
            assert user is not None
            rooms = [r for r in rooms if r.is_owned_by(user)]
        if capacity:
            # Unless it would result in an empty resultset we don't want to show rooms with >20% more capacity
            # than requested. This cannot be done easily in SQL so we do that logic here after the SQL query already
            # weeded out rooms that are too small
            matching_capacity_rooms = [r for r in rooms if r.capacity is None or r.capacity <= capacity * 1.2]
            if matching_capacity_rooms:
                rooms = matching_capacity_rooms
        return rooms

    def has_live_reservations(self):
        return self.reservations.filter_by(
            is_archived=False,
            is_cancelled=False,
            is_rejected=False
        ).count() > 0

    def get_blocked_rooms(self, *dates, **kwargs):
        states = kwargs.get('states', (BlockedRoom.State.accepted,))
        return (self.blocked_rooms
                .join(BlockedRoom.blocking)
                .options(contains_eager(BlockedRoom.blocking))
                .filter(or_(Blocking.is_active_at(d) for d in dates),
                        BlockedRoom.state.in_(states))
                .all())

    @unify_user_args
    def _can_be_booked(self, user, prebook=False, ignore_admin=False):
        if not user or not rb_check_user_access(user):
            return False

        if (not ignore_admin and rb_is_admin(user)) or (self.is_owned_by(user) and self.is_active):
            return True

        if self.is_active and self.is_reservable and (prebook or not self.reservations_need_confirmation):
            group_name = self.get_attribute_value('allowed-booking-group')
            if not group_name or user in GroupProxy.get_named_default_group(group_name):
                return True

        return False

    def can_be_booked(self, user, ignore_admin=False):
        """
        Reservable rooms which does not require pre-booking can be booked by anyone.
        Other rooms - only by their responsibles.
        """
        return self._can_be_booked(user, ignore_admin=ignore_admin)

    def can_be_prebooked(self, user, ignore_admin=False):
        """
        Reservable rooms can be pre-booked by anyone.
        Other rooms - only by their responsibles.
        """
        return self._can_be_booked(user, prebook=True, ignore_admin=ignore_admin)

    def can_be_overridden(self, user):
        if not user:
            return False
        return rb_is_admin(user) or self.is_owned_by(user)

    def can_be_modified(self, user):
        """Only admin can modify rooms."""
        if not user:
            return False
        return rb_is_admin(user)

    def can_be_deleted(self, user):
        return self.can_be_modified(user)

    @unify_user_args
    @cached(_cache)
    def is_owned_by(self, user):
        """Checks if the user is managing the room (owner or manager)"""
        if self.owner == user:
            return True
        manager_group = self.get_attribute_value('manager-group')
        if not manager_group:
            return False
        return user in GroupProxy.get_named_default_group(manager_group)

    @classmethod
    def get_owned_by(cls, user):
        return [room for room in cls.find(is_active=True) if room.is_owned_by(user)]

    @classmethod
    def user_owns_rooms(cls, user):
        return any(room for room in cls.find(is_active=True) if room.is_owned_by(user))

    def check_advance_days(self, end_date, user=None, quiet=False):
        if not self.max_advance_days:
            return True
        if user and (rb_is_admin(user) or self.is_owned_by(user)):
            return True
        advance_days = (end_date - date.today()).days
        ok = advance_days < self.max_advance_days
        if quiet or ok:
            return ok
        else:
            msg = _(u'You cannot book this room more than {} days in advance')
            raise NoReportError(msg.format(self.max_advance_days))

    def check_bookable_hours(self, start_time, end_time, user=None, quiet=False):
        if user and (rb_is_admin(user) or self.is_owned_by(user)):
            return True
        bookable_hours = self.bookable_hours.all()
        if not bookable_hours:
            return True
        for bt in bookable_hours:
            if bt.fits_period(start_time, end_time):
                return True
        if quiet:
            return False
        raise NoReportError(u'Room cannot be booked at this time')
Example #28
0
class PaperReview(ProposalReviewMixin, RenderModeMixin, db.Model):
    """A paper review, emitted by a layout or content reviewer."""

    possible_render_modes = {RenderMode.markdown}
    default_render_mode = RenderMode.markdown

    revision_attr = 'revision'
    group_attr = 'type'
    group_proxy_cls = PaperTypeProxy

    __tablename__ = 'reviews'
    __table_args__ = (db.UniqueConstraint('revision_id', 'user_id', 'type'), {
        'schema': 'event_paper_reviewing'
    })
    TIMELINE_TYPE = 'review'

    id = db.Column(db.Integer, primary_key=True)
    revision_id = db.Column(
        db.Integer,
        db.ForeignKey('event_paper_reviewing.revisions.id'),
        index=True,
        nullable=False)
    user_id = db.Column(db.Integer,
                        db.ForeignKey('users.users.id'),
                        index=True,
                        nullable=False)
    created_dt = db.Column(
        UTCDateTime,
        nullable=False,
        default=now_utc,
    )
    modified_dt = db.Column(UTCDateTime, nullable=True)
    _comment = db.Column('comment', db.Text, nullable=False, default='')
    type = db.Column(PyIntEnum(PaperReviewType), nullable=False)
    proposed_action = db.Column(PyIntEnum(PaperAction), nullable=False)

    revision = db.relationship('PaperRevision',
                               lazy=True,
                               backref=db.backref('reviews',
                                                  lazy=True,
                                                  order_by=created_dt.desc()))
    user = db.relationship('User',
                           lazy=True,
                           backref=db.backref('paper_reviews', lazy='dynamic'))

    # relationship backrefs:
    # - ratings (PaperReviewRating.review)

    comment = RenderModeMixin.create_hybrid_property('_comment')

    @locator_property
    def locator(self):
        return dict(self.revision.locator, review_id=self.id)

    @return_ascii
    def __repr__(self):
        return format_repr(self,
                           'id',
                           'type',
                           'revision_id',
                           'user_id',
                           proposed_action=None)

    def can_edit(self, user, check_state=False):
        from indico.modules.events.papers.models.revisions import PaperRevisionState
        if user is None:
            return False
        if check_state and self.revision.state != PaperRevisionState.submitted:
            return False
        return self.user == user

    def can_view(self, user):
        if user is None:
            return False
        elif user == self.user:
            return True
        elif self.revision.paper.can_judge(user):
            return True
        return False

    @property
    def visibility(self):
        return PaperCommentVisibility.reviewers

    @property
    def score(self):
        ratings = [
            r for r in self.ratings if not r.question.is_deleted
            and r.question.field_type == 'rating' and r.value is not None
        ]
        if not ratings:
            return None
        return sum(x.value for x in ratings) / len(ratings)
Example #29
0
class Track(DescriptionMixin, db.Model):
    __tablename__ = 'tracks'
    __table_args__ = {'schema': 'events'}

    possible_render_modes = {RenderMode.markdown}
    default_render_mode = RenderMode.markdown

    id = db.Column(
        db.Integer,
        primary_key=True
    )
    title = db.Column(
        db.String,
        nullable=False
    )
    code = db.Column(
        db.String,
        nullable=False,
        default=''
    )
    event_id = db.Column(
        db.Integer,
        db.ForeignKey('events.events.id'),
        index=True,
        nullable=False
    )
    position = db.Column(
        db.Integer,
        nullable=False,
        default=_get_next_position
    )

    event_new = db.relationship(
        'Event',
        lazy=True,
        backref=db.backref(
            'tracks',
            cascade='all, delete-orphan',
            lazy=True,
            order_by=position
        )
    )
    abstract_reviewers = db.relationship(
        'User',
        secondary='events.track_abstract_reviewers',
        collection_class=set,
        lazy=True,
        backref=db.backref(
            'abstract_reviewer_for_tracks',
            collection_class=set,
            lazy=True
        )
    )
    conveners = db.relationship(
        'User',
        secondary='events.track_conveners',
        collection_class=set,
        lazy=True,
        backref=db.backref(
            'convener_for_tracks',
            collection_class=set,
            lazy=True
        )
    )

    # relationship backrefs:
    # - abstract_reviews (AbstractReview.track)
    # - abstracts_accepted (Abstract.accepted_track)
    # - abstracts_reviewed (Abstract.reviewed_for_tracks)
    # - abstracts_submitted (Abstract.submitted_for_tracks)
    # - contributions (Contribution.track)
    # - proposed_abstract_reviews (AbstractReview.proposed_tracks)

    @property
    def short_title(self):
        return self.code if self.code else self.title

    @property
    def full_title(self):
        return '{} - {}'.format(self.code, self.title) if self.code else self.title

    @locator_property
    def locator(self):
        return dict(self.event_new.locator, track_id=self.id)

    @return_ascii
    def __repr__(self):
        return format_repr(self, 'id', _text=text_to_repr(self.title))

    def can_delete(self, user):
        return self.event_new.can_manage(user) and not self.abstracts_accepted

    def can_review_abstracts(self, user):
        if not user:
            return False
        elif not self.event_new.can_manage(user, role='abstract_reviewer', explicit_role=True):
            return False
        elif user in self.event_new.global_abstract_reviewers:
            return True
        elif user in self.abstract_reviewers:
            return True
        else:
            return False

    def can_convene(self, user):
        if not user:
            return False
        elif not self.event_new.can_manage(user, role='track_convener', explicit_role=True):
            return False
        elif user in self.event_new.global_conveners:
            return True
        elif user in self.conveners:
            return True
        else:
            return False
Example #30
0
class LiveSyncQueueEntry(db.Model):
    __tablename__ = 'queues'
    __table_args__ = tuple(_make_checks()) + ({'schema': 'plugin_livesync'}, )

    #: Entry ID
    id = db.Column(db.Integer, primary_key=True)

    #: ID of the agent this entry belongs to
    agent_id = db.Column(db.Integer,
                         db.ForeignKey('plugin_livesync.agents.id'),
                         nullable=False,
                         index=True)

    #: Timestamp of the change
    timestamp = db.Column(UTCDateTime, nullable=False, default=now_utc)

    #: if this record has already been processed
    processed = db.Column(db.Boolean, nullable=False, default=False)

    #: the change type, a :class:`ChangeType`
    change = db.Column(PyIntEnum(ChangeType), nullable=False)

    #: The type of the changed object
    type = db.Column(PyIntEnum(EntryType), nullable=False)

    #: The ID of the changed category
    category_id = db.Column(db.Integer,
                            db.ForeignKey('categories.categories.id'),
                            index=True,
                            nullable=True)

    #: ID of the changed event
    event_id = db.Column(db.Integer,
                         db.ForeignKey('events.events.id'),
                         index=True,
                         nullable=True)

    #: ID of the changed contribution
    contrib_id = db.Column('contribution_id',
                           db.Integer,
                           db.ForeignKey('events.contributions.id'),
                           index=True,
                           nullable=True)

    #: ID of the changed session
    session_id = db.Column('session_id',
                           db.Integer,
                           db.ForeignKey('events.sessions.id'),
                           index=True,
                           nullable=True)

    #: ID of the changed subcontribution
    subcontrib_id = db.Column('subcontribution_id',
                              db.Integer,
                              db.ForeignKey('events.subcontributions.id'),
                              index=True,
                              nullable=True)

    #: The associated :class:LiveSyncAgent
    agent = db.relationship('LiveSyncAgent',
                            backref=db.backref('queue',
                                               cascade='all, delete-orphan',
                                               lazy='dynamic'))

    category = db.relationship('Category',
                               lazy=True,
                               backref=db.backref('livesync_queue_entries',
                                                  cascade='all, delete-orphan',
                                                  lazy=True))

    event = db.relationship('Event',
                            lazy=True,
                            backref=db.backref('livesync_queue_entries',
                                               cascade='all, delete-orphan',
                                               lazy=True))

    session = db.relationship('Session',
                              lazy=False,
                              backref=db.backref('livesync_queue_entries',
                                                 cascade='all, delete-orphan',
                                                 lazy='dynamic'))

    contribution = db.relationship('Contribution',
                                   lazy=False,
                                   backref=db.backref(
                                       'livesync_queue_entries',
                                       cascade='all, delete-orphan',
                                       lazy='dynamic'))

    subcontribution = db.relationship('SubContribution',
                                      lazy=False,
                                      backref=db.backref(
                                          'livesync_queue_entries',
                                          cascade='all, delete-orphan',
                                          lazy='dynamic'))

    @property
    def object(self):
        """Return the changed object."""
        if self.type == EntryType.category:
            return self.category
        elif self.type == EntryType.event:
            return self.event
        elif self.type == EntryType.session:
            return self.session
        elif self.type == EntryType.contribution:
            return self.contribution
        elif self.type == EntryType.subcontribution:
            return self.subcontribution

    @property
    def object_ref(self):
        """Return the reference of the changed object."""
        return ImmutableDict(type=self.type,
                             category_id=self.category_id,
                             event_id=self.event_id,
                             session_id=self.session_id,
                             contrib_id=self.contrib_id,
                             subcontrib_id=self.subcontrib_id)

    @return_ascii
    def __repr__(self):
        return format_repr(self,
                           'id',
                           'agent_id',
                           'change',
                           'type',
                           category_id=None,
                           event_id=None,
                           session_id=None,
                           contrib_id=None,
                           subcontrib_id=None)

    @classmethod
    def create(cls, changes, ref, excluded_categories=set()):
        """Create a new change in all queues.

        :param changes: the change types, an iterable containing
                        :class:`ChangeType`
        :param ref: the object reference (returned by `obj_ref`)
                        of the changed object
        :param excluded_categories: set of categories (IDs) whose items
                                    will not be tracked
        """
        ref = dict(ref)
        obj = obj_deref(ref)

        if isinstance(obj, Category):
            if any(c.id in excluded_categories for c in obj.chain_query):
                return
        else:
            event = obj if isinstance(obj, Event) else obj.event
            if event.category not in g.setdefault(
                    'livesync_excluded_categories_checked', {}):
                g.livesync_excluded_categories_checked[
                    event.category] = excluded_categories & set(
                        event.category_chain)
            if g.livesync_excluded_categories_checked[event.category]:
                return

        try:
            agents = g.livesync_agents
        except AttributeError:
            agents = g.livesync_agents = LiveSyncAgent.query.all()

        for change in changes:
            for agent in agents:
                entry = cls(agent=agent, change=change, **ref)
                db.session.add(entry)

        db.session.flush()
Example #31
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 #32
0
class AbstractReview(ProposalReviewMixin, RenderModeMixin, db.Model):
    """An abstract review, emitted by a reviewer."""

    possible_render_modes = {RenderMode.markdown}
    default_render_mode = RenderMode.markdown

    revision_attr = 'abstract'
    group_attr = 'track'

    marshmallow_aliases = {'_comment': 'comment'}

    __tablename__ = 'abstract_reviews'
    __table_args__ = (
        db.UniqueConstraint('abstract_id', 'user_id', 'track_id'),
        db.CheckConstraint(
            "proposed_action = {} OR (proposed_contribution_type_id IS NULL)".
            format(AbstractAction.accept),
            name='prop_contrib_id_only_accepted'),
        db.CheckConstraint(
            "(proposed_action IN ({}, {})) = (proposed_related_abstract_id IS NOT NULL)"
            .format(AbstractAction.mark_as_duplicate, AbstractAction.merge),
            name='prop_abstract_id_only_duplicate_merge'), {
                'schema': 'event_abstracts'
            })

    id = db.Column(db.Integer, primary_key=True)
    abstract_id = db.Column(db.Integer,
                            db.ForeignKey('event_abstracts.abstracts.id'),
                            index=True,
                            nullable=False)
    user_id = db.Column(db.Integer,
                        db.ForeignKey('users.users.id'),
                        index=True,
                        nullable=False)
    track_id = db.Column(db.Integer,
                         db.ForeignKey('events.tracks.id'),
                         index=True,
                         nullable=True)
    created_dt = db.Column(
        UTCDateTime,
        nullable=False,
        default=now_utc,
    )
    modified_dt = db.Column(UTCDateTime, nullable=True)
    _comment = db.Column('comment', db.Text, nullable=False, default='')
    proposed_action = db.Column(PyIntEnum(AbstractAction), nullable=False)
    proposed_related_abstract_id = db.Column(
        db.Integer,
        db.ForeignKey('event_abstracts.abstracts.id'),
        index=True,
        nullable=True)
    proposed_contribution_type_id = db.Column(
        db.Integer,
        db.ForeignKey('events.contribution_types.id'),
        nullable=True,
        index=True)
    abstract = db.relationship('Abstract',
                               lazy=True,
                               foreign_keys=abstract_id,
                               backref=db.backref('reviews',
                                                  cascade='all, delete-orphan',
                                                  lazy=True))
    user = db.relationship('User',
                           lazy=True,
                           backref=db.backref('abstract_reviews',
                                              lazy='dynamic'))
    track = db.relationship('Track',
                            lazy=True,
                            foreign_keys=track_id,
                            backref=db.backref('abstract_reviews',
                                               lazy='dynamic'))
    proposed_related_abstract = db.relationship(
        'Abstract',
        lazy=True,
        foreign_keys=proposed_related_abstract_id,
        backref=db.backref('proposed_related_abstract_reviews',
                           lazy='dynamic'))
    proposed_tracks = db.relationship(
        'Track',
        secondary='event_abstracts.proposed_for_tracks',
        lazy=True,
        collection_class=set,
        backref=db.backref('proposed_abstract_reviews',
                           lazy='dynamic',
                           passive_deletes=True))
    proposed_contribution_type = db.relationship('ContributionType',
                                                 lazy=True,
                                                 backref=db.backref(
                                                     'abstract_reviews',
                                                     lazy='dynamic'))

    # relationship backrefs:
    # - ratings (AbstractReviewRating.review)

    comment = RenderModeMixin.create_hybrid_property('_comment')

    @locator_property
    def locator(self):
        return dict(self.abstract.locator, review_id=self.id)

    @return_ascii
    def __repr__(self):
        return format_repr(self,
                           'id',
                           'abstract_id',
                           'user_id',
                           proposed_action=None)

    @property
    def visibility(self):
        return AbstractCommentVisibility.reviewers

    @property
    def score(self):
        ratings = [
            r for r in self.ratings if not r.question.no_score
            and not r.question.is_deleted and r.value is not None
        ]
        if not ratings:
            return None
        return sum(x.value for x in ratings) / len(ratings)

    def can_edit(self, user, check_state=False):
        if user is None:
            return False
        if check_state and self.abstract.public_state.name != 'under_review':
            return False
        return self.user == user

    def can_view(self, user):
        if user is None:
            return False
        elif user == self.user:
            return True
        if self.abstract.can_judge(user):
            return True
        else:
            return self.track.can_convene(user)
Example #33
0
class Room(ProtectionManagersMixin, db.Model, Serializer):
    __tablename__ = 'rooms'
    __table_args__ = (
        db.UniqueConstraint(
            'id',
            'location_id'),  # useless but needed for the LocationMixin fkey
        db.CheckConstraint("verbose_name != ''", 'verbose_name_not_empty'),
        {
            'schema': 'roombooking'
        })

    default_protection_mode = ProtectionMode.public
    disallowed_protection_modes = frozenset({ProtectionMode.inheriting})

    __api_public__ = ('id', 'building', 'name', 'floor', 'longitude',
                      'latitude', ('number', 'roomNr'),
                      ('location_name', 'location'), ('full_name', 'fullName'))

    __api_minimal_public__ = ('id', ('full_name', 'fullName'))

    id = db.Column(db.Integer, primary_key=True)
    location_id = db.Column(db.Integer,
                            db.ForeignKey('roombooking.locations.id'),
                            nullable=False)
    photo_id = db.Column(db.Integer, db.ForeignKey('roombooking.photos.id'))
    #: Verbose name for the room (long)
    verbose_name = db.Column(db.String, nullable=True, default=None)
    site = db.Column(db.String, default='')
    division = db.Column(db.String)
    building = db.Column(db.String, nullable=False)
    floor = db.Column(db.String, default='', nullable=False)
    number = db.Column(db.String, default='', nullable=False)
    notification_emails = db.Column(ARRAY(db.String),
                                    nullable=False,
                                    default=[])
    notification_before_days = db.Column(db.Integer)
    notification_before_days_weekly = db.Column(db.Integer)
    notification_before_days_monthly = db.Column(db.Integer)
    end_notification_daily = db.Column(db.Integer, nullable=True)
    end_notification_weekly = db.Column(db.Integer, nullable=True)
    end_notification_monthly = db.Column(db.Integer, nullable=True)
    reservations_need_confirmation = db.Column(db.Boolean,
                                               nullable=False,
                                               default=False)
    notifications_enabled = db.Column(db.Boolean, nullable=False, default=True)
    end_notifications_enabled = db.Column(db.Boolean,
                                          nullable=False,
                                          default=True)
    telephone = db.Column(db.String, nullable=False, default='')
    key_location = db.Column(db.String, nullable=False, default='')
    capacity = db.Column(db.Integer, default=20)
    surface_area = db.Column(db.Integer)
    longitude = db.Column(db.Float)
    latitude = db.Column(db.Float)
    comments = db.Column(db.String, nullable=False, default='')
    owner_id = db.Column(db.Integer,
                         db.ForeignKey('users.users.id'),
                         index=True,
                         nullable=False)
    is_deleted = db.Column(
        db.Boolean,
        nullable=False,
        default=False,
    )
    is_reservable = db.Column(db.Boolean, nullable=False, default=True)
    max_advance_days = db.Column(db.Integer)
    booking_limit_days = db.Column(db.Integer)

    location = db.relationship('Location', back_populates='rooms', lazy=True)

    acl_entries = db.relationship('RoomPrincipal',
                                  lazy=True,
                                  backref='room',
                                  cascade='all, delete-orphan',
                                  collection_class=set)

    attributes = db.relationship('RoomAttributeAssociation',
                                 backref='room',
                                 cascade='all, delete-orphan',
                                 lazy='dynamic')

    blocked_rooms = db.relationship('BlockedRoom',
                                    backref='room',
                                    cascade='all, delete-orphan',
                                    lazy='dynamic')

    bookable_hours = db.relationship('BookableHours',
                                     backref='room',
                                     order_by=BookableHours.start_time,
                                     cascade='all, delete-orphan',
                                     lazy='dynamic')

    available_equipment = db.relationship('EquipmentType',
                                          secondary=RoomEquipmentAssociation,
                                          backref='rooms',
                                          lazy=True)

    nonbookable_periods = db.relationship(
        'NonBookablePeriod',
        backref='room',
        order_by=NonBookablePeriod.end_dt.desc(),
        cascade='all, delete-orphan',
        lazy='dynamic')

    photo = db.relationship('Photo',
                            backref='room',
                            cascade='all, delete-orphan',
                            single_parent=True,
                            lazy=True)

    reservations = db.relationship('Reservation',
                                   backref='room',
                                   cascade='all, delete-orphan',
                                   lazy='dynamic')

    favorite_of = db.relationship(
        'User',
        secondary=favorite_room_table,
        lazy=True,
        collection_class=set,
        backref=db.backref('favorite_rooms', lazy=True, collection_class=set),
    )

    #: The owner of the room. This is purely informational and does not grant
    #: any permissions on the room.
    owner = db.relationship(
        'User',
        # subquery load since a normal joinedload breaks `get_with_data`
        lazy='subquery',
        backref=db.backref('owned_rooms', lazy='dynamic'))

    # relationship backrefs:
    # - breaks (Break.own_room)
    # - contributions (Contribution.own_room)
    # - events (Event.own_room)
    # - session_blocks (SessionBlock.own_room)
    # - sessions (Session.own_room)

    @hybrid_property
    def is_auto_confirm(self):
        return not self.reservations_need_confirmation

    @is_auto_confirm.expression
    def is_auto_confirm(self):
        return ~self.reservations_need_confirmation

    @property
    def details_url(self):
        if self.id is None:
            return None
        return url_for('rb.room_link', room_id=self.id)

    @property
    def map_url(self):
        if not self.location.map_url_template:
            return None
        return self.location.map_url_template.format(
            id=self.id,
            building=self.building,
            floor=self.floor,
            number=self.number,
            lat=self.latitude,
            lng=self.longitude,
        )

    @property
    def has_photo(self):
        return self.photo_id is not None

    @hybrid_property
    def name(self):
        return self.generate_name()

    @name.expression
    def name(cls):
        q = (db.session.query(db.m.Location.room_name_format).filter(
            db.m.Location.id == cls.location_id).correlate(Room).as_scalar())
        return db.func.format(q, cls.building, cls.floor, cls.number)

    @hybrid_property
    def full_name(self):
        if self.verbose_name:
            return u'{} - {}'.format(self.generate_name(), self.verbose_name)
        else:
            return u'{}'.format(self.generate_name())

    @full_name.expression
    def full_name(cls):
        return db.case([[
            cls.verbose_name.isnot(None), cls.name + ' - ' + cls.verbose_name
        ]],
                       else_=cls.name)

    @property
    def location_name(self):
        return self.location.name

    @property
    def sprite_position(self):
        sprite_mapping = _cache.get('rooms-sprite-mapping')
        return sprite_mapping.get(
            self.id, 0) if sprite_mapping else 0  # placeholder at position 0

    @return_ascii
    def __repr__(self):
        return format_repr(self, 'id', 'full_name', is_deleted=False)

    def has_equipment(self, *names):
        available = {x.name for x in self.available_equipment}
        return bool(available & set(names))

    def get_attribute_by_name(self, attribute_name):
        return (self.attributes.join(RoomAttribute).filter(
            RoomAttribute.name == attribute_name).first())

    def has_attribute(self, attribute_name):
        return self.get_attribute_by_name(attribute_name) is not None

    def get_attribute_value(self, name, default=None):
        attr = self.get_attribute_by_name(name)
        return attr.value if attr else default

    def set_attribute_value(self, name, value):
        attr = self.get_attribute_by_name(name)
        if attr:
            if value:
                attr.value = value
            else:
                self.attributes.filter(RoomAttributeAssociation.attribute_id == attr.attribute_id) \
                    .delete(synchronize_session='fetch')
        elif value:
            attr = RoomAttribute.query.filter_by(name=name).first()
            if not attr:
                raise ValueError("Attribute {} does not exist".format(name))
            attr_assoc = RoomAttributeAssociation()
            attr_assoc.value = value
            attr_assoc.attribute = attr
            self.attributes.append(attr_assoc)
        db.session.flush()

    def generate_name(self):
        if self.location is None:
            warnings.warn('Room has no location; using default name format')
            return '{}/{}-{}'.format(self.building, self.floor, self.number)
        return self.location.room_name_format.format(building=self.building,
                                                     floor=self.floor,
                                                     number=self.number)

    @classmethod
    def find_all(cls, *args, **kwargs):
        """Retrieves rooms, sorted by location and full name"""
        rooms = super(Room, cls).find_all(*args, **kwargs)
        rooms.sort(
            key=lambda r: natural_sort_key(r.location_name + r.full_name))
        return rooms

    @classmethod
    def find_with_attribute(cls, attribute):
        """Search rooms which have a specific attribute"""
        return (Room.query.with_entities(
            Room, RoomAttributeAssociation.value).join(
                Room.attributes, RoomAttributeAssociation.attribute).filter(
                    RoomAttribute.name == attribute).all())

    @staticmethod
    def get_with_data(*args, **kwargs):
        from indico.modules.rb.models.locations import Location

        only_active = kwargs.pop('only_active', True)
        filters = kwargs.pop('filters', None)
        order = kwargs.pop('order', [
            Location.name, Room.building, Room.floor, Room.number,
            Room.verbose_name
        ])
        if kwargs:
            raise ValueError('Unexpected kwargs: {}'.format(kwargs))

        query = Room.query
        entities = [Room]

        if 'equipment' in args:
            entities.append(static_array.array_agg(EquipmentType.name))
            query = query.outerjoin(RoomEquipmentAssociation).outerjoin(
                EquipmentType)

        query = (query.with_entities(*entities).outerjoin(
            Location,
            Location.id == Room.location_id).group_by(Location.name, Room.id))

        if only_active:
            query = query.filter(~Room.is_deleted)
        if filters:  # pragma: no cover
            query = query.filter(*filters)
        if order:  # pragma: no cover
            query = query.order_by(*order)

        keys = ('room', ) + tuple(args)
        return (dict(zip(keys, row if args else [row])) for row in query)

    @staticmethod
    def filter_available(start_dt,
                         end_dt,
                         repetition,
                         include_blockings=True,
                         include_pre_bookings=True,
                         include_pending_blockings=False):
        """Returns a SQLAlchemy filter criterion ensuring that the room is available during the given time."""
        # Check availability against reservation occurrences
        dummy_occurrences = ReservationOccurrence.create_series(
            start_dt, end_dt, repetition)
        overlap_criteria = ReservationOccurrence.filter_overlap(
            dummy_occurrences)
        reservation_criteria = [
            Reservation.room_id == Room.id, ReservationOccurrence.is_valid,
            overlap_criteria
        ]
        if not include_pre_bookings:
            reservation_criteria.append(Reservation.is_accepted)
        occurrences_filter = (Reservation.query.join(
            ReservationOccurrence.reservation).filter(
                and_(*reservation_criteria)))
        # Check availability against blockings
        filters = ~occurrences_filter.exists()
        if include_blockings:
            if include_pending_blockings:
                valid_states = (BlockedRoom.State.accepted,
                                BlockedRoom.State.pending)
            else:
                valid_states = (BlockedRoom.State.accepted, )
            # TODO: only take blockings into account which the user cannot override
            blocking_criteria = [
                Room.id == BlockedRoom.room_id,
                BlockedRoom.state.in_(valid_states),
                db_dates_overlap(Blocking,
                                 'start_date',
                                 end_dt.date(),
                                 'end_date',
                                 start_dt.date(),
                                 inclusive=True)
            ]
            blockings_filter = (BlockedRoom.query.join(
                Blocking.blocked_rooms).filter(and_(*blocking_criteria)))
            return filters & ~blockings_filter.exists()
        return filters

    @staticmethod
    def filter_bookable_hours(start_time, end_time):
        if end_time == time(0):
            end_time = time(23, 59, 59)
        period_end_time = db.case({time(0): time(23, 59, 59)},
                                  else_=BookableHours.end_time,
                                  value=BookableHours.end_time)
        bookable_hours_filter = Room.bookable_hours.any(
            (BookableHours.start_time <= start_time)
            & (period_end_time >= end_time))
        return ~Room.bookable_hours.any() | bookable_hours_filter

    @staticmethod
    def filter_nonbookable_periods(start_dt, end_dt):
        return ~Room.nonbookable_periods.any(
            and_(NonBookablePeriod.start_dt <= end_dt,
                 NonBookablePeriod.end_dt >= start_dt))

    def get_blocked_rooms(self, *dates, **kwargs):
        states = kwargs.get('states', (BlockedRoom.State.accepted, ))
        return (self.blocked_rooms.join(BlockedRoom.blocking).options(
            contains_eager(BlockedRoom.blocking)).filter(
                or_(Blocking.is_active_at(d) for d in dates),
                BlockedRoom.state.in_(states)).all())

    @property
    def protection_parent(self):
        return None

    @staticmethod
    def is_user_admin(user):
        return rb_is_admin(user)

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

    def can_access(self, user, allow_admin=True):
        # rooms are never access-restricted
        raise NotImplementedError

    def can_manage(self,
                   user,
                   permission=None,
                   allow_admin=True,
                   check_parent=True,
                   explicit_permission=False):
        if user and user == self.owner and (permission is None
                                            or not explicit_permission):
            return True
        return super(Room,
                     self).can_manage(user,
                                      permission=permission,
                                      allow_admin=allow_admin,
                                      check_parent=check_parent,
                                      explicit_permission=explicit_permission)

    def can_book(self, user, allow_admin=True):
        # XXX: When changing the logic in here, make sure to update get_permissions_for_user as well!
        if not user:
            return False
        if not self.is_reservable and not (allow_admin
                                           and self.is_user_admin(user)):
            return False
        if self.is_public and not self.reservations_need_confirmation:
            return True
        return self.can_manage(user,
                               permission='book',
                               allow_admin=allow_admin)

    def can_prebook(self, user, allow_admin=True):
        # XXX: When changing the logic in here, make sure to update get_permissions_for_user as well!
        if not user:
            return False
        if not self.is_reservable and not (allow_admin
                                           and self.is_user_admin(user)):
            return False
        if self.is_public and self.reservations_need_confirmation:
            return True
        # When the room does not use prebookings, we do not want the prebook option to show
        # up for admins or room managers unless they are actually in the ACL with the prebook
        # permission.
        explicit = not self.reservations_need_confirmation
        return self.can_manage(user,
                               permission='prebook',
                               allow_admin=allow_admin,
                               explicit_permission=explicit)

    def can_override(self, user, allow_admin=True):
        # XXX: When changing the logic in here, make sure to update get_permissions_for_user as well!
        return self.can_manage(user,
                               permission='override',
                               allow_admin=allow_admin)

    def can_moderate(self, user, allow_admin=True):
        # XXX: When changing the logic in here, make sure to update get_permissions_for_user as well!
        return self.can_manage(user,
                               permission='moderate',
                               allow_admin=allow_admin)

    def can_edit(self, user):
        if not user:
            return False
        return rb_is_admin(user)

    def can_delete(self, user):
        if not user:
            return False
        return rb_is_admin(user)

    def check_advance_days(self, end_date, user=None, quiet=False):
        if not self.max_advance_days:
            return True
        if user and (rb_is_admin(user) or self.can_manage(user)):
            return True
        advance_days = (end_date - date.today()).days
        ok = advance_days < self.max_advance_days
        if quiet or ok:
            return ok
        else:
            msg = _(u'You cannot book this room more than {} days in advance')
            raise NoReportError(msg.format(self.max_advance_days))

    def check_bookable_hours(self,
                             start_time,
                             end_time,
                             user=None,
                             quiet=False):
        if user and (rb_is_admin(user) or self.can_manage(user)):
            return True
        bookable_hours = self.bookable_hours.all()
        if not bookable_hours:
            return True
        for bt in bookable_hours:
            if bt.fits_period(start_time, end_time):
                return True
        if quiet:
            return False
        raise NoReportError(u'Room cannot be booked at this time')
Example #34
0
class ChatroomEventAssociation(db.Model):
    __tablename__ = 'chatroom_events'
    __table_args__ = {'schema': 'plugin_chat'}

    #: ID of the event
    event_id = db.Column(db.Integer,
                         db.ForeignKey('events.events.id'),
                         primary_key=True,
                         index=True,
                         autoincrement=False)
    #: ID of the chatroom
    chatroom_id = db.Column(db.Integer,
                            db.ForeignKey('plugin_chat.chatrooms.id'),
                            primary_key=True,
                            index=True)
    #: If the chatroom should be hidden on the event page
    hidden = db.Column(db.Boolean, nullable=False, default=False)
    #: If the password should be visible on the event page
    show_password = db.Column(db.Boolean, nullable=False, default=False)

    #: The associated :class:Chatroom
    chatroom = db.relationship('Chatroom',
                               lazy=False,
                               backref=db.backref(
                                   'events', cascade='all, delete-orphan'))
    #: The associated event
    event = db.relationship('Event',
                            lazy=True,
                            backref=db.backref('chatroom_associations',
                                               lazy='dynamic'))

    @property
    def locator(self):
        return dict(self.chatroom.locator, confId=self.event_id)

    @return_ascii
    def __repr__(self):
        return '<ChatroomEventAssociation({}, {})>'.format(
            self.event_id, self.chatroom)

    @classmethod
    def find_for_event(cls, event, include_hidden=False, **kwargs):
        """Returns a Query that retrieves the chatrooms for an event

        :param event: an indico event (with a numeric ID)
        :param include_hidden: if hidden chatrooms should be included, too
        :param kwargs: extra kwargs to pass to ``find()``
        """
        query = cls.find(event_id=event.id, **kwargs)
        if not include_hidden:
            query = query.filter(~cls.hidden)
        return query

    def delete(self, reason=''):
        """Deletes the event chatroom and if necessary the chatroom, too.

        :param reason: reason for the deletion
        :return: True if the associated chatroom was also
                 deleted, otherwise False
        """
        db.session.delete(self)
        db.session.flush()
        if not self.chatroom.events:
            db.session.delete(self.chatroom)
            db.session.flush()
            delete_room(self.chatroom, reason)
            return True
        return False
Example #35
0
class Room(versioned_cache(_cache, 'id'), ProtectionManagersMixin, db.Model, Serializer):
    __tablename__ = 'rooms'
    __table_args__ = (db.UniqueConstraint('id', 'location_id'),  # useless but needed for the LocationMixin fkey
                      db.CheckConstraint("verbose_name != ''", 'verbose_name_not_empty'),
                      {'schema': 'roombooking'})

    default_protection_mode = ProtectionMode.public
    disallowed_protection_modes = frozenset({ProtectionMode.inheriting})

    __public__ = [
        'id', 'name', 'location_name', 'floor', 'number', 'building',
        'booking_url', 'capacity', 'comments', 'owner_id', 'details_url',
        'large_photo_url', 'has_photo', 'sprite_position', 'is_active',
        'is_reservable', 'is_auto_confirm', 'marker_description', 'kind',
        'booking_limit_days'
    ]

    __public_exhaustive__ = __public__ + [
        'has_webcast_recording', 'has_vc', 'has_projector', 'is_public', 'has_booking_groups'
    ]

    __calendar_public__ = [
        'id', 'building', 'name', 'floor', 'number', 'kind', 'booking_url', 'details_url', 'location_name',
        'max_advance_days'
    ]

    __api_public__ = (
        'id', 'building', 'name', 'floor', 'longitude', 'latitude', ('number', 'roomNr'), ('location_name', 'location'),
        ('full_name', 'fullName'), ('booking_url', 'bookingUrl')
    )

    __api_minimal_public__ = (
        'id', ('full_name', 'fullName')
    )

    id = db.Column(
        db.Integer,
        primary_key=True
    )
    location_id = db.Column(
        db.Integer,
        db.ForeignKey('roombooking.locations.id'),
        nullable=False
    )
    photo_id = db.Column(
        db.Integer,
        db.ForeignKey('roombooking.photos.id')
    )
    #: Verbose name for the room (long)
    verbose_name = db.Column(
        db.String,
        nullable=True,
        default=None
    )
    site = db.Column(
        db.String,
        default=''
    )
    division = db.Column(
        db.String
    )
    building = db.Column(
        db.String,
        nullable=False
    )
    floor = db.Column(
        db.String,
        default='',
        nullable=False
    )
    number = db.Column(
        db.String,
        default='',
        nullable=False
    )
    notification_before_days = db.Column(
        db.Integer
    )
    notification_before_days_weekly = db.Column(
        db.Integer
    )
    notification_before_days_monthly = db.Column(
        db.Integer
    )
    notification_for_assistance = db.Column(
        db.Boolean,
        nullable=False,
        default=False
    )
    reservations_need_confirmation = db.Column(
        db.Boolean,
        nullable=False,
        default=False
    )
    notifications_enabled = db.Column(
        db.Boolean,
        nullable=False,
        default=True
    )
    telephone = db.Column(
        db.String
    )
    key_location = db.Column(
        db.String
    )
    capacity = db.Column(
        db.Integer,
        default=20
    )
    surface_area = db.Column(
        db.Integer
    )
    longitude = db.Column(
        db.Float
    )
    latitude = db.Column(
        db.Float
    )
    comments = db.Column(
        db.String
    )
    owner_id = db.Column(
        db.Integer,
        db.ForeignKey('users.users.id'),
        index=True,
        nullable=False
    )
    is_active = db.Column(
        db.Boolean,
        nullable=False,
        default=True,
        index=True
    )
    is_reservable = db.Column(
        db.Boolean,
        nullable=False,
        default=True
    )
    max_advance_days = db.Column(
        db.Integer
    )
    booking_limit_days = db.Column(
        db.Integer
    )

    acl_entries = db.relationship(
        'RoomPrincipal',
        lazy=True,
        backref='room',
        cascade='all, delete-orphan',
        collection_class=set
    )

    attributes = db.relationship(
        'RoomAttributeAssociation',
        backref='room',
        cascade='all, delete-orphan',
        lazy='dynamic'
    )

    blocked_rooms = db.relationship(
        'BlockedRoom',
        backref='room',
        cascade='all, delete-orphan',
        lazy='dynamic'
    )

    bookable_hours = db.relationship(
        'BookableHours',
        backref='room',
        order_by=BookableHours.start_time,
        cascade='all, delete-orphan',
        lazy='dynamic'
    )

    available_equipment = db.relationship(
        'EquipmentType',
        secondary=RoomEquipmentAssociation,
        backref='rooms',
        lazy=True
    )

    nonbookable_periods = db.relationship(
        'NonBookablePeriod',
        backref='room',
        order_by=NonBookablePeriod.end_dt.desc(),
        cascade='all, delete-orphan',
        lazy='dynamic'
    )

    photo = db.relationship(
        'Photo',
        backref='room',
        cascade='all, delete-orphan',
        single_parent=True,
        lazy=True
    )

    reservations = db.relationship(
        'Reservation',
        backref='room',
        cascade='all, delete-orphan',
        lazy='dynamic'
    )

    favorite_of = db.relationship(
        'User',
        secondary=favorite_room_table,
        lazy=True,
        collection_class=set,
        backref=db.backref('favorite_rooms', lazy=True, collection_class=set),
    )

    #: The owner of the room. If the room has the `manager-group`
    #: attribute set, any users in that group are also considered
    #: owners when it comes to management privileges.
    #: Use :meth:`is_owned_by` for ownership checks that should
    #: also check against the management group.
    owner = db.relationship(
        'User',
        # subquery load since a normal joinedload breaks `get_with_data`
        lazy='subquery',
        backref=db.backref(
            'owned_rooms',
            lazy='dynamic'
        )
    )

    # relationship backrefs:
    # - breaks (Break.own_room)
    # - contributions (Contribution.own_room)
    # - events (Event.own_room)
    # - location (Location.rooms)
    # - session_blocks (SessionBlock.own_room)
    # - sessions (Session.own_room)

    @hybrid_property
    def is_auto_confirm(self):
        return not self.reservations_need_confirmation

    @is_auto_confirm.expression
    def is_auto_confirm(self):
        return ~self.reservations_need_confirmation

    @property
    def booking_url(self):
        if self.id is None:
            return None
        return url_for('rooms.room_book', self)

    @property
    def details_url(self):
        if self.id is None:
            return None
        return url_for('rooms.roomBooking-roomDetails', self)

    @property
    def large_photo_url(self):
        if self.id is None:
            return None
        return url_for('rooms.photo', roomID=self.id)

    @property
    def map_url(self):
        if self.location.map_url_template:
            return self.location.map_url_template.format(
                building=self.building,
                floor=self.floor,
                number=self.number
            )
        else:
            return None

    @property
    def has_photo(self):
        return self.photo_id is not None

    @hybrid_property
    def name(self):
        return self.generate_name()

    @name.expression
    def name(cls):
        q = (db.session.query(db.m.Location.room_name_format)
             .filter(db.m.Location.id == cls.location_id)
             .correlate(Room)
             .as_scalar())
        return db.func.format(q, cls.building, cls.floor, cls.number)

    @hybrid_property
    def full_name(self):
        if self.verbose_name:
            return u'{} - {}'.format(self.generate_name(), self.verbose_name)
        else:
            return u'{}'.format(self.generate_name())

    @full_name.expression
    def full_name(cls):
        return db.case([
            [cls.verbose_name.isnot(None), cls.name + ' - ' + cls.verbose_name]
        ], else_=cls.name)

    @property
    @cached(_cache)
    def has_booking_groups(self):
        return self.has_attribute('allowed-booking-group')

    @property
    @cached(_cache)
    def has_projector(self):
        return self.has_equipment(u'Computer Projector', u'Video projector 4:3', u'Video projector 16:9')

    @property
    @cached(_cache)
    def has_webcast_recording(self):
        return self.has_equipment('Webcast/Recording')

    @property
    @cached(_cache)
    def has_vc(self):
        return self.has_equipment('Video conference')

    @property
    def kind(self):
        if not self.is_reservable or self.has_booking_groups:
            return 'privateRoom'
        elif self.reservations_need_confirmation:
            return 'moderatedRoom'
        else:
            return 'basicRoom'

    @property
    def location_name(self):
        return self.location.name

    @property
    def marker_description(self):
        infos = []

        infos.append(u'{capacity} {label}'.format(capacity=self.capacity,
                                                  label=_(u'person') if self.capacity == 1 else _(u'people')))
        infos.append(_(u'public') if self.is_public else _(u'private'))
        infos.append(_(u'auto-confirmation') if self.is_auto_confirm else _(u'needs confirmation'))
        if self.has_vc:
            infos.append(_(u'videoconference'))

        return u', '.join(map(unicode, infos))

    @property
    def manager_emails(self):
        manager_group = self.get_attribute_value('manager-group')
        if not manager_group:
            return set()
        group = GroupProxy.get_named_default_group(manager_group)
        return {u.email for u in group.get_members()}

    @property
    def notification_emails(self):
        return set(filter(None, map(unicode.strip, self.get_attribute_value(u'notification-email', u'').split(u','))))

    @property
    def sprite_position(self):
        sprite_mapping = _cache.get('rooms-sprite-mapping')
        return sprite_mapping.get(self.id, 0) if sprite_mapping else 0  # placeholder at position 0

    @return_ascii
    def __repr__(self):
        return format_repr(self, 'id', 'full_name')

    @cached(_cache)
    def has_equipment(self, *names):
        available = {x.name for x in self.available_equipment}
        return bool(available & set(names))

    def get_attribute_by_name(self, attribute_name):
        return (self.attributes
                .join(RoomAttribute)
                .filter(RoomAttribute.name == attribute_name)
                .first())

    def has_attribute(self, attribute_name):
        return self.get_attribute_by_name(attribute_name) is not None

    @cached(_cache)
    def get_attribute_value(self, name, default=None):
        attr = self.get_attribute_by_name(name)
        return attr.value if attr else default

    def set_attribute_value(self, name, value):
        attr = self.get_attribute_by_name(name)
        if attr:
            if value:
                attr.value = value
            else:
                self.attributes.filter(RoomAttributeAssociation.attribute_id == attr.attribute_id) \
                    .delete(synchronize_session='fetch')
        elif value:
            attr = RoomAttribute.query.filter_by(name=name).first()
            if not attr:
                raise ValueError("Attribute {} does not exist".format(name))
            attr_assoc = RoomAttributeAssociation()
            attr_assoc.value = value
            attr_assoc.attribute = attr
            self.attributes.append(attr_assoc)
        db.session.flush()

    @locator_property
    def locator(self):
        return {'roomLocation': self.location_name, 'roomID': self.id}

    def generate_name(self):
        if self.location is None:
            warnings.warn('Room has no location; using default name format')
            return '{}/{}-{}'.format(self.building, self.floor, self.number)
        return self.location.room_name_format.format(
            building=self.building,
            floor=self.floor,
            number=self.number
        )

    @classmethod
    def find_all(cls, *args, **kwargs):
        """Retrieves rooms, sorted by location and full name"""
        rooms = super(Room, cls).find_all(*args, **kwargs)
        rooms.sort(key=lambda r: natural_sort_key(r.location_name + r.full_name))
        return rooms

    @classmethod
    def find_with_attribute(cls, attribute):
        """Search rooms which have a specific attribute"""
        return (Room.query
                .with_entities(Room, RoomAttributeAssociation.value)
                .join(Room.attributes, RoomAttributeAssociation.attribute)
                .filter(RoomAttribute.name == attribute)
                .all())

    @staticmethod
    def get_with_data(*args, **kwargs):
        from indico.modules.rb.models.locations import Location

        only_active = kwargs.pop('only_active', True)
        filters = kwargs.pop('filters', None)
        order = kwargs.pop('order', [Location.name, Room.building, Room.floor, Room.number, Room.verbose_name])
        if kwargs:
            raise ValueError('Unexpected kwargs: {}'.format(kwargs))

        query = Room.query
        entities = [Room]

        if 'equipment' in args:
            entities.append(static_array.array_agg(EquipmentType.name))
            query = query.outerjoin(RoomEquipmentAssociation).outerjoin(EquipmentType)

        query = (query.with_entities(*entities)
                 .outerjoin(Location, Location.id == Room.location_id)
                 .group_by(Location.name, Room.id))

        if only_active:
            query = query.filter(Room.is_active)
        if filters:  # pragma: no cover
            query = query.filter(*filters)
        if order:  # pragma: no cover
            query = query.order_by(*order)

        keys = ('room',) + tuple(args)
        return (dict(zip(keys, row if args else [row])) for row in query)

    @classproperty
    @staticmethod
    def max_capacity():
        return db.session.query(db.func.max(Room.capacity)).scalar() or 0

    @staticmethod
    def filter_available(start_dt, end_dt, repetition, include_blockings=True, include_pre_bookings=True,
                         include_pending_blockings=False):
        """Returns a SQLAlchemy filter criterion ensuring that the room is available during the given time."""
        # Check availability against reservation occurrences
        dummy_occurrences = ReservationOccurrence.create_series(start_dt, end_dt, repetition)
        overlap_criteria = ReservationOccurrence.filter_overlap(dummy_occurrences)
        reservation_criteria = [Reservation.room_id == Room.id,
                                ReservationOccurrence.is_valid,
                                overlap_criteria]
        if not include_pre_bookings:
            reservation_criteria.append(Reservation.is_accepted)
        occurrences_filter = (Reservation.query
                              .join(ReservationOccurrence.reservation)
                              .filter(and_(*reservation_criteria)))
        # Check availability against blockings
        filters = ~occurrences_filter.exists()
        if include_blockings:
            if include_pending_blockings:
                valid_states = (BlockedRoom.State.accepted, BlockedRoom.State.pending)
            else:
                valid_states = (BlockedRoom.State.accepted,)
            # TODO: only take blockings into account which the user cannot override
            blocking_criteria = [Room.id == BlockedRoom.room_id,
                                 BlockedRoom.state.in_(valid_states),
                                 db_dates_overlap(Blocking, 'start_date', end_dt.date(), 'end_date', start_dt.date(),
                                                  inclusive=True)]
            blockings_filter = (BlockedRoom.query
                                .join(Blocking.blocked_rooms)
                                .filter(and_(*blocking_criteria)))
            return filters & ~blockings_filter.exists()
        return filters

    @staticmethod
    def filter_bookable_hours(start_time, end_time):
        if end_time == time(0):
            end_time = time(23, 59, 59)
        period_end_time = db.case({time(0): time(23, 59, 59)}, else_=BookableHours.end_time,
                                  value=BookableHours.end_time)
        bookable_hours_filter = Room.bookable_hours.any(
            (BookableHours.start_time <= start_time) & (period_end_time >= end_time)
        )
        return ~Room.bookable_hours.any() | bookable_hours_filter

    @staticmethod
    def filter_nonbookable_periods(start_dt, end_dt):
        return ~Room.nonbookable_periods.any(and_(NonBookablePeriod.start_dt <= end_dt,
                                                  NonBookablePeriod.end_dt >= start_dt))

    @staticmethod
    def find_with_filters(filters, user=None):
        from indico.modules.rb.models.locations import Location

        equipment_count = len(filters.get('available_equipment', ()))
        equipment_subquery = None
        if equipment_count:
            equipment_subquery = (
                db.session.query(RoomEquipmentAssociation)
                .with_entities(func.count(RoomEquipmentAssociation.c.room_id))
                .filter(
                    RoomEquipmentAssociation.c.room_id == Room.id,
                    RoomEquipmentAssociation.c.equipment_id.in_(eq.id for eq in filters['available_equipment'])
                )
                .correlate(Room)
                .as_scalar()
            )

        capacity = filters.get('capacity')
        q = (
            Room.query
            .join(Location.rooms)
            .filter(
                Location.id == filters['location'].id if filters.get('location') else True,
                ((Room.capacity >= (capacity * 0.8)) | (Room.capacity == None)) if capacity else True,
                Room.is_reservable if filters.get('is_only_public') else True,
                Room.is_auto_confirm if filters.get('is_auto_confirm') else True,
                Room.is_active if filters.get('is_only_active', False) else True,
                (equipment_subquery == equipment_count) if equipment_subquery is not None else True)
        )

        if filters.get('available', -1) != -1:
            repetition = RepeatMapping.convert_legacy_repeatability(ast.literal_eval(filters['repeatability']))
            is_available = Room.filter_available(filters['start_dt'], filters['end_dt'], repetition,
                                                 include_blockings=True,
                                                 include_pre_bookings=filters.get('include_pre_bookings', True),
                                                 include_pending_blockings=filters.get('include_pending_blockings',
                                                                                       True))
            # Filter the search results
            if filters['available'] == 0:  # booked/unavailable
                q = q.filter(~is_available)
            elif filters['available'] == 1:  # available
                q = q.filter(is_available)
            else:
                raise ValueError('Unexpected availability value')

        free_search_columns = (
            'full_name', 'site', 'division', 'building', 'floor', 'number', 'telephone', 'key_location', 'comments'
        )
        if filters.get('details'):
            # Attributes are stored JSON-encoded, so we need to JSON-encode the provided string and remove the quotes
            # afterwards since PostgreSQL currently does not expose a function to decode a JSON string:
            # http://www.postgresql.org/message-id/[email protected]
            details = filters['details'].lower()
            details_str = u'%{}%'.format(escape_like(details))
            details_json = u'%{}%'.format(escape_like(json.dumps(details)[1:-1]))
            free_search_criteria = [getattr(Room, c).ilike(details_str) for c in free_search_columns]
            free_search_criteria.append(Room.attributes.any(cast(RoomAttributeAssociation.value, db.String)
                                                            .ilike(details_json)))
            q = q.filter(or_(*free_search_criteria))

        q = q.order_by(Room.capacity)
        rooms = q.all()
        # Apply a bunch of filters which are *much* easier to do here than in SQL!
        if filters.get('is_only_public'):
            # This may trigger additional SQL queries but is_public is cached and doing this check here is *much* easier
            rooms = [r for r in rooms if r.is_public]
        if filters.get('is_only_my_rooms'):
            assert user is not None
            rooms = [r for r in rooms if r.is_owned_by(user)]
        if capacity:
            # Unless it would result in an empty resultset we don't want to show rooms with >20% more capacity
            # than requested. This cannot be done easily in SQL so we do that logic here after the SQL query already
            # weeded out rooms that are too small
            matching_capacity_rooms = [r for r in rooms if r.capacity is None or r.capacity <= capacity * 1.2]
            if matching_capacity_rooms:
                rooms = matching_capacity_rooms
        return rooms

    def has_live_reservations(self):
        return self.reservations.filter_by(
            is_archived=False,
            is_cancelled=False,
            is_rejected=False
        ).count() > 0

    def get_blocked_rooms(self, *dates, **kwargs):
        states = kwargs.get('states', (BlockedRoom.State.accepted,))
        return (self.blocked_rooms
                .join(BlockedRoom.blocking)
                .options(contains_eager(BlockedRoom.blocking))
                .filter(or_(Blocking.is_active_at(d) for d in dates),
                        BlockedRoom.state.in_(states))
                .all())

    @property
    def protection_parent(self):
        return None

    @staticmethod
    def is_user_admin(user):
        return rb_is_admin(user)

    @classmethod
    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_active)
                           .options(load_only('id', 'protection_mode', 'reservations_need_confirmation',
                                              'is_reservable'),
                                    raiseload('owner'),
                                    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:
            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_admin and allow_admin):
                if not room.reservations_need_confirmation or (is_admin and allow_admin):
                    data[room.id]['book'] = True
                if room.reservations_need_confirmation:
                    data[room.id]['prebook'] = True
            if 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_active, 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

    def can_access(self, user, allow_admin=True):
        # rooms are never access-restricted
        raise NotImplementedError

    def can_book(self, user, allow_admin=True):
        # XXX: When changing the logic in here, make sure to update get_permissions_for_user as well!
        if not user:
            return False
        if not self.is_reservable and not (allow_admin and self.is_user_admin(user)):
            return False
        if self.is_public and not self.reservations_need_confirmation:
            return True
        return self.can_manage(user, permission='book', allow_admin=allow_admin)

    def can_prebook(self, user, allow_admin=True):
        # XXX: When changing the logic in here, make sure to update get_permissions_for_user as well!
        if not user:
            return False
        if not self.is_reservable and not (allow_admin and self.is_user_admin(user)):
            return False
        if self.is_public and self.reservations_need_confirmation:
            return True
        # When the room does not use prebookings, we do not want the prebook option to show
        # up for admins or room managers unless they are actually in the ACL with the prebook
        # permission.
        explicit = not self.reservations_need_confirmation
        return self.can_manage(user, permission='prebook', allow_admin=allow_admin, explicit_permission=explicit)

    def can_override(self, user, allow_admin=True):
        # XXX: When changing the logic in here, make sure to update get_permissions_for_user as well!
        return self.can_manage(user, permission='override', allow_admin=allow_admin)

    def can_moderate(self, user, allow_admin=True):
        # XXX: When changing the logic in here, make sure to update get_permissions_for_user as well!
        return self.can_manage(user, permission='moderate', allow_admin=allow_admin)

    def can_edit(self, user):
        if not user:
            return False
        return rb_is_admin(user)

    def can_delete(self, user):
        if not user:
            return False
        return rb_is_admin(user)

    @unify_user_args
    @cached(_cache)
    def is_owned_by(self, user):
        """Checks if the user is managing the room (owner or manager)"""
        if self.owner == user:
            return True
        manager_group = self.get_attribute_value('manager-group')
        if not manager_group:
            return False
        return user in GroupProxy.get_named_default_group(manager_group)

    @classmethod
    def get_owned_by(cls, user):
        return [room for room in cls.find(is_active=True) if room.is_owned_by(user)]

    @classmethod
    def user_owns_rooms(cls, user):
        return any(room for room in cls.find(is_active=True) if room.is_owned_by(user))

    def check_advance_days(self, end_date, user=None, quiet=False):
        if not self.max_advance_days:
            return True
        if user and (rb_is_admin(user) or self.is_owned_by(user)):
            return True
        advance_days = (end_date - date.today()).days
        ok = advance_days < self.max_advance_days
        if quiet or ok:
            return ok
        else:
            msg = _(u'You cannot book this room more than {} days in advance')
            raise NoReportError(msg.format(self.max_advance_days))

    def check_bookable_hours(self, start_time, end_time, user=None, quiet=False):
        if user and (rb_is_admin(user) or self.is_owned_by(user)):
            return True
        bookable_hours = self.bookable_hours.all()
        if not bookable_hours:
            return True
        for bt in bookable_hours:
            if bt.fits_period(start_time, end_time):
                return True
        if quiet:
            return False
        raise NoReportError(u'Room cannot be booked at this time')