class RegistrationForm(db.Model): """A registration form for an event""" __tablename__ = 'forms' __table_args__ = ( db.Index( 'ix_uq_forms_participation', 'event_id', unique=True, postgresql_where=db.text('is_participation AND NOT is_deleted')), db.UniqueConstraint( 'id', 'event_id'), # useless but needed for the registrations fkey { 'schema': 'event_registration' }) #: The ID of the object id = db.Column(db.Integer, primary_key=True) #: The ID of the event event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False) #: The title of the registration form title = db.Column(db.String, nullable=False) #: Whether it's the 'Participants' form of a meeting/lecture is_participation = db.Column(db.Boolean, nullable=False, default=False) # An introduction text for users introduction = db.Column(db.Text, nullable=False, default='') #: Contact information for registrants contact_info = db.Column(db.String, nullable=False, default='') #: Datetime when the registration form is open start_dt = db.Column(UTCDateTime, nullable=True) #: Datetime when the registration form is closed end_dt = db.Column(UTCDateTime, nullable=True) #: Whether registration modifications are allowed modification_mode = db.Column(PyIntEnum(ModificationMode), nullable=False, default=ModificationMode.not_allowed) #: Datetime when the modification period is over modification_end_dt = db.Column(UTCDateTime, nullable=True) #: Whether the registration has been marked as deleted is_deleted = db.Column(db.Boolean, nullable=False, default=False) #: Whether users must be logged in to register require_login = db.Column(db.Boolean, nullable=False, default=False) #: Whether registrations must be associated with an Indico account require_user = db.Column(db.Boolean, nullable=False, default=False) #: Maximum number of registrations allowed registration_limit = db.Column(db.Integer, nullable=True) #: Whether registrations should be displayed in the participant list publish_registrations_enabled = db.Column(db.Boolean, nullable=False, default=False) #: Whether to display the number of registrations publish_registration_count = db.Column(db.Boolean, nullable=False, default=False) #: Whether checked-in status should be displayed in the event pages and participant list publish_checkin_enabled = db.Column(db.Boolean, nullable=False, default=False) #: Whether registrations must be approved by a manager moderation_enabled = db.Column(db.Boolean, nullable=False, default=False) #: The base fee users have to pay when registering base_price = db.Column( db.Numeric(11, 2), # max. 999999999.99 nullable=False, default=0) #: Currency for prices in the registration form currency = db.Column(db.String, nullable=False) #: Notifications sender address notification_sender_address = db.Column(db.String, nullable=True) #: Custom message to include in emails for pending registrations message_pending = db.Column(db.Text, nullable=False, default='') #: Custom message to include in emails for unpaid registrations message_unpaid = db.Column(db.Text, nullable=False, default='') #: Custom message to include in emails for complete registrations message_complete = db.Column(db.Text, nullable=False, default='') #: Whether the manager notifications for this event are enabled manager_notifications_enabled = db.Column(db.Boolean, nullable=False, default=False) #: List of emails that should receive management notifications manager_notification_recipients = db.Column(ARRAY(db.String), nullable=False, default=[]) #: Whether tickets are enabled for this form tickets_enabled = db.Column(db.Boolean, nullable=False, default=False) #: Whether to send tickets by e-mail ticket_on_email = db.Column(db.Boolean, nullable=False, default=True) #: Whether to show a ticket download link on the event homepage ticket_on_event_page = db.Column(db.Boolean, nullable=False, default=True) #: Whether to show a ticket download link on the registration summary page ticket_on_summary_page = db.Column(db.Boolean, nullable=False, default=True) #: The ID of the template used to generate tickets ticket_template_id = db.Column(db.Integer, db.ForeignKey(DesignerTemplate.id), nullable=True, index=True) #: The Event containing this registration form event = db.relationship( 'Event', lazy=True, backref=db.backref( 'registration_forms', primaryjoin= '(RegistrationForm.event_id == Event.id) & ~RegistrationForm.is_deleted', cascade='all, delete-orphan', lazy=True)) #: The template used to generate tickets ticket_template = db.relationship('DesignerTemplate', lazy=True, foreign_keys=ticket_template_id, backref=db.backref('ticket_for_regforms', lazy=True)) # The items (sections, text, fields) in the form form_items = db.relationship('RegistrationFormItem', lazy=True, cascade='all, delete-orphan', order_by='RegistrationFormItem.position', backref=db.backref('registration_form', lazy=True)) #: The registrations associated with this form registrations = db.relationship( 'Registration', lazy=True, cascade='all, delete-orphan', foreign_keys=[Registration.registration_form_id], backref=db.backref('registration_form', lazy=True)) #: The registration invitations associated with this form invitations = db.relationship('RegistrationInvitation', lazy=True, cascade='all, delete-orphan', backref=db.backref('registration_form', lazy=True)) @hybrid_property def has_ended(self): return self.end_dt is not None and self.end_dt <= now_utc() @has_ended.expression def has_ended(cls): return cls.end_dt.isnot(None) & (cls.end_dt <= now_utc()) @hybrid_property def has_started(self): return self.start_dt is not None and self.start_dt <= now_utc() @has_started.expression def has_started(cls): return cls.start_dt.isnot(None) & (cls.start_dt <= now_utc()) @hybrid_property def is_modification_open(self): end_dt = self.modification_end_dt if self.modification_end_dt else self.end_dt return now_utc() <= end_dt if end_dt else True @is_modification_open.expression def is_modification_open(self): now = now_utc() return now <= db.func.coalesce(self.modification_end_dt, self.end_dt, now) @hybrid_property def is_open(self): return not self.is_deleted and self.has_started and not self.has_ended @is_open.expression def is_open(cls): return ~cls.is_deleted & cls.has_started & ~cls.has_ended @hybrid_property def is_scheduled(self): return not self.is_deleted and self.start_dt is not None @is_scheduled.expression def is_scheduled(cls): return ~cls.is_deleted & cls.start_dt.isnot(None) @property def locator(self): return dict(self.event.locator, reg_form_id=self.id) @property def active_fields(self): return [ field for field in self.form_items if (field.is_field and field.is_enabled and not field.is_deleted and field.parent.is_enabled and not field.parent.is_deleted) ] @property def sections(self): return [x for x in self.form_items if x.is_section] @property def disabled_sections(self): return [ x for x in self.sections if not x.is_visible and not x.is_deleted ] @property def limit_reached(self): return self.registration_limit and len( self.active_registrations) >= self.registration_limit @property def is_active(self): return self.is_open and not self.limit_reached @property @memoize_request def active_registrations(self): return (Registration.query.with_parent(self).filter( Registration.is_active).options(subqueryload('data')).all()) @property def sender_address(self): contact_email = self.event.contact_emails[ 0] if self.event.contact_emails else None return self.notification_sender_address or contact_email @return_ascii def __repr__(self): return '<RegistrationForm({}, {}, {})>'.format(self.id, self.event_id, self.title) def is_modification_allowed(self, registration): """Checks whether a registration may be modified""" if not registration.is_active: return False elif self.modification_mode == ModificationMode.allowed_always: return True elif self.modification_mode == ModificationMode.allowed_until_payment: return not registration.is_paid else: return False def can_submit(self, user): return self.is_active and (not self.require_login or user) @memoize_request def get_registration(self, user=None, uuid=None, email=None): """Retrieves registrations for this registration form by user or uuid""" if (bool(user) + bool(uuid) + bool(email)) != 1: raise ValueError( "Exactly one of `user`, `uuid` and `email` must be specified") if user: return user.registrations.filter_by(registration_form=self).filter( Registration.is_active).first() if uuid: try: UUID(hex=uuid) except ValueError: raise BadRequest('Malformed registration token') return Registration.query.with_parent(self).filter_by( uuid=uuid).filter(Registration.is_active).first() if email: return Registration.query.with_parent(self).filter_by( email=email).filter(Registration.is_active).first() def render_base_price(self): return format_currency(self.base_price, self.currency, locale=session.lang or 'en_GB') def get_personal_data_field_id(self, personal_data_type): """Returns the field id corresponding to the personal data field with the given name.""" for field in self.active_fields: if (isinstance(field, RegistrationFormPersonalDataField) and field.personal_data_type == personal_data_type): return field.id
class PaperRevision(ProposalRevisionMixin, RenderModeMixin, db.Model): __tablename__ = 'revisions' __table_args__ = (db.Index(None, 'contribution_id', unique=True, postgresql_where=db.text(f'state = {PaperRevisionState.accepted}')), db.UniqueConstraint('contribution_id', 'submitted_dt'), db.CheckConstraint('(state IN ({}, {}, {})) = (judge_id IS NOT NULL)' .format(PaperRevisionState.accepted, PaperRevisionState.rejected, PaperRevisionState.to_be_corrected), name='judge_if_judged'), db.CheckConstraint('(state IN ({}, {}, {})) = (judgment_dt IS NOT NULL)' .format(PaperRevisionState.accepted, PaperRevisionState.rejected, PaperRevisionState.to_be_corrected), name='judgment_dt_if_judged'), {'schema': 'event_paper_reviewing'}) possible_render_modes = {RenderMode.markdown} default_render_mode = RenderMode.markdown proposal_attr = 'paper' id = db.Column( db.Integer, primary_key=True ) state = db.Column( PyIntEnum(PaperRevisionState), nullable=False, default=PaperRevisionState.submitted ) _contribution_id = db.Column( 'contribution_id', db.Integer, db.ForeignKey('events.contributions.id'), index=True, nullable=False ) submitter_id = db.Column( db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=False ) submitted_dt = db.Column( UTCDateTime, nullable=False, default=now_utc ) judge_id = db.Column( db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=True ) judgment_dt = db.Column( UTCDateTime, nullable=True ) _judgment_comment = db.Column( 'judgment_comment', db.Text, nullable=False, default='' ) _contribution = db.relationship( 'Contribution', lazy=True, backref=db.backref( '_paper_revisions', lazy=True, order_by=submitted_dt.asc() ) ) submitter = db.relationship( 'User', lazy=True, foreign_keys=submitter_id, backref=db.backref( 'paper_revisions', lazy='dynamic' ) ) judge = db.relationship( 'User', lazy=True, foreign_keys=judge_id, backref=db.backref( 'judged_papers', lazy='dynamic' ) ) judgment_comment = RenderModeMixin.create_hybrid_property('_judgment_comment') # relationship backrefs: # - comments (PaperReviewComment.paper_revision) # - files (PaperFile.paper_revision) # - reviews (PaperReview.revision) def __init__(self, *args, **kwargs): paper = kwargs.pop('paper', None) if paper: kwargs.setdefault('_contribution', paper.contribution) super().__init__(*args, **kwargs) def __repr__(self): return format_repr(self, 'id', '_contribution_id', state=None) @locator_property def locator(self): return dict(self.paper.locator, revision_id=self.id) @property def paper(self): return self._contribution.paper @property def is_last_revision(self): return self == self.paper.last_revision @property def number(self): return self.paper.revisions.index(self) + 1 @property def spotlight_file(self): return self.get_spotlight_file() @property def timeline(self): return self.get_timeline() @paper.setter def paper(self, paper): self._contribution = paper.contribution def get_timeline(self, user=None): comments = [x for x in self.comments if x.can_view(user)] if user else self.comments reviews = [x for x in self.reviews if x.can_view(user)] if user else self.reviews judgment = [PaperJudgmentProxy(self)] if self.state == PaperRevisionState.to_be_corrected else [] return sorted(chain(comments, reviews, judgment), key=attrgetter('created_dt')) def get_reviews(self, group=None, user=None): reviews = [] if user and group: reviews = [x for x in self.reviews if x.group.instance == group and x.user == user] elif user: reviews = [x for x in self.reviews if x.user == user] elif group: reviews = [x for x in self.reviews if x.group.instance == group] return reviews def get_reviewed_for_groups(self, user, include_reviewed=False): from indico.modules.events.papers.models.reviews import PaperTypeProxy from indico.modules.events.papers.util import is_type_reviewing_possible cfp = self.paper.cfp reviewed_for = set() if include_reviewed: reviewed_for = {x.type for x in self.reviews if x.user == user and is_type_reviewing_possible(cfp, x.type)} if is_type_reviewing_possible(cfp, PaperReviewType.content) and user in self.paper.cfp.content_reviewers: reviewed_for.add(PaperReviewType.content) if is_type_reviewing_possible(cfp, PaperReviewType.layout) and user in self.paper.cfp.layout_reviewers: reviewed_for.add(PaperReviewType.layout) return set(map(PaperTypeProxy, reviewed_for)) def has_user_reviewed(self, user, review_type=None): from indico.modules.events.papers.models.reviews import PaperReviewType if review_type: if isinstance(review_type, str): review_type = PaperReviewType[review_type] return any(review.user == user and review.type == review_type for review in self.reviews) else: layout_review = next((review for review in self.reviews if review.user == user and review.type == PaperReviewType.layout), None) content_review = next((review for review in self.reviews if review.user == user and review.type == PaperReviewType.content), None) if user in self._contribution.paper_layout_reviewers and user in self._contribution.paper_content_reviewers: return bool(layout_review and content_review) elif user in self._contribution.paper_layout_reviewers: return bool(layout_review) elif user in self._contribution.paper_content_reviewers: return bool(content_review) def get_spotlight_file(self): pdf_files = [paper_file for paper_file in self.files if paper_file.content_type == 'application/pdf'] return pdf_files[0] if len(pdf_files) == 1 else None
class Session(DescriptionMixin, ColorMixin, ProtectionManagersMixin, LocationMixin, AttachedItemsMixin, AttachedNotesMixin, db.Model): __tablename__ = 'sessions' __auto_table_args = (db.Index(None, 'friendly_id', 'event_id', unique=True), { 'schema': 'events' }) location_backref_name = 'sessions' disallowed_protection_modes = frozenset() inheriting_have_acl = True default_colors = ColorTuple('#202020', '#e3f2d3') allow_relationship_preloading = True PRELOAD_EVENT_ATTACHED_ITEMS = True PRELOAD_EVENT_NOTES = True ATTACHMENT_FOLDER_ID_COLUMN = 'session_id' possible_render_modes = {RenderMode.markdown} default_render_mode = RenderMode.markdown @declared_attr def __table_args__(cls): return auto_table_args(cls) id = db.Column(db.Integer, primary_key=True) #: The human-friendly ID for the session friendly_id = db.Column(db.Integer, nullable=False, default=_get_next_friendly_id) event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False) type_id = db.Column(db.Integer, db.ForeignKey('events.session_types.id'), index=True, nullable=True) title = db.Column(db.String, nullable=False) code = db.Column(db.String, nullable=False, default='') default_contribution_duration = db.Column(db.Interval, nullable=False, default=timedelta(minutes=20)) is_deleted = db.Column(db.Boolean, nullable=False, default=False) event = db.relationship( 'Event', lazy=True, backref=db.backref( 'sessions', primaryjoin='(Session.event_id == Event.id) & ~Session.is_deleted', cascade='all, delete-orphan', lazy=True)) acl_entries = db.relationship('SessionPrincipal', lazy=True, cascade='all, delete-orphan', collection_class=set, backref='session') blocks = db.relationship('SessionBlock', lazy=True, cascade='all, delete-orphan', backref=db.backref('session', lazy=False)) type = db.relationship('SessionType', lazy=True, backref=db.backref('sessions', lazy=True)) # relationship backrefs: # - attachment_folders (AttachmentFolder.session) # - contributions (Contribution.session) # - default_for_tracks (Track.default_session) # - legacy_mapping (LegacySessionMapping.session) # - note (EventNote.session) def __init__(self, **kwargs): # explicitly initialize this relationship with None to avoid # an extra query to check whether there is an object associated # when assigning a new one (e.g. during cloning) kwargs.setdefault('note', None) super(Session, self).__init__(**kwargs) @classmethod def preload_acl_entries(cls, event): cls.preload_relationships(cls.query.with_parent(event), 'acl_entries') @property def location_parent(self): return self.event @property def protection_parent(self): return self.event @property def session(self): """Convenience property so all event entities have it""" return self @property @memoize_request def start_dt(self): from indico.modules.events.sessions.models.blocks import SessionBlock start_dt = (self.event.timetable_entries.with_entities( TimetableEntry.start_dt).join('session_block').filter( TimetableEntry.type == TimetableEntryType.SESSION_BLOCK, SessionBlock.session == self).order_by( TimetableEntry.start_dt).first()) return start_dt[0] if start_dt else None @property @memoize_request def end_dt(self): sorted_blocks = sorted(self.blocks, key=attrgetter('timetable_entry.end_dt'), reverse=True) return sorted_blocks[ 0].timetable_entry.end_dt if sorted_blocks else None @property @memoize_request def conveners(self): from indico.modules.events.sessions.models.blocks import SessionBlock from indico.modules.events.sessions.models.persons import SessionBlockPersonLink return (SessionBlockPersonLink.query.join(SessionBlock).filter( SessionBlock.session_id == self.id).distinct( SessionBlockPersonLink.person_id).all()) @property def is_poster(self): return self.type.is_poster if self.type else False @locator_property def locator(self): return dict(self.event.locator, session_id=self.id) def get_non_inheriting_objects(self): """Get a set of child objects that do not inherit protection""" return get_non_inheriting_objects(self) @return_ascii def __repr__(self): return format_repr(self, 'id', is_deleted=False, _text=self.title) def can_manage_contributions(self, user, allow_admin=True): """Check whether a user can manage contributions within the session.""" from indico.modules.events.sessions.util import session_coordinator_priv_enabled if user is None: return False elif self.session.can_manage(user, allow_admin=allow_admin): return True elif (self.session.can_manage(user, 'coordinate') and session_coordinator_priv_enabled(self.event, 'manage-contributions')): return True else: return False def can_manage_blocks(self, user, allow_admin=True): """Check whether a user can manage session blocks. This only applies to the blocks themselves, not to contributions inside them. """ from indico.modules.events.sessions.util import session_coordinator_priv_enabled if user is None: return False # full session manager can always manage blocks. this also includes event managers and higher. elif self.session.can_manage(user, allow_admin=allow_admin): return True # session coordiator if block management is allowed elif (self.session.can_manage(user, 'coordinate') and session_coordinator_priv_enabled(self.event, 'manage-blocks')): return True else: return False
class Blocking(db.Model): __tablename__ = 'blockings' __table_args__ = {'schema': 'roombooking'} id = db.Column(db.Integer, primary_key=True) created_by_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=False) created_dt = db.Column(UTCDateTime, nullable=False, default=now_utc) start_date = db.Column(db.Date, nullable=False, index=True) end_date = db.Column(db.Date, nullable=False, index=True) reason = db.Column(db.Text, nullable=False) _allowed = db.relationship('BlockingPrincipal', backref='blocking', cascade='all, delete-orphan', collection_class=set) allowed = association_proxy( '_allowed', 'principal', creator=lambda v: BlockingPrincipal(principal=v)) blocked_rooms = db.relationship('BlockedRoom', backref='blocking', cascade='all, delete-orphan') #: The user who created this blocking. created_by_user = db.relationship('User', lazy=False, backref=db.backref('blockings', lazy='dynamic')) @hybrid_method def is_active_at(self, d): return self.start_date <= d <= self.end_date @is_active_at.expression def is_active_at(self, d): return (self.start_date <= d) & (d <= self.end_date) def can_edit(self, user, allow_admin=True): if not user: return False return user == self.created_by_user or (allow_admin and rb_is_admin(user)) def can_delete(self, user, allow_admin=True): if not user: return False return user == self.created_by_user or (allow_admin and rb_is_admin(user)) def can_override(self, user, room=None, explicit_only=False, allow_admin=True): """Check if a user can override the blocking. The following persons are authorized to override a blocking: - the creator of the blocking - anyone on the blocking's ACL - unless explicit_only is set: rb admins and room managers (if a room is given) """ if not user: return False if self.created_by_user == user: return True if not explicit_only: if allow_admin and rb_is_admin(user): return True if room and room.can_manage(user): return True return any(user in principal for principal in iter_acl(self.allowed)) @property def external_details_url(self): return url_for('rb.blocking_link', blocking_id=self.id, _external=True) def __repr__(self): return format_repr(self, 'id', 'start_date', 'end_date', _text=self.reason)
class SurveyItem(DescriptionMixin, db.Model): __tablename__ = 'items' __table_args__ = (db.CheckConstraint( 'type != {type} OR (' 'title IS NOT NULL AND ' 'is_required IS NOT NULL AND ' 'field_type IS NOT NULL AND ' 'parent_id IS NOT NULL AND ' 'display_as_section IS NULL)'.format(type=SurveyItemType.question), 'valid_question'), db.CheckConstraint( 'type != {type} OR (' 'title IS NOT NULL AND ' 'is_required IS NULL AND ' 'field_type IS NULL AND ' "field_data::text = '{{}}' AND " 'parent_id IS NULL AND ' 'display_as_section IS NOT NULL)'.format( type=SurveyItemType.section), 'valid_section'), db.CheckConstraint( 'type != {type} OR (' 'title IS NULL AND ' 'is_required IS NULL AND ' 'field_type IS NULL AND ' "field_data::text = '{{}}' AND " 'parent_id IS NOT NULL AND ' 'display_as_section IS NULL)'.format( type=SurveyItemType.text), 'valid_text'), { 'schema': 'event_surveys' }) __mapper_args__ = {'polymorphic_on': 'type', 'polymorphic_identity': None} possible_render_modes = {RenderMode.markdown} default_render_mode = RenderMode.markdown #: The ID of the item id = db.Column(db.Integer, primary_key=True) #: The ID of the survey survey_id = db.Column( db.Integer, db.ForeignKey('event_surveys.surveys.id'), index=True, nullable=False, ) #: The ID of the parent section item (NULL for top-level items, i.e. sections) parent_id = db.Column( db.Integer, db.ForeignKey('event_surveys.items.id'), index=True, nullable=True, ) #: The position of the item in the survey form position = db.Column(db.Integer, nullable=False, default=_get_next_position) #: The type of the survey item type = db.Column(PyIntEnum(SurveyItemType), nullable=False) #: The title of the item title = db.Column(db.String, nullable=True, default=_get_item_default_title) #: If a section should be rendered as a section display_as_section = db.Column(db.Boolean, nullable=True) # The following columns are only used for SurveyQuestion objects, but by # specifying them here we can access them without an extra query when we # query SurveyItem objects directly instead of going through a subclass. # This is done e.g. when using the Survey.top_level_items relationship. #: If the question must be answered (wtforms DataRequired) is_required = db.Column(db.Boolean, nullable=True) #: The type of the field used for the question field_type = db.Column(db.String, nullable=True) #: Field-specific data (such as choices for multi-select fields) field_data = db.Column(JSONB, nullable=False, default={}) # relationship backrefs: # - parent (SurveySection.children) # - survey (Survey.items) def to_dict(self): """Return a json-serializable representation of this object. Subclasses must add their own data to the dict. """ return { 'type': self.type.name, 'title': self.title, 'description': self.description }
class Contribution(DescriptionMixin, ProtectionManagersMixin, LocationMixin, AttachedItemsMixin, AttachedNotesMixin, PersonLinkDataMixin, AuthorsSpeakersMixin, CustomFieldsMixin, db.Model): __tablename__ = 'contributions' __auto_table_args = ( db.Index(None, 'friendly_id', 'event_id', unique=True, postgresql_where=db.text('NOT is_deleted')), db.Index(None, 'event_id', 'track_id'), db.Index(None, 'event_id', 'abstract_id'), db.Index(None, 'abstract_id', unique=True, postgresql_where=db.text('NOT is_deleted')), db.CheckConstraint( "session_block_id IS NULL OR session_id IS NOT NULL", 'session_block_if_session'), db.CheckConstraint("date_trunc('minute', duration) = duration", 'duration_no_seconds'), db.ForeignKeyConstraint( ['session_block_id', 'session_id'], ['events.session_blocks.id', 'events.session_blocks.session_id']), { 'schema': 'events' }) location_backref_name = 'contributions' disallowed_protection_modes = frozenset() inheriting_have_acl = True possible_render_modes = {RenderMode.html, RenderMode.markdown} default_render_mode = RenderMode.markdown allow_relationship_preloading = True PRELOAD_EVENT_ATTACHED_ITEMS = True PRELOAD_EVENT_NOTES = True ATTACHMENT_FOLDER_ID_COLUMN = 'contribution_id' @classmethod def allocate_friendly_ids(cls, event, n): """Allocate n Contribution friendly_ids. This is needed so that we can allocate all IDs in one go. Not doing so could result in DB deadlocks. All operations that create more than one contribution should use this method. :param event: the :class:`Event` in question :param n: the number of ids to pre-allocate """ from indico.modules.events import Event fid = increment_and_get(Event._last_friendly_contribution_id, Event.id == event.id, n) friendly_ids = g.setdefault('friendly_ids', {}) friendly_ids.setdefault(cls, {})[event.id] = list( range(fid - n + 1, fid + 1)) @declared_attr def __table_args__(cls): return auto_table_args(cls) id = db.Column(db.Integer, primary_key=True) #: The human-friendly ID for the contribution friendly_id = db.Column(db.Integer, nullable=False, default=_get_next_friendly_id) event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False) session_id = db.Column(db.Integer, db.ForeignKey('events.sessions.id'), index=True, nullable=True) session_block_id = db.Column(db.Integer, db.ForeignKey('events.session_blocks.id'), index=True, nullable=True) track_id = db.Column(db.Integer, db.ForeignKey('events.tracks.id', ondelete='SET NULL'), index=True, nullable=True) abstract_id = db.Column(db.Integer, db.ForeignKey('event_abstracts.abstracts.id'), index=True, nullable=True) type_id = db.Column(db.Integer, db.ForeignKey('events.contribution_types.id'), index=True, nullable=True) title = db.Column(db.String, nullable=False) code = db.Column(db.String, nullable=False, default='') duration = db.Column(db.Interval, nullable=False) board_number = db.Column(db.String, nullable=False, default='') keywords = db.Column(ARRAY(db.String), nullable=False, default=[]) is_deleted = db.Column(db.Boolean, nullable=False, default=False) #: The last user-friendly sub-contribution ID _last_friendly_subcontribution_id = db.deferred( db.Column('last_friendly_subcontribution_id', db.Integer, nullable=False, default=0)) event = db.relationship( 'Event', lazy=True, backref=db.backref( 'contributions', primaryjoin= '(Contribution.event_id == Event.id) & ~Contribution.is_deleted', cascade='all, delete-orphan', lazy=True)) session = db.relationship( 'Session', lazy=True, backref=db.backref( 'contributions', primaryjoin= '(Contribution.session_id == Session.id) & ~Contribution.is_deleted', lazy=True)) session_block = db.relationship( 'SessionBlock', lazy=True, foreign_keys=[session_block_id], backref=db.backref( 'contributions', primaryjoin= '(Contribution.session_block_id == SessionBlock.id) & ~Contribution.is_deleted', lazy=True)) type = db.relationship('ContributionType', lazy=True, backref=db.backref('contributions', lazy=True)) acl_entries = db.relationship('ContributionPrincipal', lazy=True, cascade='all, delete-orphan', collection_class=set, backref='contribution') subcontributions = db.relationship( 'SubContribution', lazy=True, primaryjoin= '(SubContribution.contribution_id == Contribution.id) & ~SubContribution.is_deleted', order_by='SubContribution.position', cascade='all, delete-orphan', backref=db.backref( 'contribution', primaryjoin='SubContribution.contribution_id == Contribution.id', lazy=True)) abstract = db.relationship( 'Abstract', lazy=True, backref=db.backref( 'contribution', primaryjoin= '(Contribution.abstract_id == Abstract.id) & ~Contribution.is_deleted', lazy=True, uselist=False)) track = db.relationship( 'Track', lazy=True, backref=db.backref( 'contributions', primaryjoin= '(Contribution.track_id == Track.id) & ~Contribution.is_deleted', lazy=True, passive_deletes=True)) #: External references associated with this contribution references = db.relationship('ContributionReference', lazy=True, cascade='all, delete-orphan', backref=db.backref('contribution', lazy=True)) #: Persons associated with this contribution person_links = db.relationship('ContributionPersonLink', lazy=True, cascade='all, delete-orphan', backref=db.backref('contribution', lazy=True)) #: Data stored in abstract/contribution fields field_values = db.relationship('ContributionFieldValue', lazy=True, cascade='all, delete-orphan', backref=db.backref('contribution', lazy=True)) #: The accepted paper revision _accepted_paper_revision = db.relationship( 'PaperRevision', lazy=True, viewonly=True, uselist=False, primaryjoin= ('(PaperRevision._contribution_id == Contribution.id) & (PaperRevision.state == {})' .format(PaperRevisionState.accepted)), ) #: Paper files not submitted for reviewing pending_paper_files = db.relationship( 'PaperFile', lazy=True, viewonly=True, primaryjoin= '(PaperFile._contribution_id == Contribution.id) & (PaperFile.revision_id.is_(None))', ) #: Paper reviewing judges paper_judges = db.relationship('User', secondary='event_paper_reviewing.judges', collection_class=set, lazy=True, backref=db.backref( 'judge_for_contributions', collection_class=set, lazy=True)) #: Paper content reviewers paper_content_reviewers = db.relationship( 'User', secondary='event_paper_reviewing.content_reviewers', collection_class=set, lazy=True, backref=db.backref('content_reviewer_for_contributions', collection_class=set, lazy=True)) #: Paper layout reviewers paper_layout_reviewers = db.relationship( 'User', secondary='event_paper_reviewing.layout_reviewers', collection_class=set, lazy=True, backref=db.backref('layout_reviewer_for_contributions', collection_class=set, lazy=True)) @declared_attr def _paper_last_revision(cls): # Incompatible with joinedload subquery = (db.select([ db.func.max(PaperRevision.submitted_dt) ]).where(PaperRevision._contribution_id == cls.id).correlate_except( PaperRevision).scalar_subquery()) return db.relationship('PaperRevision', uselist=False, lazy=True, viewonly=True, primaryjoin=db.and_( PaperRevision._contribution_id == cls.id, PaperRevision.submitted_dt == subquery)) # relationship backrefs: # - _paper_files (PaperFile._contribution) # - _paper_revisions (PaperRevision._contribution) # - attachment_folders (AttachmentFolder.contribution) # - editables (Editable.contribution) # - legacy_mapping (LegacyContributionMapping.contribution) # - note (EventNote.contribution) # - room_reservation_links (ReservationLink.contribution) # - timetable_entry (TimetableEntry.contribution) # - vc_room_associations (VCRoomEventAssociation.linked_contrib) @declared_attr def is_scheduled(cls): from indico.modules.events.timetable.models.entries import TimetableEntry query = (db.exists([1]).where(TimetableEntry.contribution_id == cls.id).correlate_except(TimetableEntry)) return db.column_property(query, deferred=True) @declared_attr def subcontribution_count(cls): from indico.modules.events.contributions.models.subcontributions import SubContribution query = (db.select([ db.func.count(SubContribution.id) ]).where((SubContribution.contribution_id == cls.id) & ~SubContribution.is_deleted).correlate_except( SubContribution).scalar_subquery()) return db.column_property(query, deferred=True) @declared_attr def _paper_revision_count(cls): query = (db.select([ db.func.count(PaperRevision.id) ]).where(PaperRevision._contribution_id == cls.id).correlate_except( PaperRevision).scalar_subquery()) return db.column_property(query, deferred=True) def __init__(self, **kwargs): # explicitly initialize those relationships with None to avoid # an extra query to check whether there is an object associated # when assigning a new one (e.g. during cloning) kwargs.setdefault('note', None) kwargs.setdefault('timetable_entry', None) super().__init__(**kwargs) @classmethod def preload_acl_entries(cls, event): cls.preload_relationships(cls.query.with_parent(event), 'acl_entries') @property def location_parent(self): if self.session_block_id is not None: return self.session_block elif self.session_id is not None: return self.session else: return self.event @property def protection_parent(self): return self.session if self.session_id is not None else self.event @property def start_dt(self): return self.timetable_entry.start_dt if self.timetable_entry else None @property def end_dt(self): return self.timetable_entry.start_dt + self.duration if self.timetable_entry else None @property def start_dt_poster(self): if self.session and self.session.is_poster and self.timetable_entry and self.timetable_entry.parent: return self.timetable_entry.parent.start_dt @property def end_dt_poster(self): if self.session and self.session.is_poster and self.timetable_entry and self.timetable_entry.parent: return self.timetable_entry.parent.end_dt @property def duration_poster(self): if self.session and self.session.is_poster and self.timetable_entry and self.timetable_entry.parent: return self.timetable_entry.parent.duration @property def start_dt_display(self): """The displayed start time of the contribution. This is the start time of the poster session if applicable, otherwise the start time of the contribution itself. """ return self.start_dt_poster or self.start_dt @property def end_dt_display(self): """The displayed end time of the contribution. This is the end time of the poster session if applicable, otherwise the end time of the contribution itself. """ return self.end_dt_poster or self.end_dt @property def duration_display(self): """The displayed duration of the contribution. This is the duration of the poster session if applicable, otherwise the duration of the contribution itself. """ return self.duration_poster or self.duration @property def submitters(self): return { person_link for person_link in self.person_links if person_link.is_submitter } @locator_property def locator(self): return dict(self.event.locator, contrib_id=self.id) @property def verbose_title(self): return f'#{self.friendly_id} ({self.title})' @property def paper(self): return Paper(self) if self._paper_last_revision else None @property def allowed_types_for_editable(self): from indico.modules.events.editing.settings import editable_type_settings if not self.event.has_feature('editing'): return [] submitted_for = {editable.type.name for editable in self.editables} return [ editable_type for editable_type in self.event.editable_types if editable_type not in submitted_for and editable_type_settings[EditableType[editable_type]].get( self.event, 'submission_enabled') ] @property def enabled_editables(self): """Return all submitted editables with enabled types.""" from indico.modules.events.editing.settings import editing_settings if not self.event.has_feature('editing'): return [] enabled_editable_types = editing_settings.get(self.event, 'editable_types') enabled_editables = [ editable for editable in self.editables if editable.type.name in enabled_editable_types ] order = list(EditableType) return sorted(enabled_editables, key=lambda editable: order.index(editable.type)) @property def has_published_editables(self): return any(e.published_revision_id is not None for e in self.enabled_editables) @property def slug(self): return slugify(self.friendly_id, self.title, maxlen=30) def is_paper_reviewer(self, user): return user in self.paper_content_reviewers or user in self.paper_layout_reviewers def __repr__(self): return format_repr(self, 'id', is_deleted=False, _text=self.title) def can_manage(self, user, permission=None, allow_admin=True, check_parent=True, explicit_permission=False): if super().can_manage(user, permission, allow_admin=allow_admin, check_parent=check_parent, explicit_permission=explicit_permission): return True if (check_parent and self.session_id is not None and self.session.can_manage( user, 'coordinate', allow_admin=allow_admin, explicit_permission=explicit_permission) and session_coordinator_priv_enabled(self.event, 'manage-contributions')): return True return False def get_non_inheriting_objects(self): """Get a set of child objects that do not inherit protection.""" return get_non_inheriting_objects(self) def is_user_associated(self, user, check_abstract=False): if user is None: return False if check_abstract and self.abstract and self.abstract.submitter == user: return True return any(pl.person.user == user for pl in self.person_links if pl.person.user) def can_submit_proceedings(self, user): """Whether the user can submit editables/papers.""" if user is None: return False # The submitter of the original abstract is always authorized if self.abstract and self.abstract.submitter == user: return True # Otherwise only users with submission rights are authorized return self.can_manage(user, 'submit', allow_admin=False, check_parent=False) def get_editable(self, editable_type): """Get the editable of the given type.""" return next((e for e in self.editables if e.type == editable_type), None) def log(self, *args, **kwargs): """Log with prefilled metadata for the contribution.""" self.event.log(*args, meta={'contribution_id': self.id}, **kwargs)
class EventNoteRevision(db.Model): __tablename__ = 'note_revisions' __table_args__ = {'schema': 'events'} #: The ID of the revision id = db.Column( db.Integer, primary_key=True ) #: The ID of the associated note note_id = db.Column( db.Integer, db.ForeignKey('events.notes.id'), nullable=False, index=True ) #: The user who created the revision user_id = db.Column( db.Integer, db.ForeignKey('users.users.id'), nullable=False, index=True ) #: The date/time when the revision was created created_dt = db.Column( UTCDateTime, nullable=False, default=now_utc ) #: How the note is rendered render_mode = db.Column( PyIntEnum(RenderMode), nullable=False ) #: The raw source of the note as provided by the user source = db.Column( db.Text, nullable=False ) #: The rendered HTML of the note html = db.Column( db.Text, nullable=False ) #: The user who created the revision user = db.relationship( 'User', lazy=True, backref=db.backref( 'event_notes_revisions', lazy='dynamic' ) ) # relationship backrefs: # - note (EventNote.revisions) def __repr__(self): render_mode = self.render_mode.name if self.render_mode is not None else None source = text_to_repr(self.source, html=True) return '<EventNoteRevision({}, {}, {}, {}): "{}">'.format(self.id, self.note_id, render_mode, self.created_dt, source)
class RegistrationFormItem(db.Model): """Generic registration form item""" __tablename__ = 'form_items' __table_args__ = ( db.CheckConstraint( "(input_type IS NULL) = (type NOT IN ({t.field}, {t.field_pd}))". format(t=RegistrationFormItemType), name='valid_input'), db.CheckConstraint("NOT is_manager_only OR type = {type}".format( type=RegistrationFormItemType.section), name='valid_manager_only'), db.CheckConstraint( "(type IN ({t.section}, {t.section_pd})) = (parent_id IS NULL)". format(t=RegistrationFormItemType), name='top_level_sections'), db.CheckConstraint( "(type != {type}) = (personal_data_type IS NULL)".format( type=RegistrationFormItemType.field_pd), name='pd_field_type'), db.CheckConstraint( "NOT is_deleted OR (type NOT IN ({t.section_pd}, {t.field_pd}))". format(t=RegistrationFormItemType), name='pd_not_deleted'), db.CheckConstraint("is_enabled OR type != {type}".format( type=RegistrationFormItemType.section_pd), name='pd_section_enabled'), db.CheckConstraint( "is_enabled OR type != {type} OR personal_data_type NOT IN " "({pt.email}, {pt.first_name}, {pt.last_name})".format( type=RegistrationFormItemType.field_pd, pt=PersonalDataType), name='pd_field_enabled'), db.CheckConstraint( "is_required OR type != {type} OR personal_data_type NOT IN " "({pt.email}, {pt.first_name}, {pt.last_name})".format( type=RegistrationFormItemType.field_pd, pt=PersonalDataType), name='pd_field_required'), db.CheckConstraint( "current_data_id IS NULL OR type IN ({t.field}, {t.field_pd})". format(t=RegistrationFormItemType), name='current_data_id_only_field'), db.Index('ix_uq_form_items_pd_section', 'registration_form_id', unique=True, postgresql_where=db.text('type = {type}'.format( type=RegistrationFormItemType.section_pd))), db.Index('ix_uq_form_items_pd_field', 'registration_form_id', 'personal_data_type', unique=True, postgresql_where=db.text('type = {type}'.format( type=RegistrationFormItemType.field_pd))), { 'schema': 'event_registration' }) __mapper_args__ = {'polymorphic_on': 'type', 'polymorphic_identity': None} #: The ID of the object id = db.Column(db.Integer, primary_key=True) #: The ID of the registration form registration_form_id = db.Column( db.Integer, db.ForeignKey('event_registration.forms.id'), index=True, nullable=False) #: The type of the registration form item type = db.Column(PyIntEnum(RegistrationFormItemType), nullable=False) #: The type of a personal data field personal_data_type = db.Column(PyIntEnum(PersonalDataType), nullable=True) #: The ID of the parent form item parent_id = db.Column(db.Integer, db.ForeignKey('event_registration.form_items.id'), index=True, nullable=True) position = db.Column(db.Integer, nullable=False, default=_get_next_position) #: The title of this field title = db.Column(db.String, nullable=False) #: Description of this field description = db.Column(db.String, nullable=False, default='') #: Whether the field is enabled is_enabled = db.Column(db.Boolean, nullable=False, default=True) #: Whether field has been "deleted" is_deleted = db.Column(db.Boolean, nullable=False, default=False) #: determines if the field is mandatory is_required = db.Column(db.Boolean, nullable=False, default=False) #: if the section is only accessible to managers is_manager_only = db.Column(db.Boolean, nullable=False, default=False) #: input type of this field input_type = db.Column(db.String, nullable=True) #: unversioned field data data = db.Column(JSON, nullable=False, default=lambda: None) #: The ID of the latest data current_data_id = db.Column(db.Integer, db.ForeignKey( 'event_registration.form_field_data.id', use_alter=True), index=True, nullable=True) #: The latest value of the field current_data = db.relationship( 'RegistrationFormFieldData', primaryjoin= 'RegistrationFormItem.current_data_id == RegistrationFormFieldData.id', foreign_keys=current_data_id, lazy=True, post_update=True) #: The list of all versions of the field data data_versions = db.relationship( 'RegistrationFormFieldData', primaryjoin= 'RegistrationFormItem.id == RegistrationFormFieldData.field_id', foreign_keys='RegistrationFormFieldData.field_id', lazy=True, cascade='all, delete-orphan', backref=db.backref('field', lazy=False)) # The children of the item and the parent backref children = db.relationship('RegistrationFormItem', lazy=True, order_by='RegistrationFormItem.position', backref=db.backref('parent', lazy=False, remote_side=[id])) # relationship backrefs: # - parent (RegistrationFormItem.children) # - registration_form (RegistrationForm.form_items) @property def view_data(self): """Returns object with data that Angular can understand""" return dict(id=self.id, description=self.description, position=self.position) @hybrid_property def is_section(self): return self.type in { RegistrationFormItemType.section, RegistrationFormItemType.section_pd } @is_section.expression def is_section(cls): return cls.type.in_([ RegistrationFormItemType.section, RegistrationFormItemType.section_pd ]) @hybrid_property def is_field(self): return self.type in { RegistrationFormItemType.field, RegistrationFormItemType.field_pd } @is_field.expression def is_field(cls): return cls.type.in_([ RegistrationFormItemType.field, RegistrationFormItemType.field_pd ]) @hybrid_property def is_visible(self): return self.is_enabled and not self.is_deleted and ( self.parent_id is None or self.parent.is_visible) @is_visible.expression def is_visible(cls): sections = aliased(RegistrationFormSection) query = (db.session.query(literal(True)).filter( sections.id == cls.parent_id).filter(~sections.is_deleted).filter( sections.is_enabled).exists()) return cls.is_enabled & ~cls.is_deleted & ( (cls.parent_id == None) | query) # noqa @return_ascii def __repr__(self): return format_repr(self, 'id', 'registration_form_id', is_enabled=True, is_deleted=False, is_manager_only=False, _text=self.title)
# You should have received a copy of the GNU General Public License # along with Indico; if not, see <http://www.gnu.org/licenses/>. from __future__ import unicode_literals from indico.core.db import db from indico.util.string import format_repr, return_ascii RoomEquipmentAssociation = db.Table( 'room_equipment', db.metadata, db.Column( 'equipment_id', db.Integer, db.ForeignKey('roombooking.equipment_types.id'), primary_key=True, ), db.Column( 'room_id', db.Integer, db.ForeignKey('roombooking.rooms.id'), primary_key=True ), schema='roombooking' ) equipment_features_table = db.Table( 'equipment_features', db.metadata,
def session_block_id(cls): if LinkType.session_block in cls.allowed_link_types: return db.Column(db.Integer, db.ForeignKey('events.session_blocks.id'), nullable=True, index=True)
def subcontribution_id(cls): if LinkType.subcontribution in cls.allowed_link_types: return db.Column(db.Integer, db.ForeignKey('events.subcontributions.id'), nullable=True, index=True)
def linked_event_id(cls): if LinkType.event in cls.allowed_link_types: return db.Column(db.Integer, db.ForeignKey('events.events.id'), nullable=True, index=True)
def event_id(cls): return db.Column(db.Integer, db.ForeignKey('events.events.id'), nullable=True, index=True)
def category_id(cls): if LinkType.category in cls.allowed_link_types: return db.Column(db.Integer, db.ForeignKey('categories.categories.id'), nullable=True, index=True)
class ReservationOccurrence(db.Model, Serializer): __tablename__ = 'reservation_occurrences' __table_args__ = (db.CheckConstraint("rejection_reason != ''", 'rejection_reason_not_empty'), { 'schema': 'roombooking' }) __api_public__ = (('start_dt', 'startDT'), ('end_dt', 'endDT'), 'is_cancelled', 'is_rejected') #: A relationship loading strategy that will avoid loading the #: users linked to a reservation. You want to use this in pretty #: much all cases where you eager-load the `reservation` relationship. NO_RESERVATION_USER_STRATEGY = defaultload('reservation') NO_RESERVATION_USER_STRATEGY.lazyload('created_by_user') NO_RESERVATION_USER_STRATEGY.noload('booked_for_user') reservation_id = db.Column(db.Integer, db.ForeignKey('roombooking.reservations.id'), nullable=False, primary_key=True) start_dt = db.Column(db.DateTime, nullable=False, primary_key=True, index=True) end_dt = db.Column(db.DateTime, nullable=False, index=True) notification_sent = db.Column(db.Boolean, nullable=False, default=False) state = db.Column(PyIntEnum(ReservationOccurrenceState), nullable=False, default=ReservationOccurrenceState.valid) rejection_reason = db.Column(db.String, nullable=True) # relationship backrefs: # - reservation (Reservation.occurrences) @hybrid_property def date(self): return self.start_dt.date() @date.expression def date(self): return cast(self.start_dt, Date) @hybrid_property def is_valid(self): return self.state == ReservationOccurrenceState.valid @hybrid_property def is_cancelled(self): return self.state == ReservationOccurrenceState.cancelled @hybrid_property def is_rejected(self): return self.state == ReservationOccurrenceState.rejected @hybrid_property def is_within_cancel_grace_period(self): return self.start_dt >= datetime.now() - timedelta(minutes=10) @return_ascii def __repr__(self): return format_repr(self, 'reservation_id', 'start_dt', 'end_dt', 'state') @classmethod def create_series_for_reservation(cls, reservation): for o in cls.iter_create_occurrences(reservation.start_dt, reservation.end_dt, reservation.repetition): o.reservation = reservation @classmethod def create_series(cls, start, end, repetition): return list(cls.iter_create_occurrences(start, end, repetition)) @classmethod def iter_create_occurrences(cls, start, end, repetition): for start in cls.iter_start_time(start, end, repetition): end = datetime.combine(start.date(), end.time()) yield ReservationOccurrence(start_dt=start, end_dt=end) @staticmethod def iter_start_time(start, end, repetition): from indico.modules.rb.models.reservations import RepeatFrequency repeat_frequency, repeat_interval = repetition if repeat_frequency == RepeatFrequency.NEVER: return [start] elif repeat_frequency == RepeatFrequency.DAY: if repeat_interval != 1: raise IndicoError(u'Unsupported interval') return rrule.rrule(rrule.DAILY, dtstart=start, until=end) elif repeat_frequency == RepeatFrequency.WEEK: if repeat_interval <= 0: raise IndicoError(u'Unsupported interval') return rrule.rrule(rrule.WEEKLY, dtstart=start, until=end, interval=repeat_interval) elif repeat_frequency == RepeatFrequency.MONTH: if repeat_interval == 0: raise IndicoError(u'Unsupported interval') position = int(ceil(start.day / 7.0)) if position == 5: # The fifth weekday of the month will always be the last one position = -1 return rrule.rrule(rrule.MONTHLY, dtstart=start, until=end, byweekday=start.weekday(), bysetpos=position, interval=repeat_interval) raise IndicoError(u'Unexpected frequency {}'.format(repeat_frequency)) @staticmethod def filter_overlap(occurrences): return or_( db_dates_overlap(ReservationOccurrence, 'start_dt', occ.start_dt, 'end_dt', occ.end_dt) for occ in occurrences) @classmethod def find_overlapping_with(cls, room, occurrences, skip_reservation_id=None): from indico.modules.rb.models.reservations import Reservation return (ReservationOccurrence.find( Reservation.room == room, Reservation.id != skip_reservation_id, ReservationOccurrence.is_valid, ReservationOccurrence.filter_overlap(occurrences), _eager=ReservationOccurrence.reservation, _join=ReservationOccurrence.reservation).options( cls.NO_RESERVATION_USER_STRATEGY)) def can_reject(self, user, allow_admin=True): if not self.is_valid: return False return self.reservation.can_reject(user, allow_admin=allow_admin) def can_cancel(self, user, allow_admin=True): if user is None: return False if not self.is_valid or self.end_dt < datetime.now(): return False booking = self.reservation booked_or_owned_by_user = booking.is_owned_by( user) or booking.is_booked_for(user) if booking.is_rejected or booking.is_cancelled or booking.is_archived: return False if booked_or_owned_by_user and self.is_within_cancel_grace_period: return True return allow_admin and rb_is_admin(user) @proxy_to_reservation_if_last_valid_occurrence def cancel(self, user, reason=None, silent=False): self.state = ReservationOccurrenceState.cancelled self.rejection_reason = reason or None signals.rb.booking_occurrence_state_changed.send(self) if not silent: log = [ u'Day cancelled: {}'.format( format_date(self.date).decode('utf-8')) ] if reason: log.append(u'Reason: {}'.format(reason)) self.reservation.add_edit_log( ReservationEditLog(user_name=user.full_name, info=log)) from indico.modules.rb.notifications.reservation_occurrences import notify_cancellation notify_cancellation(self) @proxy_to_reservation_if_last_valid_occurrence def reject(self, user, reason, silent=False): self.state = ReservationOccurrenceState.rejected self.rejection_reason = reason or None signals.rb.booking_occurrence_state_changed.send(self) if not silent: log = [ u'Day rejected: {}'.format( format_date(self.date).decode('utf-8')), u'Reason: {}'.format(reason) ] self.reservation.add_edit_log( ReservationEditLog(user_name=user.full_name, info=log)) from indico.modules.rb.notifications.reservation_occurrences import notify_rejection notify_rejection(self) def get_overlap(self, occurrence, skip_self=False): if self.reservation and occurrence.reservation and self.reservation.room_id != occurrence.reservation.room_id: raise ValueError( 'ReservationOccurrence objects of different rooms') if skip_self and self.reservation and occurrence.reservation and self.reservation == occurrence.reservation: return None, None return date_time.get_overlap((self.start_dt, self.end_dt), (occurrence.start_dt, occurrence.end_dt)) def overlaps(self, occurrence, skip_self=False): if self.reservation and occurrence.reservation and self.reservation.room_id != occurrence.reservation.room_id: raise ValueError( 'ReservationOccurrence objects of different rooms') if skip_self and self.reservation and occurrence.reservation and self.reservation == occurrence.reservation: return False return date_time.overlaps((self.start_dt, self.end_dt), (occurrence.start_dt, occurrence.end_dt))
class SessionBlock(LocationMixin, db.Model): __tablename__ = 'session_blocks' __auto_table_args = (db.UniqueConstraint('id', 'session_id'), # useless but needed for the compound fkey {'schema': 'events'}) location_backref_name = 'session_blocks' @declared_attr def __table_args__(cls): return auto_table_args(cls) id = db.Column( db.Integer, primary_key=True ) session_id = db.Column( db.Integer, db.ForeignKey('events.sessions.id'), index=True, nullable=False ) title = db.Column( db.String, nullable=False, default='' ) code = db.Column( db.String, nullable=False, default='' ) duration = db.Column( db.Interval, nullable=False ) #: Persons associated with this session block person_links = db.relationship( 'SessionBlockPersonLink', lazy=True, cascade='all, delete-orphan', backref=db.backref( 'session_block', lazy=True ) ) # relationship backrefs: # - contributions (Contribution.session_block) # - legacy_mapping (LegacySessionBlockMapping.session_block) # - room_reservation_links (ReservationLink.session_block) # - session (Session.blocks) # - timetable_entry (TimetableEntry.session_block) # - vc_room_associations (VCRoomEventAssociation.linked_block) @declared_attr def contribution_count(cls): from indico.modules.events.contributions.models.contributions import Contribution query = (db.select([db.func.count(Contribution.id)]) .where((Contribution.session_block_id == cls.id) & ~Contribution.is_deleted) .correlate_except(Contribution) .scalar_subquery()) return db.column_property(query, deferred=True) def __init__(self, **kwargs): # explicitly initialize those relationships with None to avoid # an extra query to check whether there is an object associated # when assigning a new one (e.g. during cloning) kwargs.setdefault('timetable_entry', None) super().__init__(**kwargs) @property def event(self): return self.session.event @locator_property def locator(self): return dict(self.session.locator, block_id=self.id) @property def location_parent(self): return self.session def can_access(self, user, allow_admin=True): return self.session.can_access(user, allow_admin=allow_admin) @property def has_note(self): return self.session.has_note @property def note(self): return self.session.note @property def full_title(self): return f'{self.session.title}: {self.title}' if self.title else self.session.title def can_manage(self, user, allow_admin=True): return self.session.can_manage_blocks(user, allow_admin=allow_admin) def can_manage_attachments(self, user): return self.session.can_manage_attachments(user) def can_edit_note(self, user): return self.session.can_edit_note(user) @property def start_dt(self): return self.timetable_entry.start_dt if self.timetable_entry else None @property def end_dt(self): return self.timetable_entry.start_dt + self.duration if self.timetable_entry else None def __repr__(self): return format_repr(self, 'id', _text=self.title or None)
class BlockedRoom(db.Model): __tablename__ = 'blocked_rooms' __table_args__ = {'schema': 'roombooking'} State = BlockedRoomState # make it available here for convenience id = db.Column(db.Integer, primary_key=True) state = db.Column(PyIntEnum(BlockedRoomState), nullable=False, default=BlockedRoomState.pending) rejected_by = db.Column(db.String) rejection_reason = db.Column(db.String) blocking_id = db.Column(db.Integer, db.ForeignKey('roombooking.blockings.id'), nullable=False) room_id = db.Column(db.Integer, db.ForeignKey('roombooking.rooms.id'), nullable=False, index=True) # relationship backrefs: # - blocking (Blocking.blocked_rooms) # - room (Room.blocked_rooms) @property def state_name(self): return BlockedRoomState(self.state).title @classmethod def find_with_filters(cls, filters): q = cls.find(_eager=BlockedRoom.blocking, _join=BlockedRoom.blocking) if filters.get('room_ids'): q = q.filter(BlockedRoom.room_id.in_(filters['room_ids'])) if filters.get('start_date') and filters.get('end_date'): q = q.filter(Blocking.start_date <= filters['end_date'], Blocking.end_date >= filters['start_date']) if 'state' in filters: q = q.filter(BlockedRoom.state == filters['state']) return q def reject(self, user=None, reason=None): """Reject the room blocking.""" self.state = BlockedRoomState.rejected if reason: self.rejection_reason = reason if user: self.rejected_by = user.full_name notify_request_response(self) def approve(self, notify_blocker=True): """Approve the room blocking, rejecting all colliding reservations/occurrences.""" self.state = BlockedRoomState.accepted # Get colliding reservations start_dt = datetime.combine(self.blocking.start_date, time()) end_dt = datetime.combine(self.blocking.end_date, time(23, 59, 59)) reservation_criteria = [ Reservation.room_id == self.room_id, ~Reservation.is_rejected, ~Reservation.is_cancelled ] # Whole reservations to reject reservations = Reservation.find_all(Reservation.start_dt >= start_dt, Reservation.end_dt <= end_dt, *reservation_criteria) # Single occurrences to reject occurrences = ReservationOccurrence.find_all( ReservationOccurrence.start_dt >= start_dt, ReservationOccurrence.end_dt <= end_dt, ReservationOccurrence.is_valid, ~ReservationOccurrence.reservation_id.in_( map(attrgetter('id'), reservations)) if reservations else True, *reservation_criteria, _join=Reservation) reason = 'Conflict with blocking {}: {}'.format( self.blocking.id, self.blocking.reason) for reservation in reservations: if self.blocking.can_be_overridden(reservation.created_by_user, reservation.room): continue reservation.reject(self.blocking.created_by_user, reason) for occurrence in occurrences: reservation = occurrence.reservation if self.blocking.can_be_overridden(reservation.created_by_user, reservation.room): continue occurrence.reject(self.blocking.created_by_user, reason) if notify_blocker: # We only need to notify the blocking creator if the blocked room wasn't approved yet. # This is the case if it's a new blocking for a room managed by the creator notify_request_response(self) @return_ascii def __repr__(self): return '<BlockedRoom({0}, {1}, {2})>'.format(self.blocking_id, self.room_id, self.state_name)
class VCRoomEventAssociation(db.Model): __tablename__ = 'vc_room_events' __table_args__ = tuple(_make_checks()) + (db.Index( None, 'data', postgresql_using='gin'), { 'schema': 'events' }) #: Association ID id = db.Column(db.Integer, primary_key=True) #: ID of the event event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, autoincrement=False, nullable=False) #: ID of the videoconference room vc_room_id = db.Column(db.Integer, db.ForeignKey('events.vc_rooms.id'), index=True, nullable=False) #: Type of the object the vc_room is linked to link_type = db.Column(PyIntEnum(VCRoomLinkType), nullable=False) linked_event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=True) session_block_id = db.Column(db.Integer, db.ForeignKey('events.session_blocks.id'), index=True, nullable=True) contribution_id = db.Column(db.Integer, db.ForeignKey('events.contributions.id'), index=True, nullable=True) #: If the vc room should be shown on the event page show = db.Column(db.Boolean, nullable=False, default=False) #: videoconference plugin-specific data data = db.Column(JSONB, nullable=False) #: The associated :class:VCRoom vc_room = db.relationship('VCRoom', lazy=False, backref=db.backref('events', cascade='all, delete-orphan')) #: The associated Event event = db.relationship('Event', foreign_keys=event_id, lazy=True, backref=db.backref('all_vc_room_associations', lazy='dynamic')) #: The linked event (if the VC room is attached to the event itself) linked_event = db.relationship('Event', foreign_keys=linked_event_id, lazy=True, backref=db.backref('vc_room_associations', lazy=True)) #: The linked contribution (if the VC room is attached to a contribution) linked_contrib = db.relationship('Contribution', lazy=True, backref=db.backref('vc_room_associations', lazy=True)) #: The linked session block (if the VC room is attached to a block) linked_block = db.relationship('SessionBlock', lazy=True, backref=db.backref('vc_room_associations', lazy=True)) @classmethod def register_link_events(cls): event_mapping = { cls.linked_block: lambda x: x.event, cls.linked_contrib: lambda x: x.event, cls.linked_event: lambda x: x } type_mapping = { cls.linked_event: VCRoomLinkType.event, cls.linked_block: VCRoomLinkType.block, cls.linked_contrib: VCRoomLinkType.contribution } def _set_link_type(link_type, target, value, *unused): if value is not None: target.link_type = link_type def _set_event_obj(fn, target, value, *unused): if value is not None: event = fn(value) assert event is not None target.event = event for rel, fn in event_mapping.iteritems(): if rel is not None: listen(rel, 'set', partial(_set_event_obj, fn)) for rel, link_type in type_mapping.iteritems(): if rel is not None: listen(rel, 'set', partial(_set_link_type, link_type)) @property def locator(self): return dict(self.event.locator, service=self.vc_room.type, event_vc_room_id=self.id) @hybrid_property def link_object(self): if self.link_type == VCRoomLinkType.event: return self.linked_event elif self.link_type == VCRoomLinkType.contribution: return self.linked_contrib else: return self.linked_block @link_object.setter def link_object(self, obj): self.linked_event = self.linked_contrib = self.linked_block = None if isinstance(obj, db.m.Event): self.linked_event = obj elif isinstance(obj, db.m.Contribution): self.linked_contrib = obj elif isinstance(obj, db.m.SessionBlock): self.linked_block = obj else: raise TypeError('Unexpected object: {}'.format(obj)) @link_object.comparator def link_object(cls): return _LinkObjectComparator(cls) @return_ascii def __repr__(self): return '<VCRoomEventAssociation({}, {})>'.format( self.event_id, self.vc_room) @classmethod @unify_event_args def find_for_event(cls, event, include_hidden=False, include_deleted=False, only_linked_to_event=False, **kwargs): """Returns a Query that retrieves the videoconference rooms for an event :param event: an indico Event :param only_linked_to_event: only retrieve the vc rooms linked to the whole event :param kwargs: extra kwargs to pass to ``find()`` """ if only_linked_to_event: kwargs['link_type'] = int(VCRoomLinkType.event) query = event.all_vc_room_associations if kwargs: query = query.filter_by(**kwargs) if not include_hidden: query = query.filter(cls.show) if not include_deleted: query = query.filter( VCRoom.status != VCRoomStatus.deleted).join(VCRoom) return query @classmethod @memoize_request def get_linked_for_event(cls, event): """Get a dict mapping link objects to event vc rooms""" return {vcr.link_object: vcr for vcr in cls.find_for_event(event)} def delete(self, user, delete_all=False): """Deletes a VC room from an event If the room is not used anywhere else, the room itself is also deleted. :param user: the user performing the deletion :param delete_all: if True, the room is detached from all events and deleted. """ vc_room = self.vc_room if delete_all: for assoc in vc_room.events[:]: Logger.get('modules.vc').info( "Detaching VC room {} from event {} ({})".format( vc_room, assoc.event, assoc.link_object)) vc_room.events.remove(assoc) else: Logger.get('modules.vc').info( "Detaching VC room {} from event {} ({})".format( vc_room, self.event, self.link_object)) vc_room.events.remove(self) db.session.flush() if not vc_room.events: Logger.get('modules.vc').info( "Deleting VC room {}".format(vc_room)) if vc_room.status != VCRoomStatus.deleted: vc_room.plugin.delete_room(vc_room, self.event) notify_deleted(vc_room.plugin, vc_room, self, self.event, user) db.session.delete(vc_room)
class TimetableEntry(db.Model): __tablename__ = 'timetable_entries' @declared_attr def __table_args__(cls): return (db.Index('ix_timetable_entries_start_dt_desc', cls.start_dt.desc()), _make_check(TimetableEntryType.SESSION_BLOCK, 'session_block_id'), _make_check(TimetableEntryType.CONTRIBUTION, 'contribution_id'), _make_check(TimetableEntryType.BREAK, 'break_id'), db.CheckConstraint( "type != {} OR parent_id IS NULL".format( TimetableEntryType.SESSION_BLOCK), 'valid_parent'), { 'schema': 'events' }) id = db.Column(db.Integer, primary_key=True) event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False) parent_id = db.Column( db.Integer, db.ForeignKey('events.timetable_entries.id'), index=True, nullable=True, ) session_block_id = db.Column(db.Integer, db.ForeignKey('events.session_blocks.id'), index=True, unique=True, nullable=True) contribution_id = db.Column(db.Integer, db.ForeignKey('events.contributions.id'), index=True, unique=True, nullable=True) break_id = db.Column(db.Integer, db.ForeignKey('events.breaks.id'), index=True, unique=True, nullable=True) type = db.Column(PyIntEnum(TimetableEntryType), nullable=False) start_dt = db.Column(UTCDateTime, nullable=False) event_new = db.relationship('Event', lazy=True, backref=db.backref( 'timetable_entries', order_by=lambda: TimetableEntry.start_dt, cascade='all, delete-orphan', lazy='dynamic')) session_block = db.relationship('SessionBlock', lazy=False, backref=db.backref( 'timetable_entry', cascade='all, delete-orphan', uselist=False, lazy=True)) contribution = db.relationship('Contribution', lazy=False, backref=db.backref( 'timetable_entry', cascade='all, delete-orphan', uselist=False, lazy=True)) break_ = db.relationship('Break', cascade='all, delete-orphan', single_parent=True, lazy=False, backref=db.backref('timetable_entry', cascade='all, delete-orphan', uselist=False, lazy=True)) children = db.relationship('TimetableEntry', order_by='TimetableEntry.start_dt', lazy=True, backref=db.backref('parent', remote_side=[id], lazy=True)) # relationship backrefs: # - parent (TimetableEntry.children) @property def object(self): if self.type == TimetableEntryType.SESSION_BLOCK: return self.session_block elif self.type == TimetableEntryType.CONTRIBUTION: return self.contribution elif self.type == TimetableEntryType.BREAK: return self.break_ @object.setter def object(self, value): from indico.modules.events.contributions import Contribution from indico.modules.events.sessions.models.blocks import SessionBlock from indico.modules.events.timetable.models.breaks import Break self.session_block = self.contribution = self.break_ = None if isinstance(value, SessionBlock): self.session_block = value elif isinstance(value, Contribution): self.contribution = value elif isinstance(value, Break): self.break_ = value elif value is not None: raise TypeError('Unexpected object: {}'.format(value)) @hybrid_property def duration(self): return self.object.duration if self.object is not None else None @duration.setter def duration(self, value): self.object.duration = value @duration.expression def duration(cls): from indico.modules.events.contributions import Contribution from indico.modules.events.sessions.models.blocks import SessionBlock from indico.modules.events.timetable.models.breaks import Break return db.case( { TimetableEntryType.SESSION_BLOCK.value: db.select([SessionBlock.duration]).where( SessionBlock.id == cls.session_block_id).correlate_except( SessionBlock).as_scalar(), TimetableEntryType.CONTRIBUTION.value: db.select([Contribution.duration]).where( Contribution.id == cls.contribution_id).correlate_except( Contribution).as_scalar(), TimetableEntryType.BREAK.value: db.select([Break.duration]).where(Break.id == cls.break_id). correlate_except(Break).as_scalar(), }, value=cls.type) @hybrid_property def end_dt(self): if self.start_dt is None or self.duration is None: return None return self.start_dt + self.duration @end_dt.expression def end_dt(cls): return cls.start_dt + cls.duration @property def session_siblings(self): if self.type == TimetableEntryType.SESSION_BLOCK: return [ x for x in self.siblings if x.session_block and x.session_block.session == self.session_block.session ] elif self.parent: return self.siblings else: return [] @property def siblings(self): from indico.modules.events.timetable.util import get_top_level_entries, get_nested_entries tzinfo = self.event_new.tzinfo day = self.start_dt.astimezone(tzinfo).date() siblings = (get_nested_entries(self.event_new)[self.parent_id] if self.parent_id else get_top_level_entries(self.event_new)) return [ x for x in siblings if x.start_dt.astimezone(tzinfo).date() == day and x.id != self.id ] @property def siblings_query(self): tzinfo = self.event_new.tzinfo day = self.start_dt.astimezone(tzinfo).date() criteria = (TimetableEntry.id != self.id, TimetableEntry.parent == self.parent, db.cast(TimetableEntry.start_dt.astimezone(tzinfo), db.Date) == day) return TimetableEntry.query.with_parent( self.event_new).filter(*criteria) @locator_property def locator(self): return dict(self.event_new.locator, entry_id=self.id) @return_ascii def __repr__(self): return format_repr(self, 'id', 'type', 'start_dt', 'end_dt', _repr=self.object) def can_view(self, user): """Checks whether the user will see this entry in the timetable.""" if self.type in (TimetableEntryType.CONTRIBUTION, TimetableEntryType.BREAK): return self.object.can_access(user) elif self.type == TimetableEntryType.SESSION_BLOCK: if self.object.can_access(user): return True return any( x.can_access(user) for x in self.object.contributions if not x.is_inheriting) def extend_start_dt(self, start_dt): assert start_dt < self.start_dt extension = self.start_dt - start_dt self.start_dt = start_dt self.duration = self.duration + extension def extend_end_dt(self, end_dt): diff = end_dt - self.end_dt if diff < timedelta(0): raise ValueError("New end_dt is before current end_dt.") self.duration += diff def extend_parent(self, by_start=True, by_end=True): """Extend start/end of parent objects if needed. No extension if performed for entries crossing a day boundary in the event timezone. :param by_start: Extend parent by start datetime. :param by_end: Extend parent by end datetime. """ tzinfo = self.event_new.tzinfo if self.start_dt.astimezone(tzinfo).date() != self.end_dt.astimezone( tzinfo).date(): return if self.parent is None: if by_start and self.start_dt < self.event_new.start_dt: self.event_new.start_dt = self.start_dt if by_end and self.end_dt > self.event_new.end_dt: self.event_new.end_dt = self.end_dt else: extended = False if by_start and self.start_dt < self.parent.start_dt: self.parent.extend_start_dt(self.start_dt) extended = True if by_end and self.end_dt > self.parent.end_dt: self.parent.extend_end_dt(self.end_dt) extended = True if extended: self.parent.extend_parent(by_start=by_start, by_end=by_end) def is_parallel(self, in_session=False): siblings = self.siblings if not in_session else self.session_siblings for sibling in siblings: if overlaps((self.start_dt, self.end_dt), (sibling.start_dt, sibling.end_dt)): return True return False def move(self, start_dt): """Move the entry to start at a different time. This method automatically moves children of the entry to preserve their start time relative to the parent's start time. """ if self.type == TimetableEntryType.SESSION_BLOCK: diff = start_dt - self.start_dt for child in self.children: child.start_dt += diff self.start_dt = start_dt def move_next_to(self, sibling, position='before'): if sibling not in self.siblings: raise ValueError("Not a sibling") if position not in ('before', 'after'): raise ValueError("Invalid position") if position == 'before': start_dt = sibling.start_dt - self.duration else: start_dt = sibling.end_dt self.move(start_dt)
class Agreement(db.Model): """Agreements between a person and Indico""" __tablename__ = 'agreements' __table_args__ = (db.UniqueConstraint('event_id', 'type', 'identifier'), {'schema': 'events'}) #: Entry ID id = db.Column( db.Integer, primary_key=True ) #: Entry universally unique ID uuid = db.Column( db.String, nullable=False ) #: ID of the event event_id = db.Column( db.Integer, db.ForeignKey('events.events.id'), nullable=False, index=True ) #: Type of agreement type = db.Column( db.String, nullable=False ) #: Unique identifier within the event and type identifier = db.Column( db.String, nullable=False ) #: Email of the person agreeing person_email = db.Column( db.String, nullable=True ) #: Full name of the person agreeing person_name = db.Column( db.String, nullable=False ) #: A :class:`AgreementState` state = db.Column( PyIntEnum(AgreementState), default=AgreementState.pending, nullable=False ) #: The date and time the agreement was created timestamp = db.Column( UTCDateTime, default=now_utc, nullable=False ) #: ID of a linked user user_id = db.Column( db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=True ) #: The date and time the agreement was signed signed_dt = db.Column( UTCDateTime ) #: The IP from which the agreement was signed signed_from_ip = db.Column( db.String ) #: Explanation as to why the agreement was accepted/rejected reason = db.Column( db.String ) #: Attachment attachment = db.deferred(db.Column( db.LargeBinary )) #: Filename and extension of the attachment attachment_filename = db.Column( db.String ) #: Definition-specific data of the agreement data = db.Column( JSON ) #: The user this agreement is linked to user = db.relationship( 'User', lazy=False, backref=db.backref( 'agreements', lazy='dynamic' ) ) #: The Event this agreement is associated with event_new = db.relationship( 'Event', lazy=True, backref=db.backref( 'agreements', lazy='dynamic' ) ) @hybrid_property def accepted(self): return self.state in {AgreementState.accepted, AgreementState.accepted_on_behalf} @accepted.expression def accepted(self): return self.state.in_((AgreementState.accepted, AgreementState.accepted_on_behalf)) @hybrid_property def pending(self): return self.state == AgreementState.pending @hybrid_property def rejected(self): return self.state in {AgreementState.rejected, AgreementState.rejected_on_behalf} @rejected.expression def rejected(self): return self.state.in_((AgreementState.rejected, AgreementState.rejected_on_behalf)) @hybrid_property def signed_on_behalf(self): return self.state in {AgreementState.accepted_on_behalf, AgreementState.rejected_on_behalf} @signed_on_behalf.expression def signed_on_behalf(self): return self.state.in_((AgreementState.accepted_on_behalf, AgreementState.rejected_on_behalf)) @property def definition(self): from indico.modules.events.agreements.util import get_agreement_definitions return get_agreement_definitions().get(self.type) @property def event(self): from MaKaC.conference import ConferenceHolder return ConferenceHolder().getById(str(self.event_id)) @event.setter def event(self, event): self.event_id = int(event.getId()) @property def locator(self): return {'confId': self.event_id, 'id': self.id} @return_ascii def __repr__(self): state = self.state.name if self.state is not None else None return '<Agreement({}, {}, {}, {}, {}, {})>'.format(self.id, self.event_id, self.type, self.identifier, self.person_email, state) @staticmethod def create_from_data(event, type_, person): agreement = Agreement(event_new=event, type=type_, state=AgreementState.pending, uuid=str(uuid4())) agreement.identifier = person.identifier agreement.person_email = person.email agreement.person_name = person.name if person.user: agreement.user = person.user agreement.data = person.data return agreement def accept(self, from_ip, reason=None, on_behalf=False): self.state = AgreementState.accepted if not on_behalf else AgreementState.accepted_on_behalf self.signed_from_ip = from_ip self.reason = reason self.signed_dt = now_utc() self.definition.handle_accepted(self) def reject(self, from_ip, reason=None, on_behalf=False): self.state = AgreementState.rejected if not on_behalf else AgreementState.rejected_on_behalf self.signed_from_ip = from_ip self.reason = reason self.signed_dt = now_utc() self.definition.handle_rejected(self) def reset(self): self.definition.handle_reset(self) self.state = AgreementState.pending self.attachment = None self.attachment_filename = None self.reason = None self.signed_dt = None self.signed_from_ip = None def render(self, form, **kwargs): definition = self.definition if definition is None: raise IndicoError(_('This agreement type is currently not available.')) return definition.render_form(self, form, **kwargs) def belongs_to(self, person): return self.identifier == person.identifier def is_orphan(self): return self.definition.is_agreement_orphan(self.event_new, self)
class EventNote(LinkMixin, db.Model): __tablename__ = 'notes' allowed_link_types = LinkMixin.allowed_link_types - {LinkType.category, LinkType.session_block} unique_links = True events_backref_name = 'all_notes' link_backref_name = 'note' @strict_classproperty @classmethod def __auto_table_args(cls): return (make_fts_index(cls, 'html'), {'schema': 'events'}) @declared_attr def __table_args__(cls): return auto_table_args(cls) #: The ID of the note id = db.Column( db.Integer, primary_key=True ) #: If the note has been deleted is_deleted = db.Column( db.Boolean, nullable=False, default=False ) #: The rendered HTML of the note html = db.Column( db.Text, nullable=False ) #: The ID of the current revision current_revision_id = db.Column( db.Integer, db.ForeignKey('events.note_revisions.id', use_alter=True), nullable=True # needed for post_update :( ) #: The list of all revisions for the note revisions = db.relationship( 'EventNoteRevision', primaryjoin=lambda: EventNote.id == EventNoteRevision.note_id, foreign_keys=lambda: EventNoteRevision.note_id, lazy=True, cascade='all, delete-orphan', order_by=lambda: EventNoteRevision.created_dt.desc(), backref=db.backref( 'note', lazy=False ) ) #: The currently active revision of the note current_revision = db.relationship( 'EventNoteRevision', primaryjoin=lambda: EventNote.current_revision_id == EventNoteRevision.id, foreign_keys=current_revision_id, lazy=True, post_update=True ) @locator_property def locator(self): return self.object.locator @classmethod def get_for_linked_object(cls, linked_object, preload_event=True): """Get the note for the given object. This only returns a note that hasn't been deleted. :param linked_object: An event, session, contribution or subcontribution. :param preload_event: If all notes for the same event should be pre-loaded and cached in the app context. """ event = linked_object.event try: return g.event_notes[event].get(linked_object) except (AttributeError, KeyError): if not preload_event: return linked_object.note if linked_object.note and not linked_object.note.is_deleted else None if 'event_notes' not in g: g.event_notes = {} query = (event.all_notes .filter_by(is_deleted=False) .options(joinedload(EventNote.linked_event), joinedload(EventNote.session), joinedload(EventNote.contribution), joinedload(EventNote.subcontribution))) g.event_notes[event] = {n.object: n for n in query} return g.event_notes[event].get(linked_object) @classmethod def get_or_create(cls, linked_object): """Get the note for the given object or creates a new one. If there is an existing note for the object, it will be returned even. Otherwise a new note is created. """ note = cls.query.filter_by(object=linked_object).first() if note is None: note = cls(object=linked_object) return note def delete(self, user): """Mark the note as deleted and adds a new empty revision.""" self.create_revision(self.current_revision.render_mode, '', user) self.is_deleted = True def create_revision(self, render_mode, source, user): """Create a new revision if needed and marks it as undeleted if it was. Any change to the render mode or the source causes a new revision to be created. The user is not taken into account since a user "modifying" a note without changing things is not really a change. """ self.is_deleted = False with db.session.no_autoflush: current = self.current_revision if current is not None and current.render_mode == render_mode and current.source == source: return current self.current_revision = EventNoteRevision(render_mode=render_mode, source=source, user=user) return self.current_revision @classmethod def html_matches(cls, search_string, exact=False): """Check whether the html content matches a search string. To be used in a SQLAlchemy `filter` call. :param search_string: A string to search for :param exact: Whether to search for the exact string """ return fts_matches(cls.html, search_string, exact=exact) def __repr__(self): return '<EventNote({}, current_revision={}{}, {})>'.format( self.id, self.current_revision_id, ', is_deleted=True' if self.is_deleted else '', self.link_repr )
class SubContribution(DescriptionMixin, AttachedItemsMixin, AttachedNotesMixin, db.Model): __tablename__ = 'subcontributions' __table_args__ = (db.Index(None, 'friendly_id', 'contribution_id', unique=True), {'schema': 'events'}) PRELOAD_EVENT_ATTACHED_ITEMS = True PRELOAD_EVENT_NOTES = True ATTACHMENT_FOLDER_ID_COLUMN = 'subcontribution_id' possible_render_modes = {RenderMode.html, RenderMode.markdown} default_render_mode = RenderMode.markdown id = db.Column( db.Integer, primary_key=True ) #: The human-friendly ID for the sub-contribution friendly_id = db.Column( db.Integer, nullable=False, default=_get_next_friendly_id ) contribution_id = db.Column( db.Integer, db.ForeignKey('events.contributions.id'), index=True, nullable=False ) position = db.Column( db.Integer, nullable=False, default=_get_next_position ) title = db.Column( db.String, nullable=False ) code = db.Column( db.String, nullable=False, default='' ) duration = db.Column( db.Interval, nullable=False ) is_deleted = db.Column( db.Boolean, nullable=False, default=False ) #: External references associated with this contribution references = db.relationship( 'SubContributionReference', lazy=True, cascade='all, delete-orphan', backref=db.backref( 'subcontribution', lazy=True ) ) #: Persons associated with this contribution person_links = db.relationship( 'SubContributionPersonLink', lazy=True, cascade='all, delete-orphan', backref=db.backref( 'subcontribution', lazy=True ) ) # relationship backrefs: # - attachment_folders (AttachmentFolder.subcontribution) # - contribution (Contribution.subcontributions) # - legacy_mapping (LegacySubContributionMapping.subcontribution) # - note (EventNote.subcontribution) def __init__(self, **kwargs): # explicitly initialize this relationship with None to avoid # an extra query to check whether there is an object associated # when assigning a new one (e.g. during cloning) kwargs.setdefault('note', None) super().__init__(**kwargs) @property def event(self): return self.contribution.event @locator_property def locator(self): return dict(self.contribution.locator, subcontrib_id=self.id) @property def is_protected(self): return self.contribution.is_protected @property def session(self): """Convenience property so all event entities have it.""" return self.contribution.session if self.contribution.session_id is not None else None @property def timetable_entry(self): """Convenience property so all event entities have it.""" return self.contribution.timetable_entry @property def speakers(self): return self.person_links @speakers.setter def speakers(self, value): self.person_links = list(value.keys()) @property def slug(self): return slugify('sc', self.contribution.friendly_id, self.friendly_id, self.title, maxlen=30) @property def location_parent(self): return self.contribution def get_access_list(self): return self.contribution.get_access_list() def get_manager_list(self, recursive=False, include_groups=True): return self.contribution.get_manager_list(recursive=recursive, include_groups=include_groups) def __repr__(self): return format_repr(self, 'id', is_deleted=False, _text=self.title) def can_access(self, user, **kwargs): return self.contribution.can_access(user, **kwargs) def can_manage(self, user, permission=None, **kwargs): return self.contribution.can_manage(user, permission=permission, **kwargs)
class StaticListLink(db.Model): """Display configuration data used in static links to listing pages. This allows users to share links to listing pages in events while preserving e.g. column/filter configurations. """ __tablename__ = 'static_list_links' __table_args__ = {'schema': 'events'} id = db.Column(db.Integer, primary_key=True) event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False) type = db.Column(db.String, nullable=False) uuid = db.Column(pg_UUID, index=True, unique=True, nullable=False, default=lambda: unicode(uuid4())) created_dt = db.Column(UTCDateTime, nullable=False, default=now_utc) last_used_dt = db.Column(UTCDateTime, nullable=True) data = db.Column(JSONB, nullable=False) event_new = db.relationship('Event', lazy=True, backref=db.backref( 'static_list_links', cascade='all, delete-orphan', lazy='dynamic')) @classmethod def load(cls, event, type_, uuid): """Load the data associated with a link :param event: the `Event` the link belongs to :param type_: the type of the link :param uuid: the UUID of the link :return: the link data or ``None`` if the link does not exist """ try: UUID(uuid) except ValueError: return None static_list_link = event.static_list_links.filter_by( type=type_, uuid=uuid).first() if static_list_link is None: return None static_list_link.last_used_dt = now_utc() return static_list_link.data @classmethod def create(cls, event, type_, data): """Create a new static list link. If one exists with the same data, that link is used instead of creating a new one. :param event: the `Event` for which to create the link :param type_: the type of the link :param data: the data to associate with the link :return: the newly created `StaticListLink` """ static_list_link = event.static_list_links.filter_by( type=type_, data=data).first() if static_list_link is None: static_list_link = cls(event_new=event, type=type_, data=data) else: # bump timestamp in case we start expiring old links # in the future if static_list_link.last_used_dt is not None: static_list_link.last_used_dt = now_utc() else: static_list_link.created_dt = now_utc() db.session.flush() return static_list_link @return_ascii def __repr__(self): return format_repr(self, 'id', 'uuid')
class Editable(db.Model): __tablename__ = 'editables' __table_args__ = (db.UniqueConstraint('contribution_id', 'type'), { 'schema': 'event_editing' }) id = db.Column(db.Integer, primary_key=True) contribution_id = db.Column(db.ForeignKey('events.contributions.id'), index=True, nullable=False) type = db.Column(PyIntEnum(EditableType), nullable=False) editor_id = db.Column(db.ForeignKey('users.users.id'), index=True, nullable=True) published_revision_id = db.Column( db.ForeignKey('event_editing.revisions.id'), index=True, nullable=True) contribution = db.relationship('Contribution', lazy=True, backref=db.backref( 'editables', lazy=True, )) editor = db.relationship('User', lazy=True, backref=db.backref('editor_for_editables', lazy='dynamic')) published_revision = db.relationship( 'EditingRevision', foreign_keys=published_revision_id, lazy=True, ) # relationship backrefs: # - revisions (EditingRevision.editable) def __repr__(self): return format_repr(self, 'id', 'contribution_id', 'type') @locator_property def locator(self): return dict(self.contribution.locator, type=self.type.name) @property def event(self): return self.contribution.event def _has_general_editor_permissions(self, user): """Whether the user has general editor permissions on the Editable. This means that the user has editor permissions for the editable's type, but does not need to be the assigned editor. """ # Editing (and event) managers always have editor-like access return (self.event.can_manage(user, permission='editing_manager') or self.event.can_manage( user, permission=self.type.editor_permission)) def can_see_timeline(self, user): """Whether the user can see the editable's timeline. This is pure read access, without any ability to make changes or leave comments. """ # Anyone with editor access to the editable's type can see the timeline. # Users associated with the editable's contribution can do so as well. return (self._has_general_editor_permissions(user) or self.contribution.can_submit_proceedings(user) or self.contribution.is_user_associated(user, check_abstract=True)) def can_perform_submitter_actions(self, user): """Whether the user can perform any submitter actions. These are actions such as uploading a new revision after having been asked to make changes or approving/rejecting changes made by an editor. """ # If the user can't even see the timeline, we never allow any modifications if not self.can_see_timeline(user): return False # Anyone who can submit new proceedings can also perform submitter actions, # i.e. the abstract submitter and anyone with submission access to the contribution. return self.contribution.can_submit_proceedings(user) def can_perform_editor_actions(self, user): """Whether the user can perform any Editing actions. These are actions usually made by the assigned Editor of the editable, such as making changes, asking the user to make changes, or approving/rejecting the editable. """ from indico.modules.events.editing.settings import editable_type_settings # If the user can't even see the timeline, we never allow any modifications if not self.can_see_timeline(user): return False # Editing/event managers can perform actions when they are the assigned editor # even when editing is disabled in the settings if self.editor == user and self.event.can_manage( user, permission='editing_manager'): return True # Editing needs to be enabled in the settings otherwise if not editable_type_settings[self.type].get(self.event, 'editing_enabled'): return False # Editors need the permission on the editable type and also be the assigned editor if self.editor == user and self.event.can_manage( user, permission=self.type.editor_permission): return True return False def can_use_internal_comments(self, user): """Whether the user can create/see internal comments.""" return self._has_general_editor_permissions(user) def can_see_editor_names(self, user, actor=None): """Whether the user can see the names of editing team members. This is always true if team anonymity is not enabled; otherwise only users who are member of the editing team will see names. If an `actor` is set, the check applies to whether the name of this particular user can be seen. """ from indico.modules.events.editing.settings import editable_type_settings return (not editable_type_settings[self.type].get( self.event, 'anonymous_team') or (actor and not self.can_see_editor_names(actor)) or self._has_general_editor_permissions(user)) def can_comment(self, user): """Whether the user can comment on the editable.""" # We allow any user associated with the contribution to comment, even if they are # not authorized to actually perform submitter actions. return ( self.event.can_manage(user, permission=self.type.editor_permission) or self.event.can_manage(user, permission='editing_manager') or self.contribution.is_user_associated(user, check_abstract=True)) def can_assign_self(self, user): """Whether the user can assign themselves on the editable.""" from indico.modules.events.editing.settings import editable_type_settings type_settings = editable_type_settings[self.type] if self.editor and (self.editor == user or not self.can_unassign(user)): return False return ((self.event.can_manage(user, permission=self.type.editor_permission) and type_settings.get(self.event, 'editing_enabled') and type_settings.get(self.event, 'self_assign_allowed')) or self.event.can_manage(user, permission='editing_manager')) def can_unassign(self, user): """Whether the user can unassign the editor of the editable.""" from indico.modules.events.editing.settings import editable_type_settings type_settings = editable_type_settings[self.type] return (self.event.can_manage(user, permission='editing_manager') or (self.editor == user and self.event.can_manage( user, permission=self.type.editor_permission) and type_settings.get(self.event, 'editing_enabled') and type_settings.get(self.event, 'self_assign_allowed'))) @property def review_conditions_valid(self): from indico.modules.events.editing.models.review_conditions import EditingReviewCondition query = EditingReviewCondition.query.with_parent( self.event).filter_by(type=self.type) review_conditions = [{ft.id for ft in cond.file_types} for cond in query] file_types = {file.file_type_id for file in self.revisions[-1].files} if not review_conditions: return True return any(file_types >= cond for cond in review_conditions) @property def editing_enabled(self): from indico.modules.events.editing.settings import editable_type_settings return editable_type_settings[self.type].get(self.event, 'editing_enabled') @property def external_timeline_url(self): return url_for('event_editing.editable', self, _external=True) @property def timeline_url(self): return url_for('event_editing.editable', self) def log(self, *args, **kwargs): """Log with prefilled metadata for the editable.""" self.event.log(*args, meta={'editable_id': self.id}, **kwargs)
# This file is part of Indico. # Copyright (C) 2002 - 2020 CERN # # Indico is free software; you can redistribute it and/or # modify it under the terms of the MIT License; see the # LICENSE file for more details. from __future__ import unicode_literals from indico.core.db import db favorite_user_table = db.Table('favorite_users', db.metadata, db.Column('user_id', db.Integer, db.ForeignKey('users.users.id'), primary_key=True, nullable=False, index=True), db.Column('target_id', db.Integer, db.ForeignKey('users.users.id'), primary_key=True, nullable=False, index=True), schema='users') favorite_category_table = db.Table( 'favorite_categories', db.metadata, db.Column('user_id',
class Category(SearchableTitleMixin, DescriptionMixin, ProtectionManagersMixin, AttachedItemsMixin, db.Model): """An Indico category.""" __tablename__ = 'categories' disallowed_protection_modes = frozenset() inheriting_have_acl = True possible_render_modes = {RenderMode.markdown} default_render_mode = RenderMode.markdown allow_no_access_contact = True ATTACHMENT_FOLDER_ID_COLUMN = 'category_id' @strict_classproperty @classmethod def __auto_table_args(cls): return ( db.CheckConstraint( "(icon IS NULL) = (icon_metadata::text = 'null')", 'valid_icon'), db.CheckConstraint( "(logo IS NULL) = (logo_metadata::text = 'null')", 'valid_logo'), db.CheckConstraint("(parent_id IS NULL) = (id = 0)", 'valid_parent'), db.CheckConstraint("(id != 0) OR NOT is_deleted", 'root_not_deleted'), db.CheckConstraint( f"(id != 0) OR (protection_mode != {ProtectionMode.inheriting})", 'root_not_inheriting'), db.CheckConstraint('visibility IS NULL OR visibility > 0', 'valid_visibility'), { 'schema': 'categories' }) @declared_attr def __table_args__(cls): return auto_table_args(cls) id = db.Column(db.Integer, primary_key=True) parent_id = db.Column(db.Integer, db.ForeignKey('categories.categories.id'), index=True, nullable=True) is_deleted = db.Column(db.Boolean, nullable=False, default=False) position = db.Column(db.Integer, nullable=False, default=_get_next_position) visibility = db.Column(db.Integer, nullable=True, default=None) icon_metadata = db.Column(JSONB, nullable=False, default=lambda: None) icon = db.deferred(db.Column(db.LargeBinary, nullable=True)) logo_metadata = db.Column(JSONB, nullable=False, default=lambda: None) logo = db.deferred(db.Column(db.LargeBinary, nullable=True)) timezone = db.Column(db.String, nullable=False, default=lambda: config.DEFAULT_TIMEZONE) default_event_themes = db.Column(JSONB, nullable=False, default=_get_default_event_themes) event_creation_restricted = db.Column(db.Boolean, nullable=False, default=True) event_creation_notification_emails = db.Column(ARRAY(db.String), nullable=False, default=[]) event_message_mode = db.Column(PyIntEnum(EventMessageMode), nullable=False, default=EventMessageMode.disabled) _event_message = db.Column('event_message', db.Text, nullable=False, default='') suggestions_disabled = db.Column(db.Boolean, nullable=False, default=False) notify_managers = db.Column(db.Boolean, nullable=False, default=False) default_ticket_template_id = db.Column( db.ForeignKey('indico.designer_templates.id'), nullable=True, index=True) default_badge_template_id = db.Column( db.ForeignKey('indico.designer_templates.id'), nullable=True, index=True) children = db.relationship( 'Category', order_by='Category.position', primaryjoin=(id == db.remote(parent_id)) & ~db.remote(is_deleted), lazy=True, backref=db.backref('parent', primaryjoin=(db.remote(id) == parent_id), lazy=True)) acl_entries = db.relationship('CategoryPrincipal', backref='category', cascade='all, delete-orphan', collection_class=set) default_ticket_template = db.relationship( 'DesignerTemplate', lazy=True, foreign_keys=default_ticket_template_id, backref='default_ticket_template_of') default_badge_template = db.relationship( 'DesignerTemplate', lazy=True, foreign_keys=default_badge_template_id, backref='default_badge_template_of') # column properties: # - deep_events_count # relationship backrefs: # - attachment_folders (AttachmentFolder.category) # - designer_templates (DesignerTemplate.category) # - events (Event.category) # - favorite_of (User.favorite_categories) # - legacy_mapping (LegacyCategoryMapping.category) # - parent (Category.children) # - roles (CategoryRole.category) # - settings (CategorySetting.category) # - suggestions (SuggestedCategory.category) @hybrid_property def event_message(self): return MarkdownText(self._event_message) @event_message.setter def event_message(self, value): self._event_message = value @event_message.expression def event_message(cls): return cls._event_message def __repr__(self): return format_repr(self, 'id', is_deleted=False, _text=text_to_repr(self.title, max_length=75)) @property def protection_parent(self): return self.parent if not self.is_root else None @locator_property def locator(self): return {'category_id': self.id} @classmethod def get_root(cls): """Get the root category.""" return cls.query.filter(cls.is_root).one() @property def url(self): return url_for('categories.display', self) @hybrid_property def is_root(self): return self.parent_id is None @is_root.expression def is_root(cls): return cls.parent_id.is_(None) @property def is_empty(self): return not self.deep_children_count and not self.deep_events_count @property def has_icon(self): return self.icon_metadata is not None @property def has_effective_icon(self): return self.effective_icon_data['metadata'] is not None @property def has_logo(self): return self.logo_metadata is not None @property def tzinfo(self): return pytz.timezone(self.timezone) @property def display_tzinfo(self): """The tzinfo of the category or the one specified by the user.""" return get_display_tz(self, as_timezone=True) def can_create_events(self, user): """Check whether the user can create events in the category.""" # if creation is not restricted anyone who can access the category # can also create events in it, otherwise only people with the # creation role can return user and ( (not self.event_creation_restricted and self.can_access(user)) or self.can_manage(user, permission='create')) def move(self, target): """Move the category into another category.""" assert not self.is_root old_parent = self.parent self.position = (max(x.position for x in target.children) + 1) if target.children else 1 self.parent = target db.session.flush() signals.category.moved.send(self, old_parent=old_parent) @classmethod def get_tree_cte(cls, col='id'): """Create a CTE for the category tree. The CTE contains the following columns: - ``id`` -- the category id - ``path`` -- an array containing the path from the root to the category itself - ``is_deleted`` -- whether the category is deleted :param col: The name of the column to use in the path or a callable receiving the category alias that must return the expression used for the 'path' retrieved by the CTE. """ cat_alias = db.aliased(cls) if callable(col): path_column = col(cat_alias) else: path_column = getattr(cat_alias, col) cte_query = (select([ cat_alias.id, array([path_column]).label('path'), cat_alias.is_deleted ]).where(cat_alias.parent_id.is_(None)).cte(recursive=True)) rec_query = (select([ cat_alias.id, cte_query.c.path.op('||')(path_column), cte_query.c.is_deleted | cat_alias.is_deleted ]).where(cat_alias.parent_id == cte_query.c.id)) return cte_query.union_all(rec_query) @classmethod def get_protection_cte(cls): cat_alias = db.aliased(cls) cte_query = (select([cat_alias.id, cat_alias.protection_mode]).where( cat_alias.parent_id.is_(None)).cte(recursive=True)) rec_query = (select([ cat_alias.id, db.case( {ProtectionMode.inheriting.value: cte_query.c.protection_mode}, else_=cat_alias.protection_mode, value=cat_alias.protection_mode) ]).where(cat_alias.parent_id == cte_query.c.id)) return cte_query.union_all(rec_query) def get_protection_parent_cte(self): cte_query = (select([ Category.id, db.cast(literal(None), db.Integer).label('protection_parent') ]).where(Category.id == self.id).cte(recursive=True)) rec_query = (select([ Category.id, db.case( { ProtectionMode.inheriting.value: func.coalesce(cte_query.c.protection_parent, self.id) }, else_=Category.id, value=Category.protection_mode) ]).where(Category.parent_id == cte_query.c.id)) return cte_query.union_all(rec_query) @classmethod def get_icon_data_cte(cls): cat_alias = db.aliased(cls) cte_query = (select([ cat_alias.id, cat_alias.id.label('source_id'), cat_alias.icon_metadata ]).where(cat_alias.parent_id.is_(None)).cte(recursive=True)) rec_query = (select([ cat_alias.id, db.case({'null': cte_query.c.source_id}, else_=cat_alias.id, value=db.func.jsonb_typeof(cat_alias.icon_metadata)), db.case({'null': cte_query.c.icon_metadata}, else_=cat_alias.icon_metadata, value=db.func.jsonb_typeof(cat_alias.icon_metadata)) ]).where(cat_alias.parent_id == cte_query.c.id)) return cte_query.union_all(rec_query) @property def deep_children_query(self): """Get a query object for all subcategories. This includes subcategories at any level of nesting. """ cte = Category.get_tree_cte() return (Category.query.join(cte, Category.id == cte.c.id).filter( cte.c.path.contains([self.id]), cte.c.id != self.id, ~cte.c.is_deleted)) @staticmethod def _get_chain_query(start_criterion): cte_query = (select([ Category.id, Category.parent_id, literal(0).label('level') ]).where(start_criterion).cte('category_chain', recursive=True)) parent_query = (select([ Category.id, Category.parent_id, cte_query.c.level + 1 ]).where(Category.id == cte_query.c.parent_id)) cte_query = cte_query.union_all(parent_query) return Category.query.join(cte_query, Category.id == cte_query.c.id).order_by( cte_query.c.level.desc()) @property def chain_query(self): """Get a query object for the category chain. The query retrieves the root category first and then all the intermediate categories up to (and including) this category. """ return self._get_chain_query(Category.id == self.id) @property def parent_chain_query(self): """Get a query object for the category's parent chain. The query retrieves the root category first and then all the intermediate categories up to (excluding) this category. """ return self._get_chain_query(Category.id == self.parent_id) def nth_parent(self, n_categs, fail_on_overflow=True): """Return the nth parent of the category. :param n_categs: the number of categories to go up :param fail_on_overflow: whether to fail if we try to go above the root category :return: `Category` object or None (only if ``fail_on_overflow`` is not set) """ if n_categs == 0: return self chain = self.parent_chain_query.all() assert n_categs >= 0 if n_categs > len(chain): if fail_on_overflow: raise IndexError("Root category has no parent!") else: return None return chain[::-1][n_categs - 1] def is_descendant_of(self, categ): return categ != self and self.parent_chain_query.filter( Category.id == categ.id).has_rows() @property def visibility_horizon_query(self): """Get a query object that returns the highest category this one is visible from.""" cte_query = (select([ Category.id, Category.parent_id, db.case([(Category.visibility.is_(None), None)], else_=(Category.visibility - 1)).label('n'), literal(0).label('level') ]).where(Category.id == self.id).cte('visibility_horizon', recursive=True)) parent_query = (select([ Category.id, Category.parent_id, db.case([ (Category.visibility.is_(None) & cte_query.c.n.is_(None), None) ], else_=db.func.least(Category.visibility, cte_query.c.n) - 1), cte_query.c.level + 1 ]).where( db.and_(Category.id == cte_query.c.parent_id, (cte_query.c.n > 0) | cte_query.c.n.is_(None)))) cte_query = cte_query.union_all(parent_query) return db.session.query(cte_query.c.id, cte_query.c.n).order_by( cte_query.c.level.desc()).limit(1) @property def own_visibility_horizon(self): """ Get the highest category this one would like to be visible from (configured visibility). """ if self.visibility is None: return Category.get_root() else: return self.nth_parent(self.visibility - 1) @property def real_visibility_horizon(self): """ Get the highest category this one is actually visible from (as limited by categories above). """ horizon_id, final_visibility = self.visibility_horizon_query.one() if final_visibility is not None and final_visibility < 0: return None # Category is invisible return Category.get(horizon_id) @staticmethod def get_visible_categories_cte(category_id): """ Get a sqlalchemy select for the visible categories within the given category, including the category itself. """ cte_query = (select([ Category.id, literal(0).label('level') ]).where((Category.id == category_id) & (Category.visibility.is_(None) | (Category.visibility > 0))).cte(recursive=True)) parent_query = (select([Category.id, cte_query.c.level + 1]).where( db.and_( Category.parent_id == cte_query.c.id, db.or_(Category.visibility.is_(None), Category.visibility > cte_query.c.level + 1)))) return cte_query.union_all(parent_query) @property def visible_categories_query(self): """ Get a query object for the visible categories within this category, including the category itself. """ cte_query = Category.get_visible_categories_cte(self.id) return Category.query.join(cte_query, Category.id == cte_query.c.id) def get_hidden_events(self, user=None): """Get all hidden events within the given category and user.""" from indico.modules.events import Event hidden_events = Event.query.with_parent(self).filter_by( visibility=0).all() return [ event for event in hidden_events if not event.can_display(user) ] @property def icon_url(self): """Get the HTTP URL of the icon.""" return url_for('categories.display_icon', self, slug=self.icon_metadata['hash']) @property def effective_icon_url(self): """Get the HTTP URL of the icon (possibly inherited).""" data = self.effective_icon_data return url_for('categories.display_icon', category_id=data['source_id'], slug=data['metadata']['hash']) @property def logo_url(self): """Get the HTTP URL of the logo.""" return url_for('categories.display_logo', self, slug=self.logo_metadata['hash'])
class PaymentTransaction(db.Model): """Payment transactions""" __tablename__ = 'payment_transactions' __table_args__ = (db.CheckConstraint('amount > 0', 'positive_amount'), { 'schema': 'events' }) #: Entry ID id = db.Column(db.Integer, primary_key=True) #: ID of the associated registration registration_id = db.Column( db.Integer, db.ForeignKey('event_registration.registrations.id'), index=True, nullable=False) #: a :class:`TransactionStatus` status = db.Column(PyIntEnum(TransactionStatus), nullable=False) #: the base amount the user needs to pay (without payment-specific fees) amount = db.Column( db.Numeric(8, 2), # max. 999999.99 nullable=False) #: the currency of the payment (ISO string, e.g. EUR or USD) currency = db.Column(db.String, nullable=False) #: the provider of the payment (e.g. manual, PayPal etc.) provider = db.Column(db.String, nullable=False, default='_manual') #: the date and time the transaction was recorded timestamp = db.Column(UTCDateTime, default=now_utc, nullable=False) #: plugin-specific data of the payment data = db.Column(JSON, nullable=False) #: The associated registration registration = db.relationship('Registration', lazy=True, foreign_keys=[registration_id], backref=db.backref( 'transactions', cascade='all, delete-orphan', lazy=True)) @property def plugin(self): from indico.modules.events.payment.util import get_payment_plugins return get_payment_plugins().get(self.provider) @property def is_manual(self): return self.provider == '_manual' @return_ascii def __repr__(self): # in case of a new object we might not have the default status set status = TransactionStatus( self.status).name if self.status is not None else None return format_repr(self, 'id', 'registration_id', 'provider', 'amount', 'currency', 'timestamp', status=status) def render_details(self): """Renders the transaction details""" if self.is_manual: return render_template( 'events/payment/transaction_details_manual.html', transaction=self) plugin = self.plugin if plugin is None: return '[plugin not loaded: {}]'.format(self.provider) with plugin.plugin_context(): return plugin.render_transaction_details(self) @classmethod def create_next(cls, registration, amount, currency, action, provider=None, data=None): previous_transaction = registration.transaction new_transaction = PaymentTransaction(amount=amount, currency=currency, provider=provider, data=data) registration.transaction = new_transaction double_payment = False try: next_status = TransactionStatusTransition.next( previous_transaction, action, provider) except InvalidTransactionStatus as e: Logger.get('payment').exception("{}\nData received: {}".format( e, data)) return None, None except InvalidManualTransactionAction as e: Logger.get('payment').exception( "Invalid manual action code '{}' on initial status\n" "Data received: {}".format(e, data)) return None, None except InvalidTransactionAction as e: Logger.get('payment').exception( "Invalid action code '{}' on initial status\n" "Data received: {}".format(e, data)) return None, None except IgnoredTransactionAction as e: Logger.get('payment').warning("{}\nData received: {}".format( e, data)) return None, None except DoublePaymentTransaction: next_status = TransactionStatus.successful double_payment = True Logger.get('payment').warning( "Received successful payment for an already paid registration") new_transaction.status = next_status return new_transaction, double_payment
class EditingRevisionComment(RenderModeMixin, db.Model): __tablename__ = 'comments' __table_args__ = (db.CheckConstraint('(user_id IS NULL) = system', name='system_comment_no_user'), { 'schema': 'event_editing' }) possible_render_modes = {RenderMode.markdown} default_render_mode = RenderMode.markdown id = db.Column(db.Integer, primary_key=True) revision_id = db.Column(db.ForeignKey('event_editing.revisions.id', ondelete='CASCADE'), index=True, nullable=False) user_id = db.Column(db.ForeignKey('users.users.id'), index=True, nullable=True) created_dt = db.Column(UTCDateTime, nullable=False, default=now_utc) modified_dt = db.Column( UTCDateTime, nullable=True, ) is_deleted = db.Column(db.Boolean, nullable=False, default=False) #: Whether the comment is only visible to editors internal = db.Column(db.Boolean, nullable=False, default=False) #: Whether the comment is system-generated and cannot be deleted/modified. system = db.Column(db.Boolean, nullable=False, default=False) _text = db.Column('text', db.Text, nullable=False, default='') text = RenderModeMixin.create_hybrid_property('_text') user = db.relationship('User', lazy=True, backref=db.backref('editing_comments', lazy='dynamic')) revision = db.relationship( 'EditingRevision', lazy=True, backref=db.backref( 'comments', primaryjoin=( '(EditingRevisionComment.revision_id == EditingRevision.id) & ' '~EditingRevisionComment.is_deleted'), order_by=created_dt, cascade='all, delete-orphan', passive_deletes=True, lazy=True, )) @return_ascii def __repr__(self): return format_repr(self, 'id', 'revision_id', 'user_id', internal=False, _text=text_to_repr(self.text)) @locator_property def locator(self): return dict(self.revision.locator, comment_id=self.id) def can_modify(self, user): contribution = self.revision.editable.contribution authorized_submitter = contribution.is_user_associated( user, check_abstract=True) authorized_editor = contribution.event.can_manage( user, permission='paper_editing') if self.user != user: return False elif self.system: return False elif self.internal and not authorized_editor: return False return authorized_editor or authorized_submitter
class User(PersonMixin, db.Model): """Indico users""" # Useful when dealing with both users and groups in the same code is_group = False is_single_person = True is_event_role = False is_network = False principal_order = 0 principal_type = PrincipalType.user __tablename__ = 'users' __table_args__ = (db.Index(None, 'is_system', unique=True, postgresql_where=db.text('is_system')), db.CheckConstraint('NOT is_system OR (NOT is_blocked AND NOT is_pending AND NOT is_deleted)', 'valid_system_user'), db.CheckConstraint('id != merged_into_id', 'not_merged_self'), db.CheckConstraint("is_pending OR (first_name != '' AND last_name != '')", 'not_pending_proper_names'), {'schema': 'users'}) #: the unique id of the user id = db.Column( db.Integer, primary_key=True ) #: the first name of the user first_name = db.Column( db.String, nullable=False, index=True ) #: the last/family name of the user last_name = db.Column( db.String, nullable=False, index=True ) # the title of the user - you usually want the `title` property! _title = db.Column( 'title', PyIntEnum(UserTitle), nullable=False, default=UserTitle.none ) #: the phone number of the user phone = db.Column( db.String, nullable=False, default='' ) #: the address of the user address = db.Column( db.Text, nullable=False, default='' ) #: the id of the user this user has been merged into merged_into_id = db.Column( db.Integer, db.ForeignKey('users.users.id'), nullable=True ) #: if the user is the default system user is_system = db.Column( db.Boolean, nullable=False, default=False ) #: if the user is an administrator with unrestricted access to everything is_admin = db.Column( db.Boolean, nullable=False, default=False, index=True ) #: if the user has been blocked is_blocked = db.Column( db.Boolean, nullable=False, default=False ) #: if the user is pending (e.g. never logged in, only added to some list) is_pending = db.Column( db.Boolean, nullable=False, default=False ) #: if the user is deleted (e.g. due to a merge) is_deleted = db.Column( 'is_deleted', db.Boolean, nullable=False, default=False ) #: a unique secret used to generate signed URLs signing_secret = db.Column( UUID, nullable=False, default=lambda: unicode(uuid4()) ) _affiliation = db.relationship( 'UserAffiliation', lazy=False, uselist=False, cascade='all, delete-orphan', backref=db.backref('user', lazy=True) ) _primary_email = db.relationship( 'UserEmail', lazy=False, uselist=False, cascade='all, delete-orphan', primaryjoin='(User.id == UserEmail.user_id) & UserEmail.is_primary' ) _secondary_emails = db.relationship( 'UserEmail', lazy=True, cascade='all, delete-orphan', collection_class=set, primaryjoin='(User.id == UserEmail.user_id) & ~UserEmail.is_primary' ) _all_emails = db.relationship( 'UserEmail', lazy=True, viewonly=True, primaryjoin='User.id == UserEmail.user_id', collection_class=set, backref=db.backref('user', lazy=False) ) #: the affiliation of the user affiliation = association_proxy('_affiliation', 'name', creator=lambda v: UserAffiliation(name=v)) #: the primary email address of the user email = association_proxy('_primary_email', 'email', creator=lambda v: UserEmail(email=v, is_primary=True)) #: any additional emails the user might have secondary_emails = association_proxy('_secondary_emails', 'email', creator=lambda v: UserEmail(email=v)) #: all emails of the user. read-only; use it only for searching by email! also, do not use it between #: modifying `email` or `secondary_emails` and a session expire/commit! all_emails = association_proxy('_all_emails', 'email') # read-only! #: the user this user has been merged into merged_into_user = db.relationship( 'User', lazy=True, backref=db.backref('merged_from_users', lazy=True), remote_side='User.id', ) #: the users's favorite users favorite_users = db.relationship( 'User', secondary=favorite_user_table, primaryjoin=id == favorite_user_table.c.user_id, secondaryjoin=(id == favorite_user_table.c.target_id) & ~is_deleted, lazy=True, collection_class=set, backref=db.backref('favorite_of', lazy=True, collection_class=set), ) #: the users's favorite categories favorite_categories = db.relationship( 'Category', secondary=favorite_category_table, lazy=True, collection_class=set, backref=db.backref('favorite_of', lazy=True, collection_class=set), ) #: the user's category suggestions suggested_categories = db.relationship( 'SuggestedCategory', lazy='dynamic', order_by='SuggestedCategory.score.desc()', cascade='all, delete-orphan', backref=db.backref('user', lazy=True) ) #: the active API key of the user api_key = db.relationship( 'APIKey', lazy=True, uselist=False, cascade='all, delete-orphan', primaryjoin='(User.id == APIKey.user_id) & APIKey.is_active', back_populates='user' ) #: the previous API keys of the user old_api_keys = db.relationship( 'APIKey', lazy=True, cascade='all, delete-orphan', order_by='APIKey.created_dt.desc()', primaryjoin='(User.id == APIKey.user_id) & ~APIKey.is_active', back_populates='user' ) #: the identities used by this user identities = db.relationship( 'Identity', lazy=True, cascade='all, delete-orphan', collection_class=set, backref=db.backref('user', lazy=False) ) # relationship backrefs: # - _all_settings (UserSetting.user) # - abstract_comments (AbstractComment.user) # - abstract_email_log_entries (AbstractEmailLogEntry.user) # - abstract_reviews (AbstractReview.user) # - abstracts (Abstract.submitter) # - agreements (Agreement.user) # - attachment_files (AttachmentFile.user) # - attachments (Attachment.user) # - blockings (Blocking.created_by_user) # - content_reviewer_for_contributions (Contribution.paper_content_reviewers) # - created_events (Event.creator) # - editing_comments (EditingRevisionComment.user) # - editing_revisions (EditingRevision.submitter) # - editor_for_editables (Editable.editor) # - editor_for_revisions (EditingRevision.editor) # - event_log_entries (EventLogEntry.user) # - event_notes_revisions (EventNoteRevision.user) # - event_persons (EventPerson.user) # - event_reminders (EventReminder.creator) # - event_roles (EventRole.members) # - favorite_of (User.favorite_users) # - favorite_rooms (Room.favorite_of) # - in_attachment_acls (AttachmentPrincipal.user) # - in_attachment_folder_acls (AttachmentFolderPrincipal.user) # - in_blocking_acls (BlockingPrincipal.user) # - in_category_acls (CategoryPrincipal.user) # - in_contribution_acls (ContributionPrincipal.user) # - in_event_acls (EventPrincipal.user) # - in_event_settings_acls (EventSettingPrincipal.user) # - in_room_acls (RoomPrincipal.user) # - in_session_acls (SessionPrincipal.user) # - in_settings_acls (SettingPrincipal.user) # - in_track_acls (TrackPrincipal.user) # - judge_for_contributions (Contribution.paper_judges) # - judged_abstracts (Abstract.judge) # - judged_papers (PaperRevision.judge) # - layout_reviewer_for_contributions (Contribution.paper_layout_reviewers) # - local_groups (LocalGroup.members) # - merged_from_users (User.merged_into_user) # - modified_abstract_comments (AbstractComment.modified_by) # - modified_abstracts (Abstract.modified_by) # - modified_review_comments (PaperReviewComment.modified_by) # - oauth_tokens (OAuthToken.user) # - owned_rooms (Room.owner) # - paper_competences (PaperCompetence.user) # - paper_reviews (PaperReview.user) # - paper_revisions (PaperRevision.submitter) # - registrations (Registration.user) # - requests_created (Request.created_by_user) # - requests_processed (Request.processed_by_user) # - reservations (Reservation.created_by_user) # - reservations_booked_for (Reservation.booked_for_user) # - review_comments (PaperReviewComment.user) # - static_sites (StaticSite.creator) # - survey_submissions (SurveySubmission.user) # - vc_rooms (VCRoom.created_by_user) @staticmethod def get_system_user(): return User.query.filter_by(is_system=True).one() @property def as_principal(self): """The serializable principal identifier of this user""" return 'User', self.id @property def identifier(self): return 'User:{}'.format(self.id) @property def as_avatar(self): # TODO: remove this after DB is free of Avatars from indico.modules.users.legacy import AvatarUserWrapper avatar = AvatarUserWrapper(self.id) # avoid garbage collection avatar.user return avatar as_legacy = as_avatar @property def avatar_bg_color(self): from indico.modules.users.util import get_color_for_username return get_color_for_username(self.full_name) @property def avatar_css(self): return 'background-color: {};'.format(self.avatar_bg_color) @property def external_identities(self): """The external identities of the user""" return {x for x in self.identities if x.provider != 'indico'} @property def local_identities(self): """The local identities of the user""" return {x for x in self.identities if x.provider == 'indico'} @property def local_identity(self): """The main (most recently used) local identity""" identities = sorted(self.local_identities, key=attrgetter('safe_last_login_dt'), reverse=True) return identities[0] if identities else None @property def secondary_local_identities(self): """The local identities of the user except the main one""" return self.local_identities - {self.local_identity} @locator_property def locator(self): return {'user_id': self.id} @cached_property def settings(self): """Returns the user settings proxy for this user""" from indico.modules.users import user_settings return user_settings.bind(self) @property def synced_fields(self): """The fields of the user whose values are currently synced. This set is always a subset of the synced fields define in synced fields of the idp in 'indico.conf'. """ synced_fields = self.settings.get('synced_fields') # If synced_fields is missing or None, then all fields are synced if synced_fields is None: return multipass.synced_fields else: return set(synced_fields) & multipass.synced_fields @synced_fields.setter def synced_fields(self, value): value = set(value) & multipass.synced_fields if value == multipass.synced_fields: self.settings.delete('synced_fields') else: self.settings.set('synced_fields', list(value)) @property def synced_values(self): """The values from the synced identity for the user. Those values are not the actual user's values and might differ if they are not set as synchronized. """ identity = self._get_synced_identity(refresh=False) if identity is None: return {} return {field: (identity.data.get(field) or '') for field in multipass.synced_fields} def __contains__(self, user): """Convenience method for `user in user_or_group`.""" return self == user @return_ascii def __repr__(self): return format_repr(self, 'id', 'email', is_deleted=False, is_pending=False, _text=self.full_name) def can_be_modified(self, user): """If this user can be modified by the given user""" return self == user or user.is_admin def iter_identifiers(self, check_providers=False, providers=None): """Yields ``(provider, identifier)`` tuples for the user. :param check_providers: If True, providers are searched for additional identifiers once all existing identifiers have been yielded. :param providers: May be a set containing provider names to get only identifiers from the specified providers. """ done = set() for identity in self.identities: if providers is not None and identity.provider not in providers: continue item = (identity.provider, identity.identifier) done.add(item) yield item if not check_providers: return for identity_info in multipass.search_identities(providers=providers, exact=True, email=self.all_emails): item = (identity_info.provider.name, identity_info.identifier) if item not in done: yield item @property def can_get_all_multipass_groups(self): """Check whether it is possible to get all multipass groups the user is in.""" return all(multipass.identity_providers[x.provider].supports_get_identity_groups for x in self.identities if x.provider != 'indico' and x.provider in multipass.identity_providers) def iter_all_multipass_groups(self): """Iterate over all multipass groups the user is in""" return itertools.chain.from_iterable(multipass.identity_providers[x.provider].get_identity_groups(x.identifier) for x in self.identities if x.provider != 'indico' and x.provider in multipass.identity_providers) def get_full_name(self, *args, **kwargs): kwargs['_show_empty_names'] = True return super(User, self).get_full_name(*args, **kwargs) def make_email_primary(self, email): """Promotes a secondary email address to the primary email address :param email: an email address that is currently a secondary email """ secondary = next((x for x in self._secondary_emails if x.email == email), None) if secondary is None: raise ValueError('email is not a secondary email address') self._primary_email.is_primary = False db.session.flush() secondary.is_primary = True db.session.flush() def reset_signing_secret(self): self.signing_secret = unicode(uuid4()) def synchronize_data(self, refresh=False): """Synchronize the fields of the user from the sync identity. This will take only into account :attr:`synced_fields`. :param refresh: bool -- Whether to refresh the synced identity with the sync provider before instead of using the stored data. (Only if the sync provider supports refresh.) """ identity = self._get_synced_identity(refresh=refresh) if identity is None: return for field in self.synced_fields: old_value = getattr(self, field) new_value = identity.data.get(field) or '' if field in ('first_name', 'last_name') and not new_value: continue if old_value == new_value: continue flash(_("Your {field_name} has been synchronised from '{old_value}' to '{new_value}'.").format( field_name=syncable_fields[field], old_value=old_value, new_value=new_value)) setattr(self, field, new_value) def _get_synced_identity(self, refresh=False): sync_provider = multipass.sync_provider if sync_provider is None: return None identities = sorted([x for x in self.identities if x.provider == sync_provider.name], key=attrgetter('safe_last_login_dt'), reverse=True) if not identities: return None identity = identities[0] if refresh and identity.multipass_data is not None and sync_provider.supports_refresh: try: identity_info = sync_provider.refresh_identity(identity.identifier, identity.multipass_data) except IdentityRetrievalFailed: identity_info = None if identity_info: identity.data = identity_info.data return identity
class EventNote(LinkMixin, db.Model): __tablename__ = 'notes' allowed_link_types = LinkMixin.allowed_link_types - {LinkType.category} unique_links = True events_backref_name = 'notes' @declared_attr def __table_args__(cls): return auto_table_args(cls, schema='events') #: The ID of the note id = db.Column( db.Integer, primary_key=True ) #: If the note has been deleted is_deleted = db.Column( db.Boolean, nullable=False, default=False ) #: The rendered HTML of the note html = db.Column( db.Text, nullable=False ) #: The ID of the current revision current_revision_id = db.Column( db.Integer, db.ForeignKey('events.note_revisions.id', use_alter=True), nullable=True # needed for post_update :( ) #: The list of all revisions for the note revisions = db.relationship( 'EventNoteRevision', primaryjoin=lambda: EventNote.id == EventNoteRevision.note_id, foreign_keys=lambda: EventNoteRevision.note_id, lazy=True, cascade='all, delete-orphan', order_by=lambda: EventNoteRevision.created_dt.desc(), backref=db.backref( 'note', lazy=False ) ) #: The currently active revision of the note current_revision = db.relationship( 'EventNoteRevision', primaryjoin=lambda: EventNote.current_revision_id == EventNoteRevision.id, foreign_keys=current_revision_id, lazy=True, post_update=True ) @property def locator(self): return self.linked_object.getLocator() @classmethod def get_for_linked_object(cls, linked_object, preload_event=True): """Gets the note for the given object. This only returns a note that hasn't been deleted. :param linked_object: An event, session, contribution or subcontribution. :param preload_event: If all notes for the same event should be pre-loaded and cached in the app context. """ event = linked_object.getConference() try: return g.event_notes[event].get(linked_object) except (AttributeError, KeyError): if not preload_event: return cls.find_first(linked_object=linked_object, is_deleted=False) if 'event_notes' not in g: g.event_notes = {} g.event_notes[event] = {n.linked_object: n for n in EventNote.find(event_id=int(event.id), is_deleted=False)} return g.event_notes[event].get(linked_object) @classmethod def get_or_create(cls, linked_object): """Gets the note for the given object or creates a new one. If there is an existing note for the object, it will be returned even. Otherwise a new note is created. """ note = cls.find_first(linked_object=linked_object) if note is None: note = cls(linked_object=linked_object) return note def delete(self, user): """Marks the note as deleted and adds a new empty revision""" self.create_revision(self.current_revision.render_mode, '', user) self.is_deleted = True def create_revision(self, render_mode, source, user): """Creates a new revision if needed and marks it as undeleted if it was Any change to the render mode or the source causes a new revision to be created. The user is not taken into account since a user "modifying" a note without changing things is not really a change. """ self.is_deleted = False with db.session.no_autoflush: current = self.current_revision if current is not None and current.render_mode == render_mode and current.source == source: return current self.current_revision = EventNoteRevision(render_mode=render_mode, source=source, user=user) return self.current_revision @return_ascii def __repr__(self): return '<EventNote({}, current_revision={}{}, {})>'.format( self.id, self.current_revision_id, ', is_deleted=True' if self.is_deleted else '', self.link_repr )