def file(cls): return db.relationship( cls.stored_file_class, primaryjoin=lambda: cls.file_id == cls.stored_file_class.id, foreign_keys=lambda: cls.file_id, lazy=False, post_update=True)
def event_new(cls): return db.relationship( 'Event', lazy=True, backref=db.backref( cls.settings_backref_name, lazy='dynamic' ) )
def event(cls): return db.relationship( 'Event', lazy=True, backref=db.backref( cls.events_backref_name, lazy='dynamic' ) )
def reference_type(cls): return db.relationship( 'ReferenceType', lazy=False, backref=db.backref( cls.reference_backref_name, cascade='all, delete-orphan', lazy=True ) )
def review(cls): return db.relationship( cls.review_class, lazy=True, backref=db.backref( 'ratings', cascade='all, delete-orphan', lazy=True ) )
def event_new(cls): return db.relationship( 'Event', foreign_keys=cls.event_id, lazy=True, backref=db.backref( cls.events_backref_name, lazy='dynamic' ) )
def own_venue(cls): return db.relationship( 'Location', foreign_keys=[cls.own_venue_id], lazy=True, backref=db.backref( cls.location_backref_name, lazy='dynamic' ) )
def contribution_field(cls): return db.relationship( 'ContributionField', lazy=False, backref=db.backref( cls.contribution_field_backref_name, cascade='all, delete-orphan', lazy=True ) )
def own_room(cls): return db.relationship( 'Room', foreign_keys=[cls.own_room_id], lazy=True, backref=db.backref( cls.location_backref_name, lazy='dynamic' ) )
def user(cls): return db.relationship( 'User', lazy=True, foreign_keys=cls.user_id, backref=db.backref( cls.user_backref_name, primaryjoin='({0}.user_id == User.id) & ~{0}.is_deleted'.format(cls.__name__), lazy='dynamic' ) )
def all_files(cls): return db.relationship( cls.stored_file_class, primaryjoin= lambda: cls.id == getattr(cls.stored_file_class, cls.stored_file_fkey), foreign_keys= lambda: getattr(cls.stored_file_class, cls.stored_file_fkey), lazy=True, cascade='all, delete, delete-orphan', order_by=lambda: cls.stored_file_class.created_dt.desc(), backref=db.backref( getattr(cls.stored_file_class, 'version_of'), lazy=False))
def subcontribution(cls): if LinkType.subcontribution in cls.allowed_link_types: return db.relationship( 'SubContribution', lazy=True, backref=db.backref( cls.link_backref_name, cascade='all, delete-orphan', uselist=(cls.unique_links != True), # noqa lazy=cls.link_backref_lazy ) )
def paper_revision(cls): return db.relationship( 'PaperRevision', lazy=True, backref=db.backref( 'comments', primaryjoin='(PaperReviewComment.revision_id == PaperRevision.id) & ~PaperReviewComment.is_deleted', order_by=cls.created_dt, cascade='all, delete-orphan', lazy=True, ) )
def session_block(cls): if LinkType.session_block in cls.allowed_link_types: return db.relationship( 'SessionBlock', lazy=True, backref=db.backref( cls.link_backref_name, cascade='all, delete-orphan', uselist=(cls.unique_links != True), # noqa lazy=cls.link_backref_lazy ) )
def abstract(cls): return db.relationship( 'Abstract', lazy=True, backref=db.backref( 'comments', primaryjoin='(AbstractComment.abstract_id == Abstract.id) & ~AbstractComment.is_deleted', order_by=cls.created_dt, cascade='all, delete-orphan', lazy=True, ) )
def category(cls): if LinkType.category in cls.allowed_link_types: return db.relationship( 'Category', lazy=True, backref=db.backref( cls.link_backref_name, cascade='all, delete-orphan', uselist=(cls.unique_links != True), # noqa lazy=cls.link_backref_lazy ) )
def event(cls): return db.relationship( 'Event', lazy=True, backref=db.backref( cls.event_backref_name, primaryjoin='({0}.event_id == Event.id) & ~{0}.is_deleted'.format(cls.__name__), order_by=cls.position, cascade='all, delete-orphan', lazy=True ) )
def linked_event(cls): if LinkType.event in cls.allowed_link_types: return db.relationship( 'Event', foreign_keys=cls.linked_event_id, lazy=True, backref=db.backref( cls.link_backref_name, cascade='all, delete-orphan', uselist=(cls.unique_links != True), # noqa lazy=cls.link_backref_lazy ) )
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) .as_scalar()) return db.relationship( 'PaperRevision', uselist=False, lazy=True, viewonly=True, primaryjoin=db.and_(PaperRevision._contribution_id == cls.id, PaperRevision.submitted_dt == subquery) )
class EventLogEntry(db.Model): """Log entries for events""" __tablename__ = 'logs' __table_args__ = {'schema': 'events'} #: The ID of the log entry 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 ID of the user associated with the entry user_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=True) #: The date/time when the reminder was created logged_dt = db.Column(UTCDateTime, nullable=False, default=now_utc) #: The general area of the event the entry comes from realm = db.Column(PyIntEnum(EventLogRealm), nullable=False) #: The general kind of operation that was performed kind = db.Column(PyIntEnum(EventLogKind), nullable=False) #: The module the operation was related to (does not need to match #: something in indico.modules and should be human-friendly but not #: translated). module = db.Column(db.String, nullable=False) #: The type of the log entry. This needs to match the name of a log renderer. type = db.Column(db.String, nullable=False) #: A short one-line description of the logged action. #: Should not be translated! summary = db.Column(db.String, nullable=False) #: Type-specific data data = db.Column(JSON, nullable=False) #: The user associated with the log entry user = db.relationship('User', lazy=False, backref=db.backref('event_log_entries', lazy='dynamic')) #: The Event this log entry is associated with event_new = db.relationship('Event', lazy=True, backref=db.backref('log_entries', lazy='dynamic')) @property def logged_date(self): return self.logged_dt.date() @property def renderer(self): from indico.modules.events.logs.util import get_log_renderers return get_log_renderers().get(self.type) def render(self): """Renders the log entry to be displayed. If the renderer is not available anymore, e.g. because of a disabled plugin, ``None`` is returned. """ renderer = self.renderer return renderer.render_entry(self) if renderer else None @return_ascii def __repr__(self): realm = self.realm.name if self.realm is not None else None return '<EventLogEntry({}, {}, {}, {}, {}): {}>'.format( self.id, self.event_id, self.logged_dt, realm, self.module, self.summary)
class IPNetworkGroup(db.Model): __tablename__ = 'ip_network_groups' principal_type = PrincipalType.network principal_order = 1 @declared_attr def __table_args__(cls): return (db.Index('ix_uq_ip_network_groups_name_lower', db.func.lower(cls.name), unique=True), { 'schema': 'indico' }) id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String, nullable=False) description = db.Column(db.Text, nullable=False, default='') #: Whether the network group is hidden in ACL forms hidden = db.Column(db.Boolean, nullable=False, default=False) #: Grants all IPs in the network group read access to all attachments attachment_access_override = db.Column(db.Boolean, nullable=False, default=False) _networks = db.relationship('IPNetwork', lazy=False, cascade='all, delete-orphan', collection_class=set, backref=db.backref('group', lazy=True)) networks = association_proxy('_networks', 'network', creator=lambda v: IPNetwork(network=v)) # relationship backrefs: # - in_category_acls (CategoryPrincipal.ip_network_group) # - in_event_acls (EventPrincipal.ip_network_group) @property def identifier(self): return f'IPNetworkGroup:{self.id}' def __repr__(self): return format_repr(self, 'id', 'name', hidden=False, attachment_access_override=False) def __contains__(self, user): # This method is called via ``user in principal`` during ACL checks. # We have to take the IP from the request so if there's no request # (e.g. in the shell) we never grant IP-based access; same if we check # for a different user than the one from the current session. if not has_request_context() or not request.remote_addr: return False if session.user != user: return False return self.contains_ip(str(request.remote_addr)) def contains_ip(self, ip): ip = ip_address(ip) return any(ip in network for network in self.networks) @property def locator(self): return {'network_group_id': self.id}
class Abstract(ProposalMixin, ProposalRevisionMixin, DescriptionMixin, CustomFieldsMixin, AuthorsSpeakersMixin, db.Model): """An abstract that can be associated to a Contribution.""" __tablename__ = 'abstracts' __auto_table_args = ( db.Index(None, 'friendly_id', 'event_id', unique=True, postgresql_where=db.text('NOT is_deleted')), db.CheckConstraint( '(state = {}) OR (accepted_track_id IS NULL)'.format( AbstractState.accepted), name='accepted_track_id_only_accepted'), db.CheckConstraint( '(state = {}) OR (accepted_contrib_type_id IS NULL)'.format( AbstractState.accepted), name='accepted_contrib_type_id_only_accepted'), db.CheckConstraint( '(state = {}) = (merged_into_id IS NOT NULL)'.format( AbstractState.merged), name='merged_into_id_only_merged'), db.CheckConstraint( '(state = {}) = (duplicate_of_id IS NOT NULL)'.format( AbstractState.duplicate), name='duplicate_of_id_only_duplicate'), db.CheckConstraint( '(state IN ({}, {}, {}, {})) = (judge_id IS NOT NULL)'.format( AbstractState.accepted, AbstractState.rejected, AbstractState.merged, AbstractState.duplicate), name='judge_if_judged'), db.CheckConstraint( '(state IN ({}, {}, {}, {})) = (judgment_dt IS NOT NULL)'.format( AbstractState.accepted, AbstractState.rejected, AbstractState.merged, AbstractState.duplicate), name='judgment_dt_if_judged'), db.CheckConstraint( f'(state != {AbstractState.invited}) OR (uuid IS NOT NULL)', name='uuid_if_invited'), { 'schema': 'event_abstracts' }) possible_render_modes = {RenderMode.markdown} default_render_mode = RenderMode.markdown marshmallow_aliases = {'_description': 'content'} # Proposal mixin properties proposal_type = 'abstract' call_for_proposals_attr = 'cfa' delete_comment_endpoint = 'abstracts.delete_abstract_comment' create_comment_endpoint = 'abstracts.comment_abstract' edit_comment_endpoint = 'abstracts.edit_abstract_comment' create_review_endpoint = 'abstracts.review_abstract' edit_review_endpoint = 'abstracts.edit_review' create_judgment_endpoint = 'abstracts.judge_abstract' revisions_enabled = False AUTHORS_SPEAKERS_DISPLAY_ORDER_ATTR = 'display_order_key_lastname' @declared_attr def __table_args__(cls): return auto_table_args(cls) id = db.Column(db.Integer, primary_key=True) uuid = db.Column(UUID, index=True, unique=True, nullable=True) 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) title = db.Column(db.String, nullable=False) #: ID of the user who submitted the abstract submitter_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=False) submitted_contrib_type_id = db.Column(db.Integer, db.ForeignKey( 'events.contribution_types.id', ondelete='SET NULL'), nullable=True, index=True) submitted_dt = db.Column(UTCDateTime, nullable=False, default=now_utc) modified_by_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), nullable=True, index=True) modified_dt = db.Column( UTCDateTime, nullable=True, ) state = db.Column(PyIntEnum(AbstractState), nullable=False, default=AbstractState.submitted) submission_comment = db.Column(db.Text, nullable=False, default='') #: ID of the user who judged the abstract judge_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=True) _judgment_comment = db.Column('judgment_comment', db.Text, nullable=False, default='') judgment_dt = db.Column( UTCDateTime, nullable=True, ) accepted_track_id = db.Column(db.Integer, db.ForeignKey('events.tracks.id', ondelete='SET NULL'), nullable=True, index=True) accepted_contrib_type_id = db.Column(db.Integer, db.ForeignKey( 'events.contribution_types.id', ondelete='SET NULL'), nullable=True, index=True) merged_into_id = db.Column(db.Integer, db.ForeignKey('event_abstracts.abstracts.id'), index=True, nullable=True) duplicate_of_id = db.Column(db.Integer, db.ForeignKey('event_abstracts.abstracts.id'), index=True, nullable=True) is_deleted = db.Column(db.Boolean, nullable=False, default=False) event = db.relationship( 'Event', lazy=True, backref=db.backref( 'abstracts', primaryjoin= '(Abstract.event_id == Event.id) & ~Abstract.is_deleted', cascade='all, delete-orphan', lazy=True)) #: User who submitted the abstract submitter = db.relationship( 'User', lazy=True, foreign_keys=submitter_id, backref=db.backref( 'abstracts', primaryjoin= '(Abstract.submitter_id == User.id) & ~Abstract.is_deleted', lazy='dynamic')) modified_by = db.relationship( 'User', lazy=True, foreign_keys=modified_by_id, backref=db.backref( 'modified_abstracts', primaryjoin= '(Abstract.modified_by_id == User.id) & ~Abstract.is_deleted', lazy='dynamic')) submitted_contrib_type = db.relationship( 'ContributionType', lazy=True, foreign_keys=submitted_contrib_type_id, backref=db.backref( 'proposed_abstracts', primaryjoin= '(Abstract.submitted_contrib_type_id == ContributionType.id) & ~Abstract.is_deleted', lazy=True, passive_deletes=True)) submitted_for_tracks = db.relationship( 'Track', secondary='event_abstracts.submitted_for_tracks', collection_class=set, backref=db.backref( 'abstracts_submitted', primaryjoin= 'event_abstracts.submitted_for_tracks.c.track_id == Track.id', secondaryjoin= '(event_abstracts.submitted_for_tracks.c.abstract_id == Abstract.id) & ~Abstract.is_deleted', collection_class=set, lazy=True, passive_deletes=True)) reviewed_for_tracks = db.relationship( 'Track', secondary='event_abstracts.reviewed_for_tracks', collection_class=set, backref=db.backref( 'abstracts_reviewed', primaryjoin= 'event_abstracts.reviewed_for_tracks.c.track_id == Track.id', secondaryjoin= '(event_abstracts.reviewed_for_tracks.c.abstract_id == Abstract.id) & ~Abstract.is_deleted', collection_class=set, lazy=True, passive_deletes=True)) #: User who judged the abstract judge = db.relationship( 'User', lazy=True, foreign_keys=judge_id, backref=db.backref( 'judged_abstracts', primaryjoin='(Abstract.judge_id == User.id) & ~Abstract.is_deleted', lazy='dynamic')) accepted_track = db.relationship( 'Track', lazy=True, backref=db.backref( 'abstracts_accepted', primaryjoin= '(Abstract.accepted_track_id == Track.id) & ~Abstract.is_deleted', lazy=True, passive_deletes=True)) accepted_contrib_type = db.relationship( 'ContributionType', lazy=True, foreign_keys=accepted_contrib_type_id, backref=db.backref( 'abstracts_accepted', primaryjoin= '(Abstract.accepted_contrib_type_id == ContributionType.id) & ~Abstract.is_deleted', lazy=True, passive_deletes=True)) merged_into = db.relationship( 'Abstract', lazy=True, remote_side=id, foreign_keys=merged_into_id, backref=db.backref('merged_abstracts', primaryjoin=(db.remote(merged_into_id) == id) & ~db.remote(is_deleted), lazy=True)) duplicate_of = db.relationship( 'Abstract', lazy=True, remote_side=id, foreign_keys=duplicate_of_id, backref=db.backref('duplicate_abstracts', primaryjoin=(db.remote(duplicate_of_id) == id) & ~db.remote(is_deleted), lazy=True)) #: Data stored in abstract/contribution fields field_values = db.relationship('AbstractFieldValue', lazy=True, cascade='all, delete-orphan', backref=db.backref('abstract', lazy=True)) #: Persons associated with this abstract person_links = db.relationship('AbstractPersonLink', lazy=True, cascade='all, delete-orphan', order_by='AbstractPersonLink.display_order', backref=db.backref('abstract', lazy=True)) # relationship backrefs: # - comments (AbstractComment.abstract) # - contribution (Contribution.abstract) # - duplicate_abstracts (Abstract.duplicate_of) # - email_logs (AbstractEmailLogEntry.abstract) # - files (AbstractFile.abstract) # - merged_abstracts (Abstract.merged_into) # - proposed_related_abstract_reviews (AbstractReview.proposed_related_abstract) # - reviews (AbstractReview.abstract) @property def candidate_contrib_types(self): contrib_types = set() for track in self.reviewed_for_tracks: if self.get_track_reviewing_state( track) == AbstractReviewingState.positive: review = next((x for x in self.reviews if x.track == track), None) contrib_types.add(review.proposed_contribution_type) return contrib_types @property def candidate_tracks(self): states = { AbstractReviewingState.positive, AbstractReviewingState.conflicting } return { t for t in self.reviewed_for_tracks if self.get_track_reviewing_state(t) in states } @property def edit_track_mode(self): if not inspect(self).persistent: return EditTrackMode.both elif self.state not in { AbstractState.submitted, AbstractState.withdrawn }: return EditTrackMode.none elif (self.public_state in (AbstractPublicState.awaiting, AbstractPublicState.withdrawn) and self.reviewed_for_tracks == self.submitted_for_tracks): return EditTrackMode.both else: return EditTrackMode.reviewed_for @property def public_state(self): if self.state != AbstractState.submitted: return getattr(AbstractPublicState, self.state.name) elif self.reviews: return AbstractPublicState.under_review else: return AbstractPublicState.awaiting @property def reviewing_state(self): if not self.reviews: return AbstractReviewingState.not_started track_states = { x: self.get_track_reviewing_state(x) for x in self.reviewed_for_tracks } positiveish_states = { AbstractReviewingState.positive, AbstractReviewingState.conflicting } if any(x == AbstractReviewingState.not_started for x in track_states.values()): return AbstractReviewingState.in_progress elif all(x == AbstractReviewingState.negative for x in track_states.values()): return AbstractReviewingState.negative elif all(x in positiveish_states for x in track_states.values()): if len(self.reviewed_for_tracks) > 1: # Accepted for more than one track return AbstractReviewingState.conflicting elif any(x == AbstractReviewingState.conflicting for x in track_states.values()): # The only accepted track is in conflicting state return AbstractReviewingState.conflicting else: return AbstractReviewingState.positive else: return AbstractReviewingState.mixed @property def score(self): scores = [x.score for x in self.reviews if x.score is not None] if not scores: return None return sum(scores) / len(scores) @property def data_by_field(self): return { value.contribution_field_id: value for value in self.field_values } @locator_property def locator(self): return dict(self.event.locator, abstract_id=self.id) @locator.token def locator(self): return dict(self.event.locator, uuid=self.uuid) @hybrid_property def judgment_comment(self): return MarkdownText(self._judgment_comment) @judgment_comment.setter def judgment_comment(self, value): self._judgment_comment = value @judgment_comment.expression def judgment_comment(cls): return cls._judgment_comment @property def verbose_title(self): return f'#{self.friendly_id} ({self.title})' @property def is_in_final_state(self): return self.state != AbstractState.submitted @property def modification_ended(self): return self.event.cfa.modification_ended def __repr__(self): return format_repr(self, 'id', 'event_id', is_deleted=False, _text=text_to_repr(self.title)) def can_access(self, user): if not user: return False if self.submitter == user: return True if self.event.can_manage(user): return True if any(x.person.user == user for x in self.person_links): return True return self.can_judge(user) or self.can_convene( user) or self.can_review(user) def can_comment(self, user, check_state=False): if not user: return False if check_state and self.is_in_final_state: return False if not self.event.cfa.allow_comments: return False if self.user_owns( user) and self.event.cfa.allow_contributors_in_comments: return True return self.can_judge(user) or self.can_convene( user) or self.can_review(user) def can_convene(self, user): if not user: return False elif not self.event.can_manage( user, permission='track_convener', explicit_permission=True): return False elif self.event.can_manage(user, permission='convene_all_abstracts', explicit_permission=True): return True elif any( track.can_manage( user, permission='convene', explicit_permission=True) for track in self.reviewed_for_tracks): return True else: return False def can_review(self, user, check_state=False): # The total number of tracks/events a user is a reviewer for (indico-wide) # is usually reasonably low so we just access the relationships instead of # sending a more specific query which would need to be cached to avoid # repeating it when performing this check on many abstracts. if not user: return False elif check_state and self.public_state not in ( AbstractPublicState.under_review, AbstractPublicState.awaiting): return False elif not self.event.can_manage(user, permission='abstract_reviewer', explicit_permission=True): return False elif self.event.can_manage(user, permission='review_all_abstracts', explicit_permission=True): return True elif any( track.can_manage( user, permission='review', explicit_permission=True) for track in self.reviewed_for_tracks): return True else: return False def can_judge(self, user, check_state=False): if not user: return False elif check_state and self.state != AbstractState.submitted: return False elif self.event.can_manage(user): return True elif self.event.cfa.allow_convener_judgment and self.can_convene(user): return True else: return False def can_edit(self, user): if not user: return False manager_edit_states = ( AbstractPublicState.under_review, AbstractPublicState.withdrawn, AbstractPublicState.awaiting, AbstractPublicState.invited, ) if self.public_state in manager_edit_states and self.event.can_manage( user): return True elif self.public_state not in (AbstractPublicState.awaiting, AbstractPublicState.invited): return False elif not self.user_owns(user) or not self.event.cfa.can_edit_abstracts( user): return False editing_allowed = self.event.cfa.allow_editing author_type = next( (x.author_type for x in self.person_links if x.person.user == user), None) is_primary = author_type == AuthorType.primary is_secondary = author_type == AuthorType.secondary if user == self.submitter: return True elif editing_allowed == AllowEditingType.submitter_all: return True elif editing_allowed == AllowEditingType.submitter_primary and is_primary: return True elif editing_allowed == AllowEditingType.submitter_authors and ( is_primary or is_secondary): return True return False def can_withdraw(self, user, check_state=False): if not user: return False elif self.event.can_manage(user) and ( not check_state or self.state != AbstractState.withdrawn): return True elif user == self.submitter and (not check_state or self.state == AbstractState.submitted): return True else: return False def can_see_reviews(self, user): return self.can_judge(user) or self.can_convene(user) 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 return sorted(chain(comments, reviews), key=attrgetter('created_dt')) def get_track_reviewing_state(self, track): if track not in self.reviewed_for_tracks: raise ValueError("Abstract not in review for given track") reviews = self.get_reviews(group=track) if not reviews: return AbstractReviewingState.not_started rejections = any(x.proposed_action == AbstractAction.reject for x in reviews) acceptances = { x for x in reviews if x.proposed_action == AbstractAction.accept } if rejections and not acceptances: return AbstractReviewingState.negative elif acceptances and not rejections: proposed_contrib_types = { x.proposed_contribution_type for x in acceptances if x.proposed_contribution_type is not None } if len(proposed_contrib_types) <= 1: return AbstractReviewingState.positive else: return AbstractReviewingState.conflicting else: return AbstractReviewingState.mixed def get_track_question_scores(self): query = (db.session.query( AbstractReview.track_id, AbstractReviewQuestion, db.func.avg( AbstractReviewRating.value.op('#>>')('{}').cast( db.Integer))).join(AbstractReviewRating.review).join( AbstractReviewRating.question).filter( AbstractReview.abstract == self, AbstractReviewQuestion.field_type == 'rating', db.func.jsonb_typeof( AbstractReviewRating.value) == 'null', ~AbstractReviewQuestion.is_deleted, ~AbstractReviewQuestion.no_score).group_by( AbstractReview.track_id, AbstractReviewQuestion.id)) scores = defaultdict(lambda: defaultdict(lambda: None)) for track_id, question, score in query: scores[track_id][question] = score return scores def get_reviewed_for_groups(self, user, include_reviewed=False): already_reviewed = { each.track for each in self.get_reviews(user=user) } if include_reviewed else set() if self.event.can_manage(user, permission='review_all_abstracts', explicit_permission=True): return self.reviewed_for_tracks | already_reviewed reviewer_tracks = { track for track in self.reviewed_for_tracks if track.can_manage( user, permission='review', explicit_permission=True) } return reviewer_tracks | already_reviewed def get_track_score(self, track): if track not in self.reviewed_for_tracks: raise ValueError("Abstract not in review for given track") reviews = [x for x in self.reviews if x.track == track] scores = [x.score for x in reviews if x.score is not None] if not scores: return None return sum(scores) / len(scores) def reset_state(self): self.state = AbstractState.submitted self.judgment_comment = '' self.judge = None self.judgment_dt = None self.accepted_track = None self.accepted_contrib_type = None self.merged_into = None self.duplicate_of = None def user_owns(self, user): if not user: return None return user == self.submitter or any(x.person.user == user for x in self.person_links) def log(self, *args, **kwargs): """Log with prefilled metadata for the abstract.""" self.event.log(*args, meta={'abstract_id': self.id}, **kwargs)
class EventNote(LinkMixin, db.Model): __tablename__ = 'notes' allowed_link_types = LinkMixin.allowed_link_types - {LinkType.category} unique_links = True events_backref_name = 'all_notes' link_backref_name = 'note' @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 ) @locator_property def locator(self): return self.object.locator @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.event_new 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): """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(object=linked_object) if note is None: note = cls(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 )
class RegistrationData(StoredFileMixin, db.Model): """Data entry within a registration for a field in a registration form""" __tablename__ = 'registration_data' __table_args__ = {'schema': 'event_registration'} # StoredFileMixin settings add_file_date_column = False file_required = False #: The ID of the registration registration_id = db.Column( db.Integer, db.ForeignKey('event_registration.registrations.id'), primary_key=True, autoincrement=False) #: The ID of the field data field_data_id = db.Column( db.Integer, db.ForeignKey('event_registration.form_field_data.id'), primary_key=True, autoincrement=False) #: The submitted data for the field data = db.Column(JSONB, default=lambda: None, nullable=False) #: The associated field data object field_data = db.relationship('RegistrationFormFieldData', lazy=True, backref=db.backref( 'registration_data', lazy=True, cascade='all, delete-orphan')) # relationship backrefs: # - registration (Registration.data) @locator_property def locator(self): # a normal locator doesn't make much sense raise NotImplementedError @locator.file def locator(self): """A locator that pointsto the associated file.""" if not self.filename: raise Exception( 'The file locator is only available if there is a file.') return dict(self.registration.locator, field_data_id=self.field_data_id, filename=self.filename) @property def friendly_data(self): return self.get_friendly_data(for_humans=False) def get_friendly_data(self, **kwargs): return self.field_data.field.get_friendly_data(self, **kwargs) @property def price(self): return self.field_data.field.calculate_price(self) @property def summary_data(self): return {'data': self.friendly_data, 'price': self.price} @property def user_data(self): return self.filename if self.field_data.field.input_type == 'file' else self.data def _set_file(self, file_info): # in case we are deleting/replacing a file self.storage_backend = None self.storage_file_id = None self.filename = None self.content_type = None self.size = None if file_info is not None: self.filename = file_info['name'] self.content_type = file_info['content_type'] self.save(file_info['data']) file = property(fset=_set_file) del _set_file @return_ascii def __repr__(self): return '<RegistrationData({}, {}): {}>'.format(self.registration_id, self.field_data_id, self.data) def _build_storage_path(self): self.registration.registration_form.assign_id() self.registration.assign_id() path_segments = [ 'event', strict_unicode(self.registration.event_id), 'registrations', strict_unicode(self.registration.registration_form.id), strict_unicode(self.registration.id) ] assert None not in path_segments # add timestamp in case someone uploads the same file again filename = '{}-{}-{}'.format(self.field_data.field_id, int(time.time()), self.filename) path = posixpath.join(*(path_segments + [filename])) return Config.getInstance().getAttachmentStorage(), path def render_price(self): return format_currency(self.price, self.registration.currency, locale=session.lang or 'en_GB')
def review(cls): return db.relationship(cls.review_class, lazy=True, backref=db.backref('ratings', cascade='all, delete-orphan', lazy=True))
class Survey(db.Model): __tablename__ = 'surveys' __table_args__ = (db.CheckConstraint("anonymous OR require_user", 'valid_anonymous_user'), { 'schema': 'event_surveys' }) #: The ID of the survey 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 survey title = db.Column(db.String, nullable=False) uuid = db.Column(UUID, unique=True, nullable=False, default=lambda: unicode(uuid4())) # An introduction text for users of the survey introduction = db.Column(db.Text, nullable=False, default='') #: Whether submissions will not be linked to a user anonymous = db.Column(db.Boolean, nullable=False, default=False) #: Whether submissions must be done by logged users require_user = db.Column(db.Boolean, nullable=False, default=True) # #: Whether the survey is only for selected users private = db.Column(db.Boolean, nullable=False, default=False) #: Maximum number of submissions allowed submission_limit = db.Column(db.Integer, nullable=True) #: Datetime when the survey is open start_dt = db.Column(UTCDateTime, nullable=True) #: Datetime when the survey is closed end_dt = db.Column(UTCDateTime, nullable=True) #: Whether the survey has been marked as deleted is_deleted = db.Column(db.Boolean, nullable=False, default=False) #: Whether start notification has been already sent start_notification_sent = db.Column(db.Boolean, nullable=False, default=False) #: Whether to send survey related notifications to users notifications_enabled = db.Column(db.Boolean, nullable=False, default=False) #: Whether include Participants / Registrants when sending start notifications notify_participants = db.Column(db.Boolean, nullable=False, default=False) #: Email addresses to notify about the start of a survey start_notification_emails = db.Column(ARRAY(db.String), nullable=False, default=[]) #: Email addresses to notify about new submissions new_submission_emails = db.Column(ARRAY(db.String), nullable=False, default=[]) #: Whether answers can be saved without submitting the survey partial_completion = db.Column(db.Boolean, nullable=False, default=False) #: The last user-friendly submission ID _last_friendly_submission_id = db.deferred( db.Column('last_friendly_submission_id', db.Integer, nullable=False, default=0)) #: The list of submissions submissions = db.relationship('SurveySubmission', cascade='all, delete-orphan', lazy=True, backref=db.backref('survey', lazy=True)) #: The list of items items = db.relationship('SurveyItem', cascade='all, delete-orphan', lazy=True, backref=db.backref('survey', lazy=True)) #: The list of sections sections = db.relationship('SurveySection', lazy=True, viewonly=True, order_by='SurveySection.position') #: The list of questions questions = db.relationship('SurveyQuestion', lazy=True, viewonly=True) #: The Event containing this survey event = db.relationship('Event', lazy=True, backref=db.backref('surveys', 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 != None) & (cls.end_dt <= now_utc()) # noqa @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 != None) & (cls.start_dt <= now_utc()) # noqa @locator_property def locator(self): return {'confId': self.event_id, 'survey_id': self.id} @locator.token def locator(self): """A locator that adds the UUID if the survey is private""" token = self.uuid if self.private else None return dict(self.locator, token=token) @property def state(self): if not self.questions: return SurveyState.not_ready if not self.has_started: return SurveyState.ready_to_open if not self.has_ended: if not self.submissions: return SurveyState.active_and_clean return SurveyState.active_and_answered return SurveyState.finished @property def start_notification_recipients(self): """Returns all recipients of the notifications. This includes both explicit recipients and, if enabled, participants of the event. """ recipients = set(self.start_notification_emails) if self.notify_participants: recipients.update( reg.email for reg in Registration.get_all_for_event(self.event)) recipients.discard( '') # just in case there was an empty email address somewhere return recipients @hybrid_property def is_active(self): return not self.is_deleted and self.state in { SurveyState.active_and_answered, SurveyState.active_and_clean } @is_active.expression def is_active(cls): return ~cls.is_deleted & cls.questions.any( ) & cls.has_started & ~cls.has_ended @hybrid_property def is_visible(self): return (not self.is_deleted and self.state in { SurveyState.active_and_answered, SurveyState.active_and_clean, SurveyState.finished }) @is_visible.expression def is_visible(cls): return ~cls.is_deleted & cls.questions.any() & cls.has_started @return_ascii def __repr__(self): return '<Survey({}, {}): {}>'.format(self.id, self.event_id, self.title) def can_submit(self, user): return self.is_active and (not self.require_user or user) def open(self): if self.state != SurveyState.ready_to_open: raise IndicoError("Survey can't be opened") self.start_dt = now_utc() def close(self): if self.state not in (SurveyState.active_and_clean, SurveyState.active_and_answered): raise IndicoError("Survey can't be closed") self.end_dt = now_utc() def send_start_notification(self): if not self.notifications_enabled or self.start_notification_sent or not self.event.has_feature( 'surveys'): return template_module = get_template_module( 'events/surveys/emails/start_notification_email.txt', survey=self) email = make_email(bcc_list=self.start_notification_recipients, template=template_module) send_email(email, event=self.event, module='Surveys') logger.info('Sending start notification for survey %s', self) self.start_notification_sent = True def send_submission_notification(self, submission): if not self.notifications_enabled: return template_module = get_template_module( 'events/surveys/emails/new_submission_email.txt', submission=submission) email = make_email(bcc_list=self.new_submission_emails, template=template_module) send_email(email, event=self.event, module='Surveys') logger.info('Sending submission notification for survey %s', self)
class DesignerTemplate(db.Model): __tablename__ = 'designer_templates' __table_args__ = (db.CheckConstraint( "(event_id IS NULL) != (category_id IS NULL)", 'event_xor_category_id_null'), { 'schema': 'indico' }) id = db.Column(db.Integer, primary_key=True) type = db.Column(PyIntEnum(TemplateType), nullable=False) title = db.Column(db.String, nullable=False) event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=True) category_id = db.Column(db.Integer, db.ForeignKey('categories.categories.id'), index=True, nullable=True) data = db.Column(JSONB, nullable=False) background_image_id = db.Column( db.Integer, db.ForeignKey('indico.designer_image_files.id'), index=False, nullable=True) backside_template_id = db.Column( db.ForeignKey('indico.designer_templates.id'), index=True, nullable=True) is_clonable = db.Column(db.Boolean, nullable=False, default=True) is_system_template = db.Column(db.Boolean, nullable=False, default=False) category = db.relationship('Category', lazy=True, foreign_keys=category_id, backref=db.backref('designer_templates', cascade='all, delete-orphan', lazy=True)) event = db.relationship('Event', lazy=True, backref=db.backref('designer_templates', cascade='all, delete-orphan', lazy=True)) background_image = db.relationship('DesignerImageFile', lazy=True, foreign_keys=background_image_id, post_update=True) backside_template = db.relationship('DesignerTemplate', lazy=True, remote_side=id, backref='backside_template_of') # relationship backrefs: # - backside_template_of (DesignerTemplate.backside_template) # - default_badge_template_of (Category.default_badge_template) # - default_ticket_template_of (Category.default_ticket_template) # - images (DesignerImageFile.template) # - ticket_for_regforms (RegistrationForm.ticket_template) def __init__(self, **kwargs): data = kwargs.pop('data', None) tpl_type = kwargs.get('type') if data is None: data = {'items': [], 'background_position': 'stretch'} size = DEFAULT_CONFIG[tpl_type]['tpl_size'] data.update({'width': size[0], 'height': size[1]}) super().__init__(data=data, **kwargs) @hybrid_property def owner(self): return self.event if self.event else self.category @owner.comparator def owner(cls): return _OwnerComparator(cls) @locator_property def locator(self): return dict(self.owner.locator, template_id=self.id) @property def is_ticket(self): placeholders = get_placeholders('designer-fields') if any(placeholders[item['type']].is_ticket for item in self.data['items'] if item['type'] in placeholders): return True elif self.backside_template and self.backside_template.is_ticket: return True else: return False def __repr__(self): return format_repr(self, 'id', 'event_id', 'category_id', _text=self.title)
class Session(DescriptionMixin, ColorMixin, ProtectionManagersMixin, LocationMixin, AttachedItemsMixin, AttachedNotesMixin, db.Model): __tablename__ = 'sessions' __auto_table_args = (db.Index(None, 'friendly_id', 'event_id', unique=True, postgresql_where=db.text('NOT is_deleted')), db.CheckConstraint("date_trunc('minute', default_contribution_duration) = " "default_contribution_duration", 'default_contribution_duration_no_seconds'), {'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().__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) 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 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, )) 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): authorized_submitter = self.revision.editable.can_perform_submitter_actions( user) authorized_editor = self.revision.editable.can_perform_editor_actions( user) 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 ContributionField(db.Model): __tablename__ = 'contribution_fields' __table_args__ = (db.UniqueConstraint('event_id', 'legacy_id'), { '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) legacy_id = db.Column(db.String, nullable=True) position = db.Column(db.Integer, nullable=False, default=_get_next_position) title = db.Column(db.String, nullable=False) description = db.Column(db.Text, nullable=False, default='') is_required = db.Column(db.Boolean, nullable=False, default=False) is_active = db.Column(db.Boolean, nullable=False, default=True) is_user_editable = db.Column(db.Boolean, nullable=False, default=True) visibility = db.Column(PyIntEnum(ContributionFieldVisibility), nullable=False, default=ContributionFieldVisibility.public) field_type = db.Column(db.String, nullable=True) field_data = db.Column(JSONB, nullable=False, default={}) event = db.relationship('Event', lazy=True, backref=db.backref('contribution_fields', order_by=position, cascade='all, delete-orphan', lazy='dynamic')) # relationship backrefs: # - abstract_values (AbstractFieldValue.contribution_field) # - contribution_values (ContributionFieldValue.contribution_field) def _get_field(self, management=False): from indico.modules.events.contributions import get_contrib_field_types try: impl = get_contrib_field_types()[self.field_type] except KeyError: return None return impl(self, management=management) @property def field(self): return self._get_field() @property def mgmt_field(self): return self._get_field(management=True) @property def is_public(self): return self.visibility == ContributionFieldVisibility.public @property def filter_choices(self): return { x['id']: x['option'] for x in self.field_data.get('options', {}) } def __repr__(self): return format_repr(self, 'id', 'field_type', is_required=False, is_active=True, _text=self.title) @locator_property def locator(self): return dict(self.event.locator, contrib_field_id=self.id)
class VCRoom(db.Model): __tablename__ = 'vc_rooms' __table_args__ = (db.Index(None, 'data', postgresql_using='gin'), {'schema': 'events'}) #: Videoconference room ID id = db.Column( db.Integer, primary_key=True ) #: Type of the videoconference room type = db.Column( db.String, nullable=False ) #: Name of the videoconference room name = db.Column( db.String, nullable=False ) #: Status of the videoconference room status = db.Column( PyIntEnum(VCRoomStatus), nullable=False ) #: ID of the creator created_by_id = db.Column( db.Integer, db.ForeignKey('users.users.id'), nullable=False, index=True ) #: Creation timestamp of the videoconference room created_dt = db.Column( UTCDateTime, nullable=False, default=now_utc ) #: Modification timestamp of the videoconference room modified_dt = db.Column( UTCDateTime ) #: videoconference plugin-specific data data = db.Column( JSONB, nullable=False ) #: The user who created the videoconference room created_by_user = db.relationship( 'User', lazy=True, backref=db.backref( 'vc_rooms', lazy='dynamic' ) ) # relationship backrefs: # - events (VCRoomEventAssociation.vc_room) @property def plugin(self): from indico.modules.vc.util import get_vc_plugins return get_vc_plugins().get(self.type) @property def locator(self): return {'vc_room_id': self.id, 'service': self.type} @return_ascii def __repr__(self): return '<VCRoom({}, {}, {})>'.format(self.id, self.name, self.type)
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_new = 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_new, cls.linked_contrib: lambda x: x.event_new, 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_new = 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_new.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_new, assoc.link_object) ) vc_room.events.remove(assoc) else: Logger.get('modules.vc').info("Detaching VC room {} from event {} ({})".format( vc_room, self.event_new, 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_new) notify_deleted(vc_room.plugin, vc_room, self, self.event_new, user) db.session.delete(vc_room)
class Registration(db.Model): """Somebody's registration for an event through a registration form""" __tablename__ = 'registrations' __table_args__ = ( db.CheckConstraint('email = lower(email)', 'lowercase_email'), db.Index(None, 'friendly_id', 'event_id', unique=True, postgresql_where=db.text('NOT is_deleted')), db.Index(None, 'registration_form_id', 'user_id', unique=True, postgresql_where=db.text( 'NOT is_deleted AND (state NOT IN (3, 4))')), db.Index(None, 'registration_form_id', 'email', unique=True, postgresql_where=db.text( 'NOT is_deleted AND (state NOT IN (3, 4))')), db.ForeignKeyConstraint(['event_id', 'registration_form_id'], [ 'event_registration.forms.event_id', 'event_registration.forms.id' ]), { 'schema': 'event_registration' }) #: The ID of the object id = db.Column(db.Integer, primary_key=True) #: The unguessable ID for the object uuid = db.Column(UUID, index=True, unique=True, nullable=False, default=lambda: unicode(uuid4())) #: The human-friendly ID for the object friendly_id = db.Column(db.Integer, nullable=False, default=_get_next_friendly_id) #: The ID of the event event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False) #: The ID of the registration form registration_form_id = db.Column( db.Integer, db.ForeignKey('event_registration.forms.id'), index=True, nullable=False) #: The ID of the user who registered user_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=True) #: The ID of the latest payment transaction associated with this registration transaction_id = db.Column(db.Integer, db.ForeignKey('events.payment_transactions.id'), index=True, unique=True, nullable=True) #: The state a registration is in state = db.Column( PyIntEnum(RegistrationState), nullable=False, ) #: The base registration fee (that is not specific to form items) base_price = db.Column( db.Numeric(8, 2), # max. 999999.99 nullable=False, default=0) #: The price modifier applied to the final calculated price price_adjustment = db.Column( db.Numeric(8, 2), # max. 999999.99 nullable=False, default=0) #: Registration price currency currency = db.Column(db.String, nullable=False) #: The date/time when the registration was recorded submitted_dt = db.Column( UTCDateTime, nullable=False, default=now_utc, ) #: The email of the registrant email = db.Column(db.String, nullable=False) #: The first name of the registrant first_name = db.Column(db.String, nullable=False) #: The last name of the registrant last_name = db.Column(db.String, nullable=False) #: If the registration has been deleted is_deleted = db.Column(db.Boolean, nullable=False, default=False) #: The unique token used in tickets ticket_uuid = db.Column(UUID, index=True, unique=True, nullable=False, default=lambda: unicode(uuid4())) #: Whether the person has checked in. Setting this also sets or clears #: `checked_in_dt`. checked_in = db.Column(db.Boolean, nullable=False, default=False) #: The date/time when the person has checked in checked_in_dt = db.Column(UTCDateTime, nullable=True) #: The Event containing this registration event_new = db.relationship('Event', lazy=True, backref=db.backref('registrations', lazy='dynamic')) # The user linked to this registration user = db.relationship( 'User', lazy=True, backref=db.backref( 'registrations', lazy='dynamic' # XXX: a delete-orphan cascade here would delete registrations when NULLing the user )) #: The latest payment transaction associated with this registration transaction = db.relationship('PaymentTransaction', lazy=True, foreign_keys=[transaction_id], post_update=True) #: The registration this data is associated with data = db.relationship('RegistrationData', lazy=True, cascade='all, delete-orphan', backref=db.backref('registration', lazy=True)) # relationship backrefs: # - invitation (RegistrationInvitation.registration) # - legacy_mapping (LegacyRegistrationMapping.registration) # - registration_form (RegistrationForm.registrations) # - transactions (PaymentTransaction.registration) @classmethod def get_all_for_event(cls, event): """Retrieve all registrations in all registration forms of an event.""" from indico.modules.events.registration.models.forms import RegistrationForm return Registration.find_all(Registration.is_active, ~RegistrationForm.is_deleted, RegistrationForm.event_id == event.id, _join=Registration.registration_form) @hybrid_property def is_active(self): return not self.is_cancelled and not self.is_deleted @is_active.expression def is_active(cls): return ~cls.is_cancelled & ~cls.is_deleted @hybrid_property def is_cancelled(self): return self.state in (RegistrationState.rejected, RegistrationState.withdrawn) @is_cancelled.expression def is_cancelled(self): return self.state.in_( (RegistrationState.rejected, RegistrationState.withdrawn)) @locator_property def locator(self): return dict(self.registration_form.locator, registration_id=self.id) @locator.registrant def locator(self): """A locator suitable for 'display' pages. It includes the UUID of the registration unless the current request doesn't contain the uuid and the registration is tied to the currently logged-in user. """ loc = self.registration_form.locator if (not self.user or not has_request_context() or self.user != session.user or request.args.get('token') == self.uuid): loc['token'] = self.uuid return loc @locator.uuid def locator(self): """A locator that uses uuid instead of id""" return dict(self.registration_form.locator, token=self.uuid) @property def can_be_modified(self): regform = self.registration_form return regform.is_modification_open and regform.is_modification_allowed( self) @property def data_by_field(self): return {x.field_data.field_id: x for x in self.data} @property def billable_data(self): return [data for data in self.data if data.price] @property def full_name(self): """Returns the user's name in 'Firstname Lastname' notation.""" return self.get_full_name(last_name_first=False) @property def display_full_name(self): """Return the full name using the user's preferred name format.""" return format_display_full_name(session.user, self) @property def is_paid(self): """Returns whether the registration has been paid for.""" paid_states = {TransactionStatus.successful, TransactionStatus.pending} return self.transaction is not None and self.transaction.status in paid_states @property def price(self): """The total price of the registration. This includes the base price, the field-specific price, and the custom price adjustment for the registrant. :rtype: Decimal """ # we convert the calculated price (float) to a string to avoid this: # >>> Decimal(100.1) # Decimal('100.099999999999994315658113919198513031005859375') # >>> Decimal('100.1') # Decimal('100.1') calc_price = Decimal(str(sum(data.price for data in self.data))) base_price = self.base_price or Decimal('0') price_adjustment = self.price_adjustment or Decimal('0') return (base_price + price_adjustment + calc_price).max(0) @property def summary_data(self): """Export registration data nested in sections and fields""" def _fill_from_regform(): for section in self.registration_form.sections: if not section.is_visible: continue summary[section] = OrderedDict() for field in section.fields: if not field.is_visible: continue summary[section][field] = field_summary[field] def _fill_from_registration(): for field, data in field_summary.iteritems(): section = field.parent summary.setdefault(section, OrderedDict()) if field not in summary[section]: summary[section][field] = data summary = OrderedDict() field_summary = {x.field_data.field: x for x in self.data} _fill_from_regform() _fill_from_registration() return summary @property def has_files(self): return any(item.storage_file_id is not None for item in self.data) @property def sections_with_answered_fields(self): return [ x for x in self.registration_form.sections if any(child.id in self.data_by_field for child in x.children) ] @classproperty @classmethod def order_by_name(cls): return db.func.lower(cls.last_name), db.func.lower( cls.first_name), cls.friendly_id @return_ascii def __repr__(self): return format_repr(self, 'id', 'registration_form_id', 'email', 'state', user_id=None, is_deleted=False, _text=self.full_name) def get_full_name(self, last_name_first=True, last_name_upper=False, abbrev_first_name=False): """Returns the user's in the specified notation. If not format options are specified, the name is returned in the 'Lastname, Firstname' notation. Note: Do not use positional arguments when calling this method. Always use keyword arguments! :param last_name_first: if "lastname, firstname" instead of "firstname lastname" should be used :param last_name_upper: if the last name should be all-uppercase :param abbrev_first_name: if the first name should be abbreviated to use only the first character """ return format_full_name(self.first_name, self.last_name, last_name_first=last_name_first, last_name_upper=last_name_upper, abbrev_first_name=abbrev_first_name) def get_personal_data(self): personal_data = {} for data in self.data: field = data.field_data.field if field.personal_data_type is not None and data.data: personal_data[ field.personal_data_type.name] = data.friendly_data # might happen with imported legacy registrations (missing personal data) personal_data.setdefault('first_name', self.first_name) personal_data.setdefault('last_name', self.last_name) personal_data.setdefault('email', self.email) return personal_data def _render_price(self, price): return format_currency(price, self.currency, locale=session.lang or 'en_GB') def render_price(self): return self._render_price(self.price) def render_base_price(self): return self._render_price(self.base_price) def render_price_adjustment(self): return self._render_price(self.price_adjustment) def sync_state(self, _skip_moderation=True): """Sync the state of the registration""" initial_state = self.state regform = self.registration_form invitation = self.invitation moderation_required = (regform.moderation_enabled and not _skip_moderation and (not invitation or not invitation.skip_moderation)) with db.session.no_autoflush: payment_required = regform.event_new.has_feature( 'payment') and self.price and not self.is_paid if self.state is None: if moderation_required: self.state = RegistrationState.pending elif payment_required: self.state = RegistrationState.unpaid else: self.state = RegistrationState.complete elif self.state == RegistrationState.unpaid: if not self.price: self.state = RegistrationState.complete elif self.state == RegistrationState.complete: if payment_required: self.state = RegistrationState.unpaid if self.state != initial_state: signals.event.registration_state_updated.send( self, previous_state=initial_state) def update_state(self, approved=None, paid=None, rejected=None, _skip_moderation=False): """Update the state of the registration for a given action The accepted kwargs are the possible actions. ``True`` means that the action occured and ``False`` that it was reverted. """ if sum(action is not None for action in (approved, paid, rejected)) > 1: raise Exception("More than one action specified") initial_state = self.state regform = self.registration_form invitation = self.invitation moderation_required = (regform.moderation_enabled and not _skip_moderation and (not invitation or not invitation.skip_moderation)) with db.session.no_autoflush: payment_required = regform.event_new.has_feature( 'payment') and self.price if self.state == RegistrationState.pending: if approved and payment_required: self.state = RegistrationState.unpaid elif approved: self.state = RegistrationState.complete elif rejected: self.state = RegistrationState.rejected elif self.state == RegistrationState.unpaid: if paid: self.state = RegistrationState.complete elif approved is False: self.state = RegistrationState.pending elif self.state == RegistrationState.complete: if approved is False and payment_required is False and moderation_required: self.state = RegistrationState.pending elif paid is False and payment_required: self.state = RegistrationState.unpaid if self.state != initial_state: signals.event.registration_state_updated.send( self, previous_state=initial_state)
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("(id != 0) OR (protection_mode != {})".format(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( JSON, nullable=False, default=lambda: None ) icon = db.deferred(db.Column( db.LargeBinary, nullable=True )) logo_metadata = db.Column( JSON, 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( JSON, 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 ) 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' ) # 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) # - 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 @return_ascii 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) @property def has_only_events(self): return self.has_events and not self.children @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.json_typeof(cat_alias.icon_metadata)), db.case({'null': cte_query.c.icon_metadata}, else_=cat_alias.icon_metadata, value=db.func.json_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) @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 Attachment(ProtectionMixin, VersionedResourceMixin, db.Model): __tablename__ = 'attachments' __table_args__ = ( # links: url but no file db.CheckConstraint( 'type != {} OR (link_url IS NOT NULL AND file_id IS NULL)'.format( AttachmentType.link.value), 'valid_link'), # we can't require the file_id to be NOT NULL for files because of the circular relationship... # but we can ensure that we never have both a file_id AND a link_url...for db.CheckConstraint('link_url IS NULL OR file_id IS NULL', 'link_or_file'), { 'schema': 'attachments' }) stored_file_table = 'attachments.files' stored_file_class = AttachmentFile stored_file_fkey = 'attachment_id' #: The ID of the attachment id = db.Column(db.Integer, primary_key=True) #: The ID of the folder the attachment belongs to folder_id = db.Column(db.Integer, db.ForeignKey('attachments.folders.id'), nullable=False, index=True) #: The ID of the user who created the attachment user_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=False) #: If the attachment has been deleted is_deleted = db.Column(db.Boolean, nullable=False, default=False) #: The name of the attachment title = db.Column(db.String, nullable=False) #: The description of the attachment description = db.Column(db.Text, nullable=False, default='') #: The date/time when the attachment was created/modified modified_dt = db.Column(UTCDateTime, nullable=False, default=now_utc, onupdate=now_utc) #: The type of the attachment (file or link) type = db.Column(PyIntEnum(AttachmentType), nullable=False) #: The target URL for a link attachment link_url = db.Column(db.String, nullable=True) #: The user who created the attachment user = db.relationship('User', lazy=True, backref=db.backref('attachments', lazy='dynamic')) #: The folder containing the attachment folder = db.relationship('AttachmentFolder', lazy=True, backref=db.backref('all_attachments', lazy=True)) acl_entries = db.relationship('AttachmentPrincipal', backref='attachment', cascade='all, delete-orphan', collection_class=set) #: The ACL of the folder (used for ProtectionMode.protected) acl = association_proxy('acl_entries', 'principal', creator=lambda v: AttachmentPrincipal(principal=v)) # relationship backrefs: # - legacy_mapping (LegacyAttachmentMapping.attachment) @property def protection_parent(self): return self.folder @property def locator(self): return dict(self.folder.locator, attachment_id=self.id) def get_download_url(self, absolute=False): """Returns the download url for the attachment. During static site generation this returns a local URL for the file or the target URL for the link. :param absolute: If the returned URL should be absolute. """ if g.get('static_site'): return _offline_download_url(self) else: filename = self.file.filename if self.type == AttachmentType.file else 'go' return url_for('attachments.download', self, filename=filename, _external=absolute) @property def download_url(self): """The download url for the attachment""" return self.get_download_url() @property def absolute_download_url(self): """The absolute download url for the attachment""" return self.get_download_url(absolute=True) def can_access(self, user, *args, **kwargs): """Checks if the user is allowed to access the attachment. This is the case if the user has access to see the attachment or if the user can manage attachments for the linked object. """ return (super(Attachment, self).can_access(user, *args, **kwargs) or can_manage_attachments(self.folder.object, user)) @return_ascii def __repr__(self): return '<Attachment({}, {}, {}{}, {}, {})>'.format( self.id, self.title, self.file if self.type == AttachmentType.file else self.link_url, ', is_deleted=True' if self.is_deleted else '', self.protection_repr, self.folder_id)
def own_room(cls): return db.relationship('Room', foreign_keys=[cls.own_room_id], lazy=True, backref=db.backref(cls.location_backref_name, lazy='dynamic'))
class EditingRevision(RenderModeMixin, db.Model): __tablename__ = 'revisions' __table_args__ = (db.CheckConstraint(_make_state_check(), name='valid_state_combination'), db.CheckConstraint( _make_reviewed_dt_check(), name='reviewed_dt_set_when_final_state'), { 'schema': 'event_editing' }) possible_render_modes = {RenderMode.markdown} default_render_mode = RenderMode.markdown id = db.Column(db.Integer, primary_key=True) editable_id = db.Column(db.ForeignKey('event_editing.editables.id'), index=True, nullable=False) submitter_id = db.Column(db.ForeignKey('users.users.id'), index=True, nullable=False) editor_id = db.Column(db.ForeignKey('users.users.id'), index=True, nullable=True) created_dt = db.Column(UTCDateTime, nullable=False, default=now_utc) reviewed_dt = db.Column(UTCDateTime, nullable=True) initial_state = db.Column(PyIntEnum(InitialRevisionState), nullable=False, default=InitialRevisionState.new) final_state = db.Column(PyIntEnum(FinalRevisionState), nullable=False, default=FinalRevisionState.none) _comment = db.Column('comment', db.Text, nullable=False, default='') editable = db.relationship('Editable', foreign_keys=editable_id, lazy=True, backref=db.backref( 'revisions', lazy=True, order_by=created_dt, cascade='all, delete-orphan')) submitter = db.relationship('User', lazy=True, foreign_keys=submitter_id, backref=db.backref('editing_revisions', lazy='dynamic')) editor = db.relationship('User', lazy=True, foreign_keys=editor_id, backref=db.backref('editor_for_revisions', lazy='dynamic')) tags = db.relationship('EditingTag', secondary='event_editing.revision_tags', collection_class=set, lazy=True, backref=db.backref('revisions', collection_class=set, lazy=True)) #: A comment provided by whoever assigned the final state of the revision. comment = RenderModeMixin.create_hybrid_property('_comment') # relationship backrefs: # - comments (EditingRevisionComment.revision) # - files (EditingRevisionFile.revision) def __repr__(self): return format_repr(self, 'id', 'editable_id', 'initial_state', final_state=FinalRevisionState.none) @locator_property def locator(self): return dict(self.editable.locator, revision_id=self.id) def get_spotlight_file(self): files = [file for file in self.files if file.file_type.publishable] return files[0] if len(files) == 1 else None def get_published_files(self): """Get the published files, grouped by file type.""" files = defaultdict(list) for file in self.files: if file.file_type.publishable: files[file.file_type].append(file) return dict(files)
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) @return_ascii 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 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 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 EventRole(db.Model): __tablename__ = 'roles' __table_args__ = (db.CheckConstraint('code = upper(code)', 'uppercase_code'), db.Index(None, 'event_id', 'code', unique=True), {'schema': 'events'}) is_group = False is_event_role = True is_category_role = False is_single_person = True is_network = False is_registration_form = False principal_order = 2 principal_type = PrincipalType.event_role id = db.Column( db.Integer, primary_key=True ) event_id = db.Column( db.Integer, db.ForeignKey('events.events.id'), nullable=False, index=True ) name = db.Column( db.String, nullable=False ) code = db.Column( db.String, nullable=False ) color = db.Column( db.String, nullable=False ) event = db.relationship( 'Event', lazy=True, backref=db.backref( 'roles', cascade='all, delete-orphan', lazy=True ) ) members = db.relationship( 'User', secondary='events.role_members', lazy=True, collection_class=set, backref=db.backref('event_roles', lazy=True, collection_class=set), ) # relationship backrefs: # - in_attachment_acls (AttachmentPrincipal.event_role) # - in_attachment_folder_acls (AttachmentFolderPrincipal.event_role) # - in_contribution_acls (ContributionPrincipal.event_role) # - in_event_acls (EventPrincipal.event_role) # - in_event_settings_acls (EventSettingPrincipal.event_role) # - in_session_acls (SessionPrincipal.event_role) # - in_track_acls (TrackPrincipal.event_role) def __contains__(self, user): return user is not None and self in user.event_roles def __repr__(self): return format_repr(self, 'id', 'code', _text=self.name) @locator_property def locator(self): return dict(self.event.locator, role_id=self.id) @property def identifier(self): return f'EventRole:{self.id}' @property def css(self): return 'color: #{0} !important; border-color: #{0} !important'.format(self.color) @property def style(self): return {'color': '#' + self.color, 'borderColor': '#' + self.color}
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) @return_ascii def __repr__(self): return format_repr(self, 'id', 'contribution_id', 'type') # TODO: state - either a column property referencing the newest revision's state or a normal column @locator_property def locator(self): return dict(self.contribution.locator, type=self.type.name) @property def event(self): return self.contribution.event def can_comment(self, user): return ( self.event.can_manage(user, permission=self.type.editor_permission) or self.contribution.is_user_associated(user, check_abstract=True)) @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)
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)
def event_new(cls): return db.relationship("Event", lazy=True, backref=db.backref(cls.settings_backref_name, lazy="dynamic"))
class RegistrationInvitation(db.Model): """An invitation for someone to register.""" __tablename__ = 'invitations' __table_args__ = (db.CheckConstraint('(state = {state}) OR (registration_id IS NULL)' .format(state=InvitationState.accepted), name='registration_state'), db.UniqueConstraint('registration_form_id', 'email'), {'schema': 'event_registration'}) #: The ID of the invitation id = db.Column( db.Integer, primary_key=True ) #: The UUID of the invitation uuid = db.Column( UUID, index=True, unique=True, nullable=False, default=lambda: str(uuid4()) ) #: The ID of the registration form registration_form_id = db.Column( db.Integer, db.ForeignKey('event_registration.forms.id'), index=True, nullable=False ) #: The ID of the registration (if accepted) registration_id = db.Column( db.Integer, db.ForeignKey('event_registration.registrations.id'), index=True, unique=True, nullable=True ) #: The state of the invitation state = db.Column( PyIntEnum(InvitationState), nullable=False, default=InvitationState.pending ) #: Whether registration moderation should be skipped skip_moderation = db.Column( db.Boolean, nullable=False, default=False ) #: The email of the invited person email = db.Column( db.String, nullable=False ) #: The first name of the invited person first_name = db.Column( db.String, nullable=False ) #: The last name of the invited person last_name = db.Column( db.String, nullable=False ) #: The affiliation of the invited person affiliation = db.Column( db.String, nullable=False ) #: The associated registration registration = db.relationship( 'Registration', lazy=True, backref=db.backref( 'invitation', lazy=True, uselist=False ) ) # relationship backrefs: # - registration_form (RegistrationForm.invitations) @locator_property def locator(self): return dict(self.registration_form.locator, invitation_id=self.id) @locator.uuid def locator(self): """A locator suitable for 'display' pages. Instead of the numeric ID it uses the UUID. """ assert self.uuid is not None return dict(self.registration_form.locator, invitation=self.uuid) def __repr__(self): full_name = f'{self.first_name} {self.last_name}' return format_repr(self, 'id', 'registration_form_id', 'email', 'state', _text=full_name)
class ReportLink(db.Model): """Display configuration data used in static links to report pages. This allows users to share links to report/listing pages in events while preserving e.g. column/filter configurations. """ __tablename__ = 'report_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( 'report_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 report_link = event.report_links.filter_by(type=type_, uuid=uuid).first() if report_link is None: return None report_link.last_used_dt = now_utc() return report_link.data @classmethod def create(cls, event, type_, data): """Create a new report 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 `ReportLink` """ report_link = event.report_links.filter_by(type=type_, data=data).first() if report_link is None: report_link = cls(event_new=event, type=type_, data=data) else: # bump timestamp in case we start expiring old links # in the future if report_link.last_used_dt is not None: report_link.last_used_dt = now_utc() else: report_link.created_dt = now_utc() db.session.flush() return report_link @return_ascii def __repr__(self): return format_repr(self, 'id', 'uuid')
def own_venue(cls): return db.relationship('Location', foreign_keys=[cls.own_venue_id], lazy=True, backref=db.backref(cls.location_backref_name, lazy='dynamic'))
def event(cls): return db.relationship('Event', lazy=True, backref=db.backref(cls.settings_backref_name, lazy='dynamic'))
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(JSONB) #: 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 = 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 locator(self): return {'event_id': self.event_id, 'id': self.id} 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=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 ServiceUnavailable( '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): definition = self.definition if definition is None: raise ServiceUnavailable( 'This agreement type is currently not available.') return definition.is_agreement_orphan(self.event, self)
class User(PersonMixin, db.Model): """Indico users.""" 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'), db.CheckConstraint( "(picture IS NULL) = (picture_metadata::text = 'null')", 'valid_picture'), { '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, index=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: str(uuid4())) #: the user profile picture picture = db.deferred(db.Column(db.LargeBinary, nullable=True)) #: user profile picture metadata picture_metadata = db.Column(JSONB, nullable=False, default=lambda: None) #: user profile picture source picture_source = db.Column( PyIntEnum(ProfilePictureSource), nullable=False, default=ProfilePictureSource.standard, ) _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', overlaps='_secondary_emails') _secondary_emails = db.relationship( 'UserEmail', lazy=True, cascade='all, delete-orphan', collection_class=set, primaryjoin='(User.id == UserEmail.user_id) & ~UserEmail.is_primary', overlaps='_primary_email') _all_emails = db.relationship('UserEmail', lazy=True, viewonly=True, sync_backref=False, 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', overlaps='old_api_keys') #: 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', overlaps='api_key') #: 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) # - category_roles (CategoryRole.members) # - 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_app_links (OAuthApplicationUserLink.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 f'User:{self.id}' @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 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} @property def last_login_dt(self): """The datetime when the user last logged in.""" if not self.identities: return None return max(self.identities, key=attrgetter('safe_last_login_dt')).last_login_dt @locator_property def locator(self): return {'user_id': self.id} @cached_property def settings(self): """Return 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 } @property def has_picture(self): return self.picture_metadata is not None @property def avatar_url(self): if self.is_system: return url_for('assets.image', filename='robot.svg') slug = self.picture_metadata[ 'hash'] if self.picture_metadata else 'default' return url_for('users.user_profile_picture_display', self, slug=slug) def __contains__(self, user): """Convenience method for `user in user_or_group`.""" return self == user 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().get_full_name(*args, **kwargs) def make_email_primary(self, email): """Promote 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') old = self.email self._primary_email.is_primary = False db.session.flush() secondary.is_primary = True db.session.flush() signals.users.primary_email_changed.send(self, old=old, new=email) def reset_signing_secret(self): self.signing_secret = str(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 def get_merged_from_users_recursive(self): """Get the users merged into this users recursively.""" user_alias = db.aliased(User) cte_query = (select([ user_alias.id.label('merged_from_id') ]).where(user_alias.merged_into_id == self.id).cte(recursive=True)) rec_query = (select([ user_alias.id ]).where(user_alias.merged_into_id == cte_query.c.merged_from_id)) cte = cte_query.union_all(rec_query) return User.query.join(cte, User.id == cte.c.merged_from_id).all()
class AttachmentFile(StoredFileMixin, db.Model): __tablename__ = 'files' __table_args__ = {'schema': 'attachments'} version_of = 'attachment' #: The ID of the file id = db.Column(db.Integer, primary_key=True) #: The ID of the associated attachment attachment_id = db.Column(db.Integer, db.ForeignKey('attachments.attachments.id'), nullable=False, index=True) #: The user who uploaded the file user_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), nullable=False, index=True) #: The user who uploaded the file user = db.relationship('User', lazy=True, backref=db.backref('attachment_files', lazy='dynamic')) # relationship backrefs: # - attachment (Attachment.all_files) @property def is_previewable(self): return get_file_previewer(self) is not None @no_autoflush def _build_storage_path(self): folder = self.attachment.folder assert folder.object is not None if folder.link_type == LinkType.category: # category/<id>/... path_segments = ['category', strict_unicode(folder.category.id)] else: # event/<id>/event/... path_segments = [ 'event', strict_unicode(folder.event.id), folder.link_type.name ] if folder.link_type == LinkType.session: # event/<id>/session/<session_id>/... path_segments.append(strict_unicode(folder.session.id)) elif folder.link_type == LinkType.contribution: # event/<id>/contribution/<contribution_id>/... path_segments.append(strict_unicode(folder.contribution.id)) elif folder.link_type == LinkType.subcontribution: # event/<id>/subcontribution/<subcontribution_id>/... path_segments.append(strict_unicode(folder.subcontribution.id)) self.attachment.assign_id() self.assign_id() filename = '{}-{}-{}'.format(self.attachment.id, self.id, self.filename) path = posixpath.join(*(path_segments + [filename])) return config.ATTACHMENT_STORAGE, path @return_ascii def __repr__(self): return '<AttachmentFile({}, {}, {}, {})>'.format( self.id, self.attachment_id, self.filename, self.content_type)