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'))
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'))
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'))
def _affiliation_link(cls): return db.relationship( 'Affiliation', lazy=False, backref=db.backref( cls.person_link_backref_name, cascade_backrefs=False, lazy='dynamic', ) )
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 ) )
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' ) )
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' ) )
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 ) )
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' ) )
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' ) )
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)
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' ) )
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' ) )
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
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))
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
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))
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))
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)))
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)
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
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)
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')
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)
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
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()
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
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)
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')
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
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')