class AbstractReviewRating(db.Model): __tablename__ = 'abstract_review_ratings' __table_args__ = (db.UniqueConstraint('review_id', 'question_id'), { 'schema': 'event_abstracts' }) id = db.Column(db.Integer, primary_key=True) question_id = db.Column( db.Integer, db.ForeignKey('event_abstracts.abstract_review_questions.id'), index=True, nullable=False) review_id = db.Column(db.Integer, db.ForeignKey('event_abstracts.abstract_reviews.id'), index=True, nullable=False) value = db.Column(db.Integer, nullable=False) question = db.relationship('AbstractReviewQuestion', lazy=True, backref=db.backref('ratings', cascade='all, delete-orphan', lazy=True)) review = db.relationship('AbstractReview', lazy=True, backref=db.backref('ratings', cascade='all, delete-orphan', lazy=True)) @return_ascii def __repr__(self): return format_repr(self, 'id', 'review_id', 'question_id')
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')) @return_ascii def __repr__(self): return '<VidyoExtension({}, {}, {})>'.format(self.vc_room, self.extension, self.owned_by_user)
class Judgment(db.Model): """Represents an abstract judgment, emitted by a judge""" __tablename__ = 'judgments' __table_args__ = (db.UniqueConstraint('abstract_id', 'track_id', 'judge_user_id'), { 'schema': 'event_abstracts' }) id = db.Column(db.Integer, primary_key=True) creation_dt = db.Column(UTCDateTime, nullable=False, default=now_utc, index=True) abstract_id = db.Column(db.Integer, db.ForeignKey('event_abstracts.abstracts.id'), index=True, nullable=False) track_id = db.Column(db.Integer, nullable=False) judge_user_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), nullable=False, index=True) accepted_type_id = db.Column(db.Integer, db.ForeignKey('events.contribution_types.id'), nullable=True, index=True) abstract = db.relationship('Abstract', lazy=False, backref=db.backref('judgments', lazy='dynamic')) judge = db.relationship('User', lazy=False, backref=db.backref('abstract_judgments', lazy='dynamic')) accepted_type = db.relationship('ContributionType', lazy=False, backref=db.backref('abstract_judgments', lazy='dynamic')) @return_ascii def __repr__(self): return format_repr(self, 'id', abstract=self.abstract, judge=self.judge) @property def as_legacy(self): return next( (judgment for judgment in self.abstract.as_legacy.getJudgementHistoryByTrack(self.track) if judgment.getResponsible().as_new == self.judge), None) @property def track(self): return self.abstract.event_new.as_legacy.getTrackById( str(self.track_id))
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)
class OutlookQueueEntry(db.Model): """Pending calendar updates""" __tablename__ = 'queue' __table_args__ = (db.Index(None, 'user_id', 'event_id', 'action'), { 'schema': 'plugin_outlook' }) #: Entry ID (mainly used to sort by insertion order) id = db.Column(db.Integer, primary_key=True) #: ID of the user user_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=False) #: ID of the event event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False) #: :class:`OutlookAction` to perform action = db.Column(PyIntEnum(OutlookAction), nullable=False) #: The user associated with the queue entry user = db.relationship('User', lazy=False, backref=db.backref('outlook_queue', lazy='dynamic')) #: The Event this queue entry is associated with event = db.relationship('Event', lazy=True, backref=db.backref('outlook_queue_entries', lazy='dynamic')) @return_ascii def __repr__(self): return '<OutlookQueueEntry({}, {}, {}, {})>'.format( self.id, self.event_id, self.user_id, OutlookAction(self.action).name) @classmethod def record(cls, event, user, action): """Records a new calendar action :param event: the event (a :class:`.Event` instance) :param user: the user (a :class:`.User` instance) :param action: the action (an :class:`OutlookAction` member) """ # It would be nice to delete matching records first, but this sometimes results in very weird deadlocks event.outlook_queue_entries.append(cls(user_id=user.id, action=action)) db.session.flush()
def user_id(cls): return db.Column( db.Integer, db.ForeignKey('users.users.id'), nullable=True, index=True )
def category_role_id(cls): if not cls.allow_category_roles: return return db.Column(db.Integer, db.ForeignKey('categories.roles.id'), nullable=True, index=True)
def _affiliation_id(cls): return db.Column( 'affiliation_id', db.ForeignKey('indico.affiliations.id'), nullable=True, index=True )
class SubContributionPersonLink(PersonLinkBase): """Association between EventPerson and SubContribution.""" __tablename__ = 'subcontribution_person_links' __auto_table_args = {'schema': 'events'} person_link_backref_name = 'subcontribution_links' person_link_unique_columns = ('subcontribution_id', ) object_relationship_name = 'subcontribution' # subcontribution persons are always speakers and never authors # we provide these attributes to make subcontribution links # compatible with contribution links is_speaker = True author_type = AuthorType.none subcontribution_id = db.Column(db.Integer, db.ForeignKey('events.subcontributions.id'), index=True, nullable=False) # relationship backrefs: # - subcontribution (SubContribution.person_links) @return_ascii def __repr__(self): return format_repr(self, 'id', 'person_id', 'subcontribution_id', _text=self.full_name)
def registration_form_id(cls): if not cls.allow_registration_forms: return return db.Column(db.Integer, db.ForeignKey('event_registration.forms.id'), nullable=True, index=True)
class EventPersonLink(PersonLinkBase): """Association between EventPerson and Event. Chairperson or speaker (lecture) """ __tablename__ = 'event_person_links' __auto_table_args = {'schema': 'events'} person_link_backref_name = 'event_links' person_link_unique_columns = ('event_id', ) object_relationship_name = 'event' event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False) # relationship backrefs: # - event (Event.person_links) @property def is_submitter(self): if not self.event: raise Exception("No event to check submission rights against") return self.person.has_role('submit', self.event) @return_ascii def __repr__(self): return format_repr(self, 'id', 'person_id', 'event_id', _text=self.full_name)
def event_role_id(cls): if not cls.allow_event_roles: return return db.Column(db.Integer, db.ForeignKey('events.roles.id'), nullable=True, index=True)
class AbstractPersonLink(PersonLinkBase): """Association between EventPerson and Abstract.""" __tablename__ = 'abstract_person_links' __auto_table_args = {'schema': 'event_abstracts'} person_link_backref_name = 'abstract_links' person_link_unique_columns = ('abstract_id', ) object_relationship_name = 'abstract' abstract_id = db.Column(db.Integer, db.ForeignKey('event_abstracts.abstracts.id'), index=True, nullable=False) is_speaker = db.Column(db.Boolean, nullable=False, default=False) author_type = db.Column(PyIntEnum(AuthorType), nullable=False, default=AuthorType.none) # relationship backrefs: # - abstract (Abstract.person_links) @locator_property def locator(self): return dict(self.abstract.locator, person_id=self.id) @return_ascii def __repr__(self): return format_repr(self, 'id', 'person_id', 'abstract_id', is_speaker=False, author_type=None, _text=self.full_name)
def person_id(cls): return db.Column( db.Integer, db.ForeignKey('events.persons.id'), index=True, nullable=False )
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) def __repr__(self): return format_repr(self, 'id', _text=text_to_repr(self.title))
class SessionBlockPersonLink(PersonLinkBase): """Association between EventPerson and SessionBlock. Also known as a 'session convener' """ __tablename__ = 'session_block_person_links' __auto_table_args = {'schema': 'events'} person_link_backref_name = 'session_block_links' person_link_unique_columns = ('session_block_id', ) object_relationship_name = 'session_block' session_block_id = db.Column(db.Integer, db.ForeignKey('events.session_blocks.id'), index=True, nullable=False) # relationship backrefs: # - session_block (SessionBlock.person_links) @return_ascii def __repr__(self): return format_repr(self, 'id', 'person_id', 'session_block_id', _text=self.full_name)
def ip_network_group_id(cls): if not cls.allow_networks: return return db.Column(db.Integer, db.ForeignKey('indico.ip_network_groups.id'), nullable=True, index=True)
def local_group_id(cls): return db.Column( db.Integer, db.ForeignKey('users.groups.id'), nullable=True, index=True )
class Chatroom(db.Model): __tablename__ = 'chatrooms' __table_args__ = (db.UniqueConstraint('jid_node', 'custom_server'), { 'schema': 'plugin_chat' }) #: Chatroom ID id = db.Column(db.Integer, primary_key=True) #: Node of the chatroom's JID (the part before `@domain`) jid_node = db.Column(db.String, nullable=False) #: Name of the chatroom name = db.Column(db.String, nullable=False) #: Description of the chatroom description = db.Column(db.Text, nullable=False, default='') #: Password to join the room password = db.Column(db.String, nullable=False, default='') #: Custom Jabber MUC server hostname custom_server = db.Column(db.String, nullable=False, default='') #: ID of the creator created_by_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=False) #: Creation timestamp of the chatroom created_dt = db.Column(UTCDateTime, nullable=False, default=now_utc) #: Modification timestamp of the chatroom modified_dt = db.Column(UTCDateTime) #: The user who created the chatroom created_by_user = db.relationship('User', lazy=True, backref=db.backref('chatrooms', lazy='dynamic')) @property def locator(self): return {'chatroom_id': self.id} @property def server(self): """The server name of the chatroom. Usually the default one unless a custom one is set. """ from indico_chat.plugin import ChatPlugin return self.custom_server or ChatPlugin.settings.get('muc_server') @property def jid(self): return '{}@{}'.format(self.jid_node, self.server) @return_ascii def __repr__(self): server = self.server if self.custom_server: server = '!' + server return '<Chatroom({}, {}, {}, {})>'.format(self.id, self.name, self.jid_node, server)
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 ContributionPersonLink(PersonLinkBase): """Association between EventPerson and Contribution.""" __tablename__ = 'contribution_person_links' __auto_table_args = {'schema': 'events'} person_link_backref_name = 'contribution_links' person_link_unique_columns = ('contribution_id',) object_relationship_name = 'contribution' contribution_id = db.Column( db.Integer, db.ForeignKey('events.contributions.id'), index=True, nullable=False ) is_speaker = db.Column( db.Boolean, nullable=False, default=False ) author_type = db.Column( PyIntEnum(AuthorType), nullable=False, default=AuthorType.none ) # relationship backrefs: # - contribution (Contribution.person_links) @property def is_submitter(self): if not self.contribution: raise Exception("No contribution to check submission rights against") return self.person.has_role('submit', self.contribution) @property def is_author(self): return self.author_type != AuthorType.none @locator_property def locator(self): return dict(self.contribution.locator, person_id=self.id) @return_ascii def __repr__(self): return format_repr(self, 'id', 'person_id', 'contribution_id', is_speaker=False, author_type=AuthorType.none, _text=self.full_name)
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 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 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 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)