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'))
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 Event(SearchableTitleMixin, DescriptionMixin, LocationMixin, ProtectionManagersMixin, AttachedItemsMixin, AttachedNotesMixin, PersonLinkDataMixin, db.Model): """An fossir 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 fossir.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 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 fossir.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 fossir.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 fossir.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 fossir.modules.events.features.util import is_feature_enabled return is_feature_enabled(self, feature) @property @memoize_request def scheduled_notes(self): from fossir.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. :return: The newly created `EventLogEntry` In most cases the ``simple`` log type is fine. For this type, any items from data will be shown in the detailed view of the log entry. You may either use a dict (which will be sorted) alphabetically or a list of ``key, value`` pairs which will be displayed in the given order. """ if self.__logging_disabled: return entry = EventLogEntry(user=user, realm=realm, kind=kind, module=module, type=type_, summary=summary, data=data or {}) self.log_entries.append(entry) return entry def get_contribution_field(self, field_id): return next((v for v in self.contribution_fields if v.id == field_id), '') def move_start_dt(self, start_dt): """Set event start_dt and adjust its timetable entries""" diff = start_dt - self.start_dt for entry in self.timetable_entries.filter(TimetableEntry.parent_id.is_(None)): new_dt = entry.start_dt + diff entry.move(new_dt) self.start_dt = start_dt def iter_days(self, tzinfo=None): start_dt = self.start_dt end_dt = self.end_dt if tzinfo: start_dt = start_dt.astimezone(tzinfo) end_dt = end_dt.astimezone(tzinfo) duration = (end_dt - 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 fossir.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 fossir.modules.events.abstracts.models.call_for_abstracts import CallForAbstracts return CallForAbstracts(self) @property @memoize_request def cfp(self): from fossir.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 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 = 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.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.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, role='abstract_reviewer', explicit_role=True): return False elif user in self.event.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.can_manage(user, role='track_convener', explicit_role=True): return False elif user in self.event.global_conveners: return True elif user in self.conveners: return True else: return False
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 fossir.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 fossir.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_role(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 PaperReview(ProposalReviewMixin, RenderModeMixin, db.Model): """Represents 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 fossir.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 ratings: return None return sum(x.value for x in ratings) / len(ratings)
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 fossir.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 fossir.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 AbstractReview(ProposalReviewMixin, RenderModeMixin, db.Model): """Represents 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 ] 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 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, cascade='all, delete-orphan')) 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['to'] | email_data['cc'] | email_data['bcc']) 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)