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 EventLogEntry(db.Model): """Log entries for events.""" __tablename__ = 'logs' __table_args__ = (db.Index(None, 'meta', postgresql_using='gin'), { '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) #: Non-displayable data meta = db.Column(JSONB, 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 = db.relationship('Event', lazy=True, backref=db.backref('log_entries', lazy='dynamic')) @property def logged_date(self): return self.logged_dt.astimezone(self.event.tzinfo).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): """Render 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 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 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(JSONB, 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)
class User(PersonMixin, db.Model): """Indico users""" # Useful when dealing with both users and groups in the same code is_group = False is_single_person = True is_event_role = False is_network = False principal_order = 0 principal_type = PrincipalType.user __tablename__ = 'users' __table_args__ = ( db.Index(None, 'is_system', unique=True, postgresql_where=db.text('is_system')), db.CheckConstraint( 'NOT is_system OR (NOT is_blocked AND NOT is_pending AND NOT is_deleted)', 'valid_system_user'), db.CheckConstraint('id != merged_into_id', 'not_merged_self'), db.CheckConstraint( "is_pending OR (first_name != '' AND last_name != '')", 'not_pending_proper_names'), { 'schema': 'users' }) #: the unique id of the user id = db.Column(db.Integer, primary_key=True) #: the first name of the user first_name = db.Column(db.String, nullable=False, index=True) #: the last/family name of the user last_name = db.Column(db.String, nullable=False, index=True) # the title of the user - you usually want the `title` property! _title = db.Column('title', PyIntEnum(UserTitle), nullable=False, default=UserTitle.none) #: the phone number of the user phone = db.Column(db.String, nullable=False, default='') #: the address of the user address = db.Column(db.Text, nullable=False, default='') #: the id of the user this user has been merged into merged_into_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), nullable=True) #: if the user is the default system user is_system = db.Column(db.Boolean, nullable=False, default=False) #: if the user is an administrator with unrestricted access to everything is_admin = db.Column(db.Boolean, nullable=False, default=False, index=True) #: if the user has been blocked is_blocked = db.Column(db.Boolean, nullable=False, default=False) #: if the user is pending (e.g. never logged in, only added to some list) is_pending = db.Column(db.Boolean, nullable=False, default=False) #: if the user is deleted (e.g. due to a merge) is_deleted = db.Column('is_deleted', db.Boolean, nullable=False, default=False) _affiliation = db.relationship('UserAffiliation', lazy=False, uselist=False, cascade='all, delete-orphan', backref=db.backref('user', lazy=True)) _primary_email = db.relationship( 'UserEmail', lazy=False, uselist=False, cascade='all, delete-orphan', primaryjoin='(User.id == UserEmail.user_id) & UserEmail.is_primary') _secondary_emails = db.relationship( 'UserEmail', lazy=True, cascade='all, delete-orphan', collection_class=set, primaryjoin='(User.id == UserEmail.user_id) & ~UserEmail.is_primary') _all_emails = db.relationship('UserEmail', lazy=True, viewonly=True, primaryjoin='User.id == UserEmail.user_id', collection_class=set, backref=db.backref('user', lazy=False)) #: the affiliation of the user affiliation = association_proxy('_affiliation', 'name', creator=lambda v: UserAffiliation(name=v)) #: the primary email address of the user email = association_proxy( '_primary_email', 'email', creator=lambda v: UserEmail(email=v, is_primary=True)) #: any additional emails the user might have secondary_emails = association_proxy('_secondary_emails', 'email', creator=lambda v: UserEmail(email=v)) #: all emails of the user. read-only; use it only for searching by email! also, do not use it between #: modifying `email` or `secondary_emails` and a session expire/commit! all_emails = association_proxy('_all_emails', 'email') # read-only! #: the user this user has been merged into merged_into_user = db.relationship( 'User', lazy=True, backref=db.backref('merged_from_users', lazy=True), remote_side='User.id', ) #: the users's favorite users favorite_users = db.relationship( 'User', secondary=favorite_user_table, primaryjoin=id == favorite_user_table.c.user_id, secondaryjoin=(id == favorite_user_table.c.target_id) & ~is_deleted, lazy=True, collection_class=set, backref=db.backref('favorite_of', lazy=True, collection_class=set), ) #: the users's favorite categories favorite_categories = db.relationship( 'Category', secondary=favorite_category_table, lazy=True, collection_class=set, backref=db.backref('favorite_of', lazy=True, collection_class=set), ) #: the user's category suggestions suggested_categories = db.relationship( 'SuggestedCategory', lazy='dynamic', order_by='SuggestedCategory.score.desc()', cascade='all, delete-orphan', backref=db.backref('user', lazy=True)) #: the active API key of the user api_key = db.relationship( 'APIKey', lazy=True, uselist=False, cascade='all, delete-orphan', primaryjoin='(User.id == APIKey.user_id) & APIKey.is_active', back_populates='user') #: the previous API keys of the user old_api_keys = db.relationship( 'APIKey', lazy=True, cascade='all, delete-orphan', order_by='APIKey.created_dt.desc()', primaryjoin='(User.id == APIKey.user_id) & ~APIKey.is_active', back_populates='user') #: the identities used by this user identities = db.relationship('Identity', lazy=True, cascade='all, delete-orphan', collection_class=set, backref=db.backref('user', lazy=False)) # relationship backrefs: # - _all_settings (UserSetting.user) # - abstract_comments (AbstractComment.user) # - abstract_email_log_entries (AbstractEmailLogEntry.user) # - abstract_reviewer_for_tracks (Track.abstract_reviewers) # - abstract_reviews (AbstractReview.user) # - abstracts (Abstract.submitter) # - agreements (Agreement.user) # - attachment_files (AttachmentFile.user) # - attachments (Attachment.user) # - blockings (Blocking.created_by_user) # - content_reviewer_for_contributions (Contribution.paper_content_reviewers) # - convener_for_tracks (Track.conveners) # - created_events (Event.creator) # - 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) # - global_abstract_reviewer_for_events (Event.global_abstract_reviewers) # - global_convener_for_events (Event.global_conveners) # - 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) # - judge_for_contributions (Contribution.paper_judges) # - judged_abstracts (Abstract.judge) # - judged_papers (PaperRevision.judge) # - layout_reviewer_for_contributions (Contribution.paper_layout_reviewers) # - local_groups (LocalGroup.members) # - merged_from_users (User.merged_into_user) # - modified_abstract_comments (AbstractComment.modified_by) # - modified_abstracts (Abstract.modified_by) # - modified_review_comments (PaperReviewComment.modified_by) # - oauth_tokens (OAuthToken.user) # - owned_rooms (Room.owner) # - paper_competences (PaperCompetence.user) # - paper_reviews (PaperReview.user) # - paper_revisions (PaperRevision.submitter) # - registrations (Registration.user) # - requests_created (Request.created_by_user) # - requests_processed (Request.processed_by_user) # - reservations (Reservation.created_by_user) # - reservations_booked_for (Reservation.booked_for_user) # - review_comments (PaperReviewComment.user) # - static_sites (StaticSite.creator) # - survey_submissions (SurveySubmission.user) # - vc_rooms (VCRoom.created_by_user) @staticmethod def get_system_user(): return User.query.filter_by(is_system=True).one() @property def as_principal(self): """The serializable principal identifier of this user""" return 'User', self.id @property def identifier(self): return 'User:{}'.format(self.id) @property def as_avatar(self): # TODO: remove this after DB is free of Avatars from indico.modules.users.legacy import AvatarUserWrapper avatar = AvatarUserWrapper(self.id) # avoid garbage collection avatar.user return avatar as_legacy = as_avatar @property def avatar_bg_color(self): from indico.modules.users.util import get_color_for_username return get_color_for_username(self.full_name) @property def avatar_css(self): return 'background-color: {};'.format(self.avatar_bg_color) @property def external_identities(self): """The external identities of the user""" return {x for x in self.identities if x.provider != 'indico'} @property def local_identities(self): """The local identities of the user""" return {x for x in self.identities if x.provider == 'indico'} @property def local_identity(self): """The main (most recently used) local identity""" identities = sorted(self.local_identities, key=attrgetter('safe_last_login_dt'), reverse=True) return identities[0] if identities else None @property def secondary_local_identities(self): """The local identities of the user except the main one""" return self.local_identities - {self.local_identity} @locator_property def locator(self): return {'user_id': self.id} @cached_property def settings(self): """Returns the user settings proxy for this user""" from indico.modules.users import user_settings return user_settings.bind(self) @property def synced_fields(self): """The fields of the user whose values are currently synced. This set is always a subset of the synced fields define in synced fields of the idp in 'indico.conf'. """ synced_fields = self.settings.get('synced_fields') # If synced_fields is missing or None, then all fields are synced if synced_fields is None: return multipass.synced_fields else: return set(synced_fields) & multipass.synced_fields @synced_fields.setter def synced_fields(self, value): value = set(value) & multipass.synced_fields if value == multipass.synced_fields: self.settings.delete('synced_fields') else: self.settings.set('synced_fields', list(value)) @property def synced_values(self): """The values from the synced identity for the user. Those values are not the actual user's values and might differ if they are not set as synchronized. """ identity = self._get_synced_identity(refresh=False) if identity is None: return {} return { field: (identity.data.get(field) or '') for field in multipass.synced_fields } def __contains__(self, user): """Convenience method for `user in user_or_group`.""" return self == user @return_ascii def __repr__(self): return format_repr(self, 'id', 'email', is_deleted=False, is_pending=False, _text=self.full_name) def can_be_modified(self, user): """If this user can be modified by the given user""" return self == user or user.is_admin def iter_identifiers(self, check_providers=False, providers=None): """Yields ``(provider, identifier)`` tuples for the user. :param check_providers: If True, providers are searched for additional identifiers once all existing identifiers have been yielded. :param providers: May be a set containing provider names to get only identifiers from the specified providers. """ done = set() for identity in self.identities: if providers is not None and identity.provider not in providers: continue item = (identity.provider, identity.identifier) done.add(item) yield item if not check_providers: return for identity_info in multipass.search_identities( providers=providers, exact=True, email=self.all_emails): item = (identity_info.provider.name, identity_info.identifier) if item not in done: yield item @property def can_get_all_multipass_groups(self): """Check whether it is possible to get all multipass groups the user is in.""" return all(multipass.identity_providers[ x.provider].supports_get_identity_groups for x in self.identities if x.provider != 'indico' and x.provider in multipass.identity_providers) def iter_all_multipass_groups(self): """Iterate over all multipass groups the user is in""" return itertools.chain.from_iterable( multipass.identity_providers[x.provider].get_identity_groups( x.identifier) for x in self.identities if x.provider != 'indico' and x.provider in multipass.identity_providers) def get_full_name(self, *args, **kwargs): kwargs['_show_empty_names'] = True return super(User, self).get_full_name(*args, **kwargs) def make_email_primary(self, email): """Promotes a secondary email address to the primary email address :param email: an email address that is currently a secondary email """ secondary = next( (x for x in self._secondary_emails if x.email == email), None) if secondary is None: raise ValueError('email is not a secondary email address') self._primary_email.is_primary = False db.session.flush() secondary.is_primary = True db.session.flush() def 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: try: identity_info = sync_provider.refresh_identity( identity.identifier, identity.multipass_data) except IdentityRetrievalFailed: identity_info = None if identity_info: identity.data = identity_info.data return identity
class 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(11, 2), # max. 999999999.99 nullable=False, default=0) #: The price modifier applied to the final calculated price price_adjustment = db.Column( db.Numeric(11, 2), # max. 999999999.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 = 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_publishable(self): return self.is_active and self.state in (RegistrationState.complete, RegistrationState.unpaid) @is_publishable.expression def is_publishable(cls): return cls.is_active & (cls.state.in_( (RegistrationState.complete, RegistrationState.unpaid))) @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 can_be_withdrawn(self): from indico.modules.events.registration.models.forms import ModificationMode if self.is_paid: return False elif self.event.end_dt < now_utc(): return False elif self.registration_form.modification_mode == ModificationMode.not_allowed: return False elif self.registration_form.modification_end_dt and self.registration_form.modification_end_dt < now_utc( ): return False else: return True @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_ticket_blocked(self): """Check whether the ticket is blocked by a plugin""" return any( values_from_signal(signals.event.is_ticket_blocked.send(self), single_value=True)) @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 payment_dt(self): """The date/time when the registration has been paid for""" return self.transaction.timestamp if self.is_paid else None @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.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, withdrawn=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, withdrawn)) > 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.has_feature('payment') and bool( 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 withdrawn: self.state = RegistrationState.withdrawn elif self.state == RegistrationState.unpaid: if paid: self.state = RegistrationState.complete elif approved is False: self.state = RegistrationState.pending elif withdrawn: self.state = RegistrationState.withdrawn 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 elif withdrawn: self.state = RegistrationState.withdrawn elif self.state == RegistrationState.rejected: if rejected is False and moderation_required: self.state = RegistrationState.pending elif rejected is False and payment_required: self.state = RegistrationState.unpaid elif rejected is False: self.state = RegistrationState.complete elif self.state == RegistrationState.withdrawn: if withdrawn is False and moderation_required: self.state = RegistrationState.pending elif withdrawn is False and payment_required: self.state = RegistrationState.unpaid elif withdrawn is False: self.state = RegistrationState.complete if self.state != initial_state: signals.event.registration_state_updated.send( self, previous_state=initial_state) def has_conflict(self): """Check if there are other valid registrations for the same user. This is intended for cases where this registration is currenly invalid (rejected or withdrawn) to determine whether it would be acceptable to restore it. """ conflict_criteria = [Registration.email == self.email] if self.user_id is not None: conflict_criteria.append(Registration.user_id == self.user_id) return (Registration.query.with_parent(self.registration_form).filter( Registration.id != self.id, db.or_(*conflict_criteria), Registration.state.notin_( [RegistrationState.rejected, RegistrationState.withdrawn])).has_rows())
def __table_args__(cls): return (db.Index('ix_uq_ip_network_groups_name_lower', db.func.lower(cls.name), unique=True), {'schema': 'indico'})
def __table_args__(cls): return (db.Index('ix_uq_reference_types_name_lower', db.func.lower(cls.name), unique=True), { 'schema': 'indico' })
class APIKey(db.Model): """API keys for users.""" __tablename__ = 'api_keys' __table_args__ = (db.Index(None, 'user_id', unique=True, postgresql_where=db.text('is_active')), { 'schema': 'users' }) #: api key id id = db.Column(db.Integer, primary_key=True) #: unique api key for a user token = db.Column(UUID, nullable=False, unique=True, default=lambda: str(uuid4())) #: secret key used for signed requests secret = db.Column(UUID, nullable=False, default=lambda: str(uuid4())) #: ID of the user associated with the key user_id = db.Column( db.Integer, db.ForeignKey('users.users.id'), nullable=False, index=True, ) #: if the key is the currently active key for the user is_active = db.Column(db.Boolean, nullable=False, default=True) #: if the key has been blocked by an admin is_blocked = db.Column(db.Boolean, nullable=False, default=False) #: if persistent signatures are allowed is_persistent_allowed = db.Column(db.Boolean, nullable=False, default=False) #: the time when the key has been created created_dt = db.Column(UTCDateTime, nullable=False, default=now_utc) #: the last time when the key has been used last_used_dt = db.Column(UTCDateTime, nullable=True) #: the last ip address from which the key has been used last_used_ip = db.Column(INET, nullable=True) #: the last URI this key was used with last_used_uri = db.Column(db.String, nullable=True) #: if the last use was from an authenticated request last_used_auth = db.Column(db.Boolean, nullable=True) #: the number of times the key has been used use_count = db.Column(db.Integer, nullable=False, default=0) #: the user associated with this API key user = db.relationship('User', lazy=False) def __repr__(self): return '<APIKey({}, {}, {})>'.format(self.token, self.user_id, self.last_used_dt or 'never') def register_used(self, ip, uri, authenticated): """Update the last used information.""" self.last_used_dt = now_utc() self.last_used_ip = ip self.last_used_uri = uri self.last_used_auth = authenticated self.use_count = APIKey.use_count + 1
def __auto_table_args(cls): return (db.Index(None, 'meta', postgresql_using='gin'), )
def make_fts_index(model, column_name, db_column_name=None): if db_column_name is None: db_column_name = column_name return db.Index(f'ix_{model.__tablename__}_{db_column_name}_fts', db.func.to_tsvector('simple', getattr(model, column_name)), postgresql_using='gin')
def __table_args__(cls): return (db.Index('ix_uq_applications_name_lower', db.func.lower(cls.name), unique=True), db.Index(None, cls.system_app_type, unique=True, postgresql_where=db.text('system_app_type != {}'.format(SystemAppType.none.value))), {'schema': 'oauth'})
class VCRoomEventAssociation(db.Model): __tablename__ = 'vc_room_events' __table_args__ = tuple(_make_checks()) + (db.Index( None, 'data', postgresql_using='gin'), { 'schema': 'events' }) #: Association ID id = db.Column(db.Integer, primary_key=True) #: ID of the event event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, autoincrement=False, nullable=False) #: ID of the videoconference room vc_room_id = db.Column(db.Integer, db.ForeignKey('events.vc_rooms.id'), index=True, nullable=False) #: Type of the object the vc_room is linked to link_type = db.Column(PyIntEnum(VCRoomLinkType), nullable=False) linked_event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=True) session_block_id = db.Column(db.Integer, db.ForeignKey('events.session_blocks.id'), index=True, nullable=True) contribution_id = db.Column(db.Integer, db.ForeignKey('events.contributions.id'), index=True, nullable=True) #: If the vc room should be shown on the event page show = db.Column(db.Boolean, nullable=False, default=False) #: videoconference plugin-specific data data = db.Column(JSONB, nullable=False) #: The associated :class:VCRoom vc_room = db.relationship('VCRoom', lazy=False, backref=db.backref('events', cascade='all, delete-orphan')) #: The associated Event event = db.relationship('Event', foreign_keys=event_id, lazy=True, backref=db.backref('all_vc_room_associations', lazy='dynamic')) #: The linked event (if the VC room is attached to the event itself) linked_event = db.relationship('Event', foreign_keys=linked_event_id, lazy=True, backref=db.backref('vc_room_associations', lazy=True)) #: The linked contribution (if the VC room is attached to a contribution) linked_contrib = db.relationship('Contribution', lazy=True, backref=db.backref('vc_room_associations', lazy=True)) #: The linked session block (if the VC room is attached to a block) linked_block = db.relationship('SessionBlock', lazy=True, backref=db.backref('vc_room_associations', lazy=True)) @classmethod def register_link_events(cls): event_mapping = { cls.linked_block: lambda x: x.event, cls.linked_contrib: lambda x: x.event, cls.linked_event: lambda x: x } type_mapping = { cls.linked_event: VCRoomLinkType.event, cls.linked_block: VCRoomLinkType.block, cls.linked_contrib: VCRoomLinkType.contribution } def _set_link_type(link_type, target, value, *unused): if value is not None: target.link_type = link_type def _set_event_obj(fn, target, value, *unused): if value is not None: event = fn(value) assert event is not None target.event = event for rel, fn in event_mapping.items(): if rel is not None: listen(rel, 'set', partial(_set_event_obj, fn)) for rel, link_type in type_mapping.items(): if rel is not None: listen(rel, 'set', partial(_set_link_type, link_type)) @property def locator(self): return dict(self.event.locator, service=self.vc_room.type, event_vc_room_id=self.id) @hybrid_property def link_object(self): if self.link_type == VCRoomLinkType.event: return self.linked_event elif self.link_type == VCRoomLinkType.contribution: return self.linked_contrib else: return self.linked_block @link_object.setter def link_object(self, obj): self.linked_event = self.linked_contrib = self.linked_block = None if isinstance(obj, db.m.Event): self.linked_event = obj elif isinstance(obj, db.m.Contribution): self.linked_contrib = obj elif isinstance(obj, db.m.SessionBlock): self.linked_block = obj else: raise TypeError(f'Unexpected object: {obj}') @link_object.comparator def link_object(cls): return _LinkObjectComparator(cls) def __repr__(self): return f'<VCRoomEventAssociation({self.event_id}, {self.vc_room})>' @classmethod def find_for_event(cls, event, include_hidden=False, include_deleted=False, only_linked_to_event=False, **kwargs): """Return 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 ``filter_by()`` """ 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): """Delete a VC room from an event. If the room is not used anywhere else, the room itself is also deleted. :param user: the user performing the deletion :param delete_all: if True, the room is detached from all events and deleted. """ vc_room = self.vc_room if delete_all: for assoc in vc_room.events[:]: Logger.get('modules.vc').info( "Detaching VC room {} from event {} ({})".format( vc_room, assoc.event, assoc.link_object)) vc_room.events.remove(assoc) else: Logger.get('modules.vc').info( "Detaching VC room {} from event {} ({})".format( vc_room, self.event, self.link_object)) vc_room.events.remove(self) db.session.flush() if vc_room.plugin and not vc_room.events: Logger.get('modules.vc').info(f"Deleting VC room {vc_room}") if vc_room.status != VCRoomStatus.deleted: vc_room.plugin.delete_room(vc_room, self.event) notify_deleted(vc_room.plugin, vc_room, self, self.event, user) db.session.delete(vc_room)
def __auto_table_args(): return (db.Index(None, 'category_id', 'module', 'name'), db.Index(None, 'category_id', 'module'), db.UniqueConstraint('category_id', 'module', 'name'), { 'schema': 'categories' })
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 Abstract(ProposalMixin, ProposalRevisionMixin, DescriptionMixin, CustomFieldsMixin, AuthorsSpeakersMixin, db.Model): """Represents 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('(state != {}) OR (uuid IS NOT NULL)'.format( AbstractState.invited), 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.itervalues()): return AbstractReviewingState.in_progress elif all(x == AbstractReviewingState.negative for x in track_states.itervalues()): return AbstractReviewingState.negative elif all(x in positiveish_states for x in track_states.itervalues()): 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.itervalues()): # 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 '#{} ({})'.format(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 @return_ascii 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)
def __table_args__(cls): return (db.Index('ix_uq_user_id_name_lower', cls.user_id, db.func.lower(cls.name), unique=True, postgresql_where=db.text('revoked_dt IS NULL')), {'schema': 'users'})
class RegistrationForm(db.Model): """A registration form for an event.""" __tablename__ = 'forms' principal_type = PrincipalType.registration_form principal_order = 2 __table_args__ = ( db.Index( 'ix_uq_forms_participation', 'event_id', unique=True, postgresql_where=db.text('is_participation AND NOT is_deleted')), db.UniqueConstraint( 'id', 'event_id'), # useless but needed for the registrations fkey db.CheckConstraint( 'publish_registrations_public <= publish_registrations_participants', name='publish_registrations_more_restrictive_to_public'), { 'schema': 'event_registration' }) #: The ID of the object id = db.Column(db.Integer, primary_key=True) #: The ID of the event event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False) #: The title of the registration form title = db.Column(db.String, nullable=False) #: Whether it's the 'Participants' form of a meeting/lecture is_participation = db.Column(db.Boolean, nullable=False, default=False) # An introduction text for users introduction = db.Column(db.Text, nullable=False, default='') #: Contact information for registrants contact_info = db.Column(db.String, nullable=False, default='') #: Datetime when the registration form is open start_dt = db.Column(UTCDateTime, nullable=True) #: Datetime when the registration form is closed end_dt = db.Column(UTCDateTime, nullable=True) #: Whether registration modifications are allowed modification_mode = db.Column(PyIntEnum(ModificationMode), nullable=False, default=ModificationMode.not_allowed) #: Datetime when the modification period is over modification_end_dt = db.Column(UTCDateTime, nullable=True) #: Whether the registration has been marked as deleted is_deleted = db.Column(db.Boolean, nullable=False, default=False) #: Whether users must be logged in to register require_login = db.Column(db.Boolean, nullable=False, default=False) #: Whether registrations must be associated with an Indico account require_user = db.Column(db.Boolean, nullable=False, default=False) #: Maximum number of registrations allowed registration_limit = db.Column(db.Integer, nullable=True) #: Which registrations should be displayed in the public participant list publish_registrations_public = db.Column( PyIntEnum(PublishRegistrationsMode), nullable=False, default=PublishRegistrationsMode.hide_all) #: Which registrations should be displayed in the private participant list publish_registrations_participants = db.Column( PyIntEnum(PublishRegistrationsMode), nullable=False, default=PublishRegistrationsMode.show_with_consent) #: For how long should the registrations be displayed after the event ends publish_registrations_duration = db.Column(db.Interval, nullable=True) #: Whether to display the number of registrations publish_registration_count = db.Column(db.Boolean, nullable=False, default=False) #: Whether checked-in status should be displayed in the event pages and participant list publish_checkin_enabled = db.Column(db.Boolean, nullable=False, default=False) #: Whether registrations must be approved by a manager moderation_enabled = db.Column(db.Boolean, nullable=False, default=False) #: The base fee users have to pay when registering base_price = db.Column( db.Numeric(11, 2), # max. 999999999.99 nullable=False, default=0) #: Currency for prices in the registration form currency = db.Column(db.String, nullable=False) #: Notifications sender address notification_sender_address = db.Column(db.String, nullable=True) #: Custom message to include in emails for pending registrations message_pending = db.Column(db.Text, nullable=False, default='') #: Custom message to include in emails for unpaid registrations message_unpaid = db.Column(db.Text, nullable=False, default='') #: Custom message to include in emails for complete registrations message_complete = db.Column(db.Text, nullable=False, default='') #: If the completed registration email should include the event's iCalendar file. attach_ical = db.Column(db.Boolean, nullable=False, default=False) #: Whether the manager notifications for this event are enabled manager_notifications_enabled = db.Column(db.Boolean, nullable=False, default=False) #: List of emails that should receive management notifications manager_notification_recipients = db.Column(ARRAY(db.String), nullable=False, default=[]) #: Whether tickets are enabled for this form tickets_enabled = db.Column(db.Boolean, nullable=False, default=False) #: Whether to send tickets by e-mail ticket_on_email = db.Column(db.Boolean, nullable=False, default=True) #: Whether to show a ticket download link on the event homepage ticket_on_event_page = db.Column(db.Boolean, nullable=False, default=True) #: Whether to show a ticket download link on the registration summary page ticket_on_summary_page = db.Column(db.Boolean, nullable=False, default=True) #: The ID of the template used to generate tickets ticket_template_id = db.Column(db.Integer, db.ForeignKey(DesignerTemplate.id), nullable=True, index=True) #: period for which the registration itself should be kept retention_period = db.Column(db.Interval, nullable=True) #: Whether the registrations have been deleted due to an expired retention period is_purged = db.Column(db.Boolean, default=False, nullable=False) #: The Event containing this registration form event = db.relationship( 'Event', lazy=True, backref=db.backref( 'registration_forms', primaryjoin= '(RegistrationForm.event_id == Event.id) & ~RegistrationForm.is_deleted', cascade='all, delete-orphan', lazy=True)) #: The template used to generate tickets ticket_template = db.relationship('DesignerTemplate', lazy=True, foreign_keys=ticket_template_id, backref=db.backref('ticket_for_regforms', lazy=True)) # The items (sections, text, fields) in the form form_items = db.relationship('RegistrationFormItem', lazy=True, cascade='all, delete-orphan', order_by='RegistrationFormItem.position', backref=db.backref('registration_form', lazy=True)) #: The registrations associated with this form registrations = db.relationship( 'Registration', lazy=True, cascade='all, delete-orphan', foreign_keys=[Registration.registration_form_id], backref=db.backref('registration_form', lazy=True)) #: The registration invitations associated with this form invitations = db.relationship('RegistrationInvitation', lazy=True, cascade='all, delete-orphan', backref=db.backref('registration_form', lazy=True)) # relationship backrefs: # - in_attachment_acls (AttachmentPrincipal.registration_form) # - in_attachment_folder_acls (AttachmentFolderPrincipal.registration_form) # - in_contribution_acls (ContributionPrincipal.registration_form) # - in_event_acls (EventPrincipal.registration_form) # - in_session_acls (SessionPrincipal.registration_form) def __contains__(self, user): if user is None: return False return (Registration.query.with_parent(self).join( Registration.registration_form).filter( Registration.user == user, Registration.state.in_( [RegistrationState.unpaid, RegistrationState.complete]), ~Registration.is_deleted, ~RegistrationForm.is_deleted).has_rows()) @property def name(self): # needed when sorting acl entries by name return self.title @property def identifier(self): return f'RegistrationForm:{self.id}' @hybrid_property def has_ended(self): return self.end_dt is not None and self.end_dt <= now_utc() @has_ended.expression def has_ended(cls): return cls.end_dt.isnot(None) & (cls.end_dt <= now_utc()) @hybrid_property def has_started(self): return self.start_dt is not None and self.start_dt <= now_utc() @has_started.expression def has_started(cls): return cls.start_dt.isnot(None) & (cls.start_dt <= now_utc()) @hybrid_property def is_modification_open(self): end_dt = self.modification_end_dt if self.modification_end_dt else self.end_dt return now_utc() <= end_dt if end_dt else True @is_modification_open.expression def is_modification_open(self): now = now_utc() return now <= db.func.coalesce(self.modification_end_dt, self.end_dt, now) @hybrid_property def is_open(self): return not self.is_deleted and self.has_started and not self.has_ended @is_open.expression def is_open(cls): return ~cls.is_deleted & cls.has_started & ~cls.has_ended @hybrid_property def is_scheduled(self): return not self.is_deleted and self.start_dt is not None @is_scheduled.expression def is_scheduled(cls): return ~cls.is_deleted & cls.start_dt.isnot(None) @property def locator(self): return dict(self.event.locator, reg_form_id=self.id) @property def active_fields(self): return [ field for field in self.form_items if field.is_field and field.is_active ] @property def active_labels(self): return [ field for field in self.form_items if field.is_label and field.is_active ] @property def sections(self): return [x for x in self.form_items if x.is_section] @property def active_sections(self): return [x for x in self.sections if x.is_visible and not x.is_deleted] @property def disabled_sections(self): return [ x for x in self.sections if not x.is_visible and not x.is_deleted ] @property def limit_reached(self): return (self.registration_limit and sum( reg.occupied_slots for reg in self.active_registrations) >= self.registration_limit) @property def is_active(self): return self.is_open and not self.limit_reached and not self.is_purged @property @memoize_request def active_registrations(self): return (Registration.query.with_parent(self).filter( Registration.is_active).options(subqueryload('data')).all()) @property def needs_publish_consent(self): return (self.publish_registrations_participants == PublishRegistrationsMode.show_with_consent or self.publish_registrations_public == PublishRegistrationsMode.show_with_consent) @hybrid_method def is_participant_list_visible(self, is_participant): if self.is_deleted: return False if is_participant: return self.publish_registrations_participants != PublishRegistrationsMode.hide_all return self.publish_registrations_public != PublishRegistrationsMode.hide_all @is_participant_list_visible.expression def is_participant_list_visible(cls, is_participant): if is_participant: return ~cls.is_deleted & (cls.publish_registrations_participants != PublishRegistrationsMode.hide_all) return ~cls.is_deleted & (cls.publish_registrations_public != PublishRegistrationsMode.hide_all) def __repr__(self): return f'<RegistrationForm({self.id}, {self.event_id}, {self.title})>' def is_modification_allowed(self, registration): """Check whether a registration may be modified.""" if not registration.is_active: return False elif self.modification_mode == ModificationMode.allowed_always: return True elif self.modification_mode == ModificationMode.allowed_until_approved: return registration.state == RegistrationState.pending elif self.modification_mode == ModificationMode.allowed_until_payment: return not registration.is_paid else: return False def can_submit(self, user): return self.is_active and (not self.require_login or user) @memoize_request def get_registration(self, user=None, uuid=None, email=None): """Retrieve registrations for this registration form by user or uuid.""" if (bool(user) + bool(uuid) + bool(email)) != 1: raise ValueError( 'Exactly one of `user`, `uuid` and `email` must be specified') if user: return user.registrations.filter_by(registration_form=self).filter( ~Registration.is_deleted).first() if uuid: try: UUID(hex=uuid) except ValueError: raise BadRequest('Malformed registration token') return Registration.query.with_parent(self).filter_by( uuid=uuid).filter(~Registration.is_deleted).first() if email: return Registration.query.with_parent(self).filter_by( email=email).filter(~Registration.is_deleted).first() def render_base_price(self): return format_currency(self.base_price, self.currency, locale=session.lang or 'en_GB') def get_personal_data_field_id(self, personal_data_type): """Return the field id corresponding to the personal data field with the given name.""" for field in self.active_fields: if (isinstance(field, RegistrationFormPersonalDataField) and field.personal_data_type == personal_data_type): return field.id
class RegistrationForm(db.Model): """A registration form for an event""" __tablename__ = 'forms' __table_args__ = ( db.Index( 'ix_uq_forms_participation', 'event_id', unique=True, postgresql_where=db.text('is_participation AND NOT is_deleted')), db.UniqueConstraint( 'id', 'event_id'), # useless but needed for the registrations fkey { 'schema': 'event_registration' }) #: The ID of the object id = db.Column(db.Integer, primary_key=True) #: The ID of the event event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False) #: The title of the registration form title = db.Column(db.String, nullable=False) is_participation = db.Column(db.Boolean, nullable=False, default=False) # An introduction text for users introduction = db.Column(db.Text, nullable=False, default='') #: Contact information for registrants contact_info = db.Column(db.String, nullable=False, default='') #: Datetime when the registration form is open start_dt = db.Column(UTCDateTime, nullable=True) #: Datetime when the registration form is closed end_dt = db.Column(UTCDateTime, nullable=True) #: Whether registration modifications are allowed modification_mode = db.Column(PyIntEnum(ModificationMode), nullable=False, default=ModificationMode.not_allowed) #: Datetime when the modification period is over modification_end_dt = db.Column(UTCDateTime, nullable=True) #: Whether the registration has been marked as deleted is_deleted = db.Column(db.Boolean, nullable=False, default=False) #: Whether users must be logged in to register require_login = db.Column(db.Boolean, nullable=False, default=False) #: Whether registrations must be associated with an Indico account require_user = db.Column(db.Boolean, nullable=False, default=False) #: Maximum number of registrations allowed registration_limit = db.Column(db.Integer, nullable=True) #: Whether registrations should be displayed in the participant list publish_registrations_enabled = db.Column(db.Boolean, nullable=False, default=False) #: Whether checked-in status should be displayed in the event pages and participant list publish_checkin_enabled = db.Column(db.Boolean, nullable=False, default=False) #: Whether registrations must be approved by a manager moderation_enabled = db.Column(db.Boolean, nullable=False, default=False) #: The base fee users have to pay when registering base_price = db.Column( db.Numeric(8, 2), # max. 999999.99 nullable=False, default=0) #: Currency for prices in the registration form currency = db.Column(db.String, nullable=False) #: Notifications sender address notification_sender_address = db.Column(db.String, nullable=True) #: Custom message to include in emails for pending registrations message_pending = db.Column(db.Text, nullable=False, default='') #: Custom message to include in emails for unpaid registrations message_unpaid = db.Column(db.Text, nullable=False, default='') #: Custom message to include in emails for complete registrations message_complete = db.Column(db.Text, nullable=False, default='') #: Whether the manager notifications for this event are enabled manager_notifications_enabled = db.Column(db.Boolean, nullable=False, default=False) #: List of emails that should receive management notifications manager_notification_recipients = db.Column(ARRAY(db.String), nullable=False, default=[]) #: Whether tickets are enabled for this form tickets_enabled = db.Column(db.Boolean, nullable=False, default=False) #: Whether to send tickets by e-mail ticket_on_email = db.Column(db.Boolean, nullable=False, default=True) #: Whether to show a ticket download link on the event homepage ticket_on_event_page = db.Column(db.Boolean, nullable=False, default=True) #: Whether to show a ticket download link on the registration summary page ticket_on_summary_page = db.Column(db.Boolean, nullable=False, default=True) #: The Event containing this registration form event_new = db.relationship('Event', lazy=True, backref=db.backref('registration_forms', lazy='dynamic')) # The items (sections, text, fields) in the form form_items = db.relationship('RegistrationFormItem', lazy=True, cascade='all, delete-orphan', order_by='RegistrationFormItem.position', backref=db.backref('registration_form', lazy=True)) #: The registrations associated with this form registrations = db.relationship( 'Registration', lazy=True, cascade='all, delete-orphan', foreign_keys=[Registration.registration_form_id], backref=db.backref('registration_form', lazy=True)) #: The registration invitations associated with this form invitations = db.relationship('RegistrationInvitation', lazy=True, cascade='all, delete-orphan', backref=db.backref('registration_form', lazy=True)) @hybrid_property def has_ended(self): return self.end_dt is not None and self.end_dt <= now_utc() @has_ended.expression def has_ended(cls): return cls.end_dt.isnot(None) & (cls.end_dt <= now_utc()) @hybrid_property def has_started(self): return self.start_dt is not None and self.start_dt <= now_utc() @has_started.expression def has_started(cls): return cls.start_dt.isnot(None) & (cls.start_dt <= now_utc()) @hybrid_property def is_modification_open(self): end_dt = self.modification_end_dt if self.modification_end_dt else self.end_dt return now_utc() <= end_dt if end_dt else True @is_modification_open.expression def is_modification_open(self): now = now_utc() return now <= db.func.coalesce(self.modification_end_dt, self.end_dt, now) @hybrid_property def is_open(self): return not self.is_deleted and self.has_started and not self.has_ended @is_open.expression def is_open(cls): return ~cls.is_deleted & cls.has_started & ~cls.has_ended @hybrid_property def is_scheduled(self): return not self.is_deleted and self.start_dt is not None @is_scheduled.expression def is_scheduled(cls): return ~cls.is_deleted & cls.start_dt.isnot(None) @property def locator(self): return dict(self.event_new.locator, reg_form_id=self.id) @property def active_fields(self): return [ field for field in self.form_items if (field.is_field and field.is_enabled and not field.is_deleted and field.parent.is_enabled and not field.parent.is_deleted) ] @property def sections(self): return [x for x in self.form_items if x.is_section] @property def limit_reached(self): return self.registration_limit and len( self.active_registrations) >= self.registration_limit @property def is_active(self): return self.is_open and not self.limit_reached @property def active_registrations(self): return [r for r in self.registrations if r.is_active] @property def sender_address(self): return self.notification_sender_address or self.event_new.as_legacy.getSupportInfo( ).getEmail() @return_ascii def __repr__(self): return '<RegistrationForm({}, {}, {})>'.format(self.id, self.event_id, self.title) def is_modification_allowed(self, registration): """Checks whether a registration may be modified""" if not registration.is_active: return False elif self.modification_mode == ModificationMode.allowed_always: return True elif self.modification_mode == ModificationMode.allowed_until_payment: return not registration.is_paid else: return False def can_submit(self, user): return self.is_active and (not self.require_login or user) @memoize_request def get_registration(self, user=None, uuid=None, email=None): """Retrieves registrations for this registration form by user or uuid""" if (bool(user) + bool(uuid) + bool(email)) != 1: raise ValueError( "Exactly one of `user`, `uuid` and `email` must be specified") if user: return user.registrations.filter_by(registration_form=self).filter( Registration.is_active).first() if uuid: try: UUID(hex=uuid) except ValueError: raise BadRequest('Malformed registration token') return Registration.query.with_parent(self).filter_by( uuid=uuid).filter(Registration.is_active).first() if email: return Registration.query.with_parent(self).filter_by( email=email).filter(Registration.is_active).first() def render_base_price(self): return format_currency(self.base_price, self.currency, locale=session.lang or 'en_GB') def get_personal_data_field_id(self, personal_data_type): """Returns the field id corresponding to the personal data field with the given name.""" for field in self.active_fields: if (isinstance(field, RegistrationFormPersonalDataField) and field.personal_data_type == personal_data_type): return field.id
class MenuEntry(MenuEntryMixin, db.Model): __tablename__ = 'menu_entries' __table_args__ = ( db.CheckConstraint( '(type IN ({type.internal_link.value}, {type.plugin_link.value}) AND name IS NOT NULL) OR ' '(type NOT IN ({type.internal_link.value}, {type.plugin_link.value}) and name IS NULL)' .format(type=MenuEntryType), 'valid_name'), db.CheckConstraint( '(type = {type.user_link.value}) = (link_url IS NOT NULL)'.format( type=MenuEntryType), 'valid_link_url'), db.CheckConstraint( '(type = {type.page.value} AND page_id IS NOT NULL) OR' ' (type != {type.page.value} AND page_id IS NULL)'.format( type=MenuEntryType), 'valid_page_id'), db.CheckConstraint( '(type = {type.plugin_link.value} AND plugin IS NOT NULL) OR' ' (type != {type.plugin_link.value} AND plugin IS NULL)'.format( type=MenuEntryType), 'valid_plugin'), db.CheckConstraint( '(type = {type.separator.value} AND title IS NULL) OR' ' (type IN ({type.user_link.value}, {type.page.value}) AND title IS NOT NULL) OR' ' (type NOT IN ({type.separator.value}, {type.user_link.value}, {type.page.value}))' .format(type=MenuEntryType), 'valid_title'), db.CheckConstraint("title != ''", 'title_not_empty'), db.Index( None, 'event_id', 'name', unique=True, postgresql_where=db.text( '(type = {type.internal_link.value} OR type = {type.plugin_link.value})' .format(type=MenuEntryType))), { 'schema': 'events' }) #: The ID of the menu entry id = db.Column(db.Integer, primary_key=True) #: The ID of the parent menu entry (NULL if root menu entry) parent_id = db.Column( db.Integer, db.ForeignKey('events.menu_entries.id'), index=True, nullable=True, ) #: The ID of the event which contains the menu event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False) #: Whether the entry is visible in the event's menu is_enabled = db.Column(db.Boolean, nullable=False, default=True) #: The title of the menu entry (to be displayed to the user) title = db.Column( db.String, nullable=True, ) #: The name of the menu entry (to uniquely identify a default entry for a given event) name = db.Column(db.String, nullable=True) #: The relative position of the entry in the menu position = db.Column(db.Integer, nullable=False, default=_get_next_position) #: Whether the menu entry should be opened in a new tab or window new_tab = db.Column(db.Boolean, nullable=False, default=False) #: The target URL of a custom link link_url = db.Column(db.String, nullable=True, default=None) #: The name of the plugin from which the entry comes from (NULL if the entry does not come from a plugin) plugin = db.Column(db.String, nullable=True) #: The page ID if the entry is a page page_id = db.Column(db.Integer, db.ForeignKey('events.pages.id'), nullable=True, index=True, default=None) #: The type of the menu entry type = db.Column(PyIntEnum(MenuEntryType), nullable=False) #: The Event containing the menu entry event_new = db.relationship('Event', lazy=True, backref=db.backref('menu_entries', lazy='dynamic')) #: The page of the menu entry page = db.relationship( 'EventPage', lazy=True, cascade='all, delete-orphan', single_parent=True, backref=db.backref('menu_entry', lazy=False, uselist=False), ) #: The children menu entries and parent backref children = db.relationship( 'MenuEntry', order_by='MenuEntry.position', backref=db.backref('parent', remote_side=[id]), ) # relationship backrefs: # - parent (MenuEntry.children) @property def is_root(self): return self.parent_id is None @staticmethod def get_for_event(event): return (MenuEntry.query.with_parent(event).filter( MenuEntry.parent_id.is_(None)).options( joinedload('children')).order_by(MenuEntry.position).all()) def move(self, to): from_ = self.position new_pos = to value = -1 if to is None or to < 0: new_pos = to = -1 if from_ > to: new_pos += 1 from_, to = to, from_ to -= 1 value = 1 entries = (MenuEntry.query.with_parent(self.event_new).filter( MenuEntry.parent == self.parent, MenuEntry.position.between(from_ + 1, to))) for e in entries: e.position += value self.position = new_pos def insert(self, parent, position): if position is None or position < 0: position = -1 old_siblings = (MenuEntry.query.with_parent(self.event_new).filter( MenuEntry.position > self.position, MenuEntry.parent == self.parent)) for sibling in old_siblings: sibling.position -= 1 new_siblings = (MenuEntry.query.with_parent(self.event_new).filter( MenuEntry.position > position, MenuEntry.parent == parent)) for sibling in new_siblings: sibling.position += 1 self.parent = parent self.position = position + 1
class EventReminder(db.Model): """Email reminders for events""" __tablename__ = 'reminders' __table_args__ = (db.Index(None, 'scheduled_dt', postgresql_where=db.text('not is_sent')), {'schema': 'events'}) #: The ID of the reminder 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 who created the reminder creator_id = db.Column( db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=False ) #: The date/time when the reminder was created created_dt = db.Column( UTCDateTime, nullable=False, default=now_utc ) #: The date/time when the reminder should be sent scheduled_dt = db.Column( UTCDateTime, nullable=False ) #: If the reminder has been sent is_sent = db.Column( db.Boolean, nullable=False, default=False ) #: How long before the event start the reminder should be sent #: This is needed to update the `scheduled_dt` when changing the #: start time of the event. event_start_delta = db.Column( db.Interval, nullable=True ) #: The recipients of the notification recipients = db.Column( ARRAY(db.String), nullable=False, default=[] ) #: If the notification should also be sent to all event participants send_to_participants = db.Column( db.Boolean, nullable=False, default=False ) #: If the notification should include a summary of the event's schedule. include_summary = db.Column( db.Boolean, nullable=False, default=False ) #: The address to use as Reply-To in the notification email. reply_to_address = db.Column( db.String, nullable=False ) #: Custom message to include in the email message = db.Column( db.String, nullable=False, default='' ) #: The user who created the reminder creator = db.relationship( 'User', lazy=True, backref=db.backref( 'event_reminders', lazy='dynamic' ) ) #: The Event this reminder is associated with event_new = db.relationship( 'Event', lazy=True, backref=db.backref( 'reminders', lazy='dynamic' ) ) @property def locator(self): return dict(self.event_new.locator, reminder_id=self.id) @property def all_recipients(self): """Returns all recipients of the notifications. This includes both explicit recipients and, if enabled, participants of the event. """ recipients = set(self.recipients) if self.send_to_participants: recipients.update(reg.email for reg in Registration.get_all_for_event(self.event_new)) recipients.discard('') # just in case there was an empty email address somewhere return recipients @hybrid_property def is_relative(self): """Returns if the reminder is relative to the event time""" return self.event_start_delta is not None @is_relative.expression def is_relative(self): return self.event_start_delta != None # NOQA @property def is_overdue(self): return not self.is_sent and self.scheduled_dt <= now_utc() def send(self): """Sends the reminder to its recipients.""" self.is_sent = True recipients = self.all_recipients if not recipients: logger.info('Notification %s has no recipients; not sending anything', self) return email_tpl = make_reminder_email(self.event_new, self.include_summary, self.message) email = make_email(bcc_list=recipients, from_address=self.reply_to_address, template=email_tpl) send_email(email, self.event_new, 'Reminder', self.creator) @return_ascii def __repr__(self): return format_repr(self, 'id', 'event_id', 'scheduled_dt', is_sent=False)
class Contribution(DescriptionMixin, ProtectionManagersMixin, LocationMixin, AttachedItemsMixin, AttachedNotesMixin, PersonLinkDataMixin, AuthorsSpeakersMixin, CustomFieldsMixin, db.Model): __tablename__ = 'contributions' __auto_table_args = ( db.Index(None, 'friendly_id', 'event_id', unique=True, postgresql_where=db.text('NOT is_deleted')), db.Index(None, 'event_id', 'track_id'), db.Index(None, 'event_id', 'abstract_id'), db.Index(None, 'abstract_id', unique=True, postgresql_where=db.text('NOT is_deleted')), db.CheckConstraint( "session_block_id IS NULL OR session_id IS NOT NULL", 'session_block_if_session'), db.ForeignKeyConstraint( ['session_block_id', 'session_id'], ['events.session_blocks.id', 'events.session_blocks.session_id']), { 'schema': 'events' }) location_backref_name = 'contributions' disallowed_protection_modes = frozenset() inheriting_have_acl = True possible_render_modes = {RenderMode.html, RenderMode.markdown} default_render_mode = RenderMode.markdown allow_relationship_preloading = True PRELOAD_EVENT_ATTACHED_ITEMS = True PRELOAD_EVENT_NOTES = True ATTACHMENT_FOLDER_ID_COLUMN = 'contribution_id' @classmethod def allocate_friendly_ids(cls, event, n): """Allocate n Contribution friendly_ids. This is needed so that we can allocate all IDs in one go. Not doing so could result in DB deadlocks. All operations that create more than one contribution should use this method. :param event: the :class:`Event` in question :param n: the number of ids to pre-allocate """ from indico.modules.events import Event fid = increment_and_get(Event._last_friendly_contribution_id, Event.id == event.id, n) friendly_ids = g.setdefault('friendly_ids', {}) friendly_ids.setdefault(cls, {})[event.id] = list( range(fid - n + 1, fid + 1)) @declared_attr def __table_args__(cls): return auto_table_args(cls) id = db.Column(db.Integer, primary_key=True) #: The human-friendly ID for the contribution friendly_id = db.Column(db.Integer, nullable=False, default=_get_next_friendly_id) event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False) session_id = db.Column(db.Integer, db.ForeignKey('events.sessions.id'), index=True, nullable=True) session_block_id = db.Column(db.Integer, db.ForeignKey('events.session_blocks.id'), index=True, nullable=True) track_id = db.Column(db.Integer, db.ForeignKey('events.tracks.id', ondelete='SET NULL'), index=True, nullable=True) abstract_id = db.Column(db.Integer, db.ForeignKey('event_abstracts.abstracts.id'), index=True, nullable=True) type_id = db.Column(db.Integer, db.ForeignKey('events.contribution_types.id'), index=True, nullable=True) title = db.Column(db.String, nullable=False) code = db.Column(db.String, nullable=False, default='') duration = db.Column(db.Interval, nullable=False) board_number = db.Column(db.String, nullable=False, default='') keywords = db.Column(ARRAY(db.String), nullable=False, default=[]) is_deleted = db.Column(db.Boolean, nullable=False, default=False) #: The last user-friendly sub-contribution ID _last_friendly_subcontribution_id = db.deferred( db.Column('last_friendly_subcontribution_id', db.Integer, nullable=False, default=0)) event = db.relationship( 'Event', lazy=True, backref=db.backref( 'contributions', primaryjoin= '(Contribution.event_id == Event.id) & ~Contribution.is_deleted', cascade='all, delete-orphan', lazy=True)) session = db.relationship( 'Session', lazy=True, backref=db.backref( 'contributions', primaryjoin= '(Contribution.session_id == Session.id) & ~Contribution.is_deleted', lazy=True)) session_block = db.relationship( 'SessionBlock', lazy=True, foreign_keys=[session_block_id], backref=db.backref( 'contributions', primaryjoin= '(Contribution.session_block_id == SessionBlock.id) & ~Contribution.is_deleted', lazy=True)) type = db.relationship('ContributionType', lazy=True, backref=db.backref('contributions', lazy=True)) acl_entries = db.relationship('ContributionPrincipal', lazy=True, cascade='all, delete-orphan', collection_class=set, backref='contribution') subcontributions = db.relationship( 'SubContribution', lazy=True, primaryjoin= '(SubContribution.contribution_id == Contribution.id) & ~SubContribution.is_deleted', order_by='SubContribution.position', cascade='all, delete-orphan', backref=db.backref( 'contribution', primaryjoin='SubContribution.contribution_id == Contribution.id', lazy=True)) abstract = db.relationship( 'Abstract', lazy=True, backref=db.backref( 'contribution', primaryjoin= '(Contribution.abstract_id == Abstract.id) & ~Contribution.is_deleted', lazy=True, uselist=False)) track = db.relationship( 'Track', lazy=True, backref=db.backref( 'contributions', primaryjoin= '(Contribution.track_id == Track.id) & ~Contribution.is_deleted', lazy=True, passive_deletes=True)) #: External references associated with this contribution references = db.relationship('ContributionReference', lazy=True, cascade='all, delete-orphan', backref=db.backref('contribution', lazy=True)) #: Persons associated with this contribution person_links = db.relationship('ContributionPersonLink', lazy=True, cascade='all, delete-orphan', backref=db.backref('contribution', lazy=True)) #: Data stored in abstract/contribution fields field_values = db.relationship('ContributionFieldValue', lazy=True, cascade='all, delete-orphan', backref=db.backref('contribution', lazy=True)) #: The accepted paper revision _accepted_paper_revision = db.relationship( 'PaperRevision', lazy=True, viewonly=True, uselist=False, primaryjoin= ('(PaperRevision._contribution_id == Contribution.id) & (PaperRevision.state == {})' .format(PaperRevisionState.accepted)), ) #: Paper files not submitted for reviewing pending_paper_files = db.relationship( 'PaperFile', lazy=True, viewonly=True, primaryjoin= '(PaperFile._contribution_id == Contribution.id) & (PaperFile.revision_id.is_(None))', ) #: Paper reviewing judges paper_judges = db.relationship('User', secondary='event_paper_reviewing.judges', collection_class=set, lazy=True, backref=db.backref( 'judge_for_contributions', collection_class=set, lazy=True)) #: Paper content reviewers paper_content_reviewers = db.relationship( 'User', secondary='event_paper_reviewing.content_reviewers', collection_class=set, lazy=True, backref=db.backref('content_reviewer_for_contributions', collection_class=set, lazy=True)) #: Paper layout reviewers paper_layout_reviewers = db.relationship( 'User', secondary='event_paper_reviewing.layout_reviewers', collection_class=set, lazy=True, backref=db.backref('layout_reviewer_for_contributions', collection_class=set, lazy=True)) @declared_attr def _paper_last_revision(cls): # Incompatible with joinedload subquery = (db.select([ db.func.max(PaperRevision.submitted_dt) ]).where(PaperRevision._contribution_id == cls.id).correlate_except( PaperRevision).as_scalar()) return db.relationship('PaperRevision', uselist=False, lazy=True, viewonly=True, primaryjoin=db.and_( PaperRevision._contribution_id == cls.id, PaperRevision.submitted_dt == subquery)) # relationship backrefs: # - _paper_files (PaperFile._contribution) # - _paper_revisions (PaperRevision._contribution) # - attachment_folders (AttachmentFolder.contribution) # - editables (Editable.contribution) # - legacy_mapping (LegacyContributionMapping.contribution) # - note (EventNote.contribution) # - room_reservation_links (ReservationLink.contribution) # - timetable_entry (TimetableEntry.contribution) # - vc_room_associations (VCRoomEventAssociation.linked_contrib) @declared_attr def is_scheduled(cls): from indico.modules.events.timetable.models.entries import TimetableEntry query = (db.exists([1]).where(TimetableEntry.contribution_id == cls.id).correlate_except(TimetableEntry)) return db.column_property(query, deferred=True) @declared_attr def subcontribution_count(cls): from indico.modules.events.contributions.models.subcontributions import SubContribution query = (db.select([db.func.count(SubContribution.id)]).where( (SubContribution.contribution_id == cls.id) & ~SubContribution.is_deleted).correlate_except(SubContribution)) return db.column_property(query, deferred=True) @declared_attr def _paper_revision_count(cls): query = (db.select([db.func.count(PaperRevision.id) ]).where(PaperRevision._contribution_id == cls.id).correlate_except(PaperRevision)) return db.column_property(query, deferred=True) def __init__(self, **kwargs): # explicitly initialize those relationships with None to avoid # an extra query to check whether there is an object associated # when assigning a new one (e.g. during cloning) kwargs.setdefault('note', None) kwargs.setdefault('timetable_entry', None) super().__init__(**kwargs) @classmethod def preload_acl_entries(cls, event): cls.preload_relationships(cls.query.with_parent(event), 'acl_entries') @property def location_parent(self): if self.session_block_id is not None: return self.session_block elif self.session_id is not None: return self.session else: return self.event @property def protection_parent(self): return self.session if self.session_id is not None else self.event @property def start_dt(self): return self.timetable_entry.start_dt if self.timetable_entry else None @property def end_dt(self): return self.timetable_entry.start_dt + self.duration if self.timetable_entry else None @property def start_dt_poster(self): if self.session and self.session.is_poster and self.timetable_entry and self.timetable_entry.parent: return self.timetable_entry.parent.start_dt @property def end_dt_poster(self): if self.session and self.session.is_poster and self.timetable_entry and self.timetable_entry.parent: return self.timetable_entry.parent.end_dt @property def duration_poster(self): if self.session and self.session.is_poster and self.timetable_entry and self.timetable_entry.parent: return self.timetable_entry.parent.duration @property def start_dt_display(self): """The displayed start time of the contribution. This is the start time of the poster session if applicable, otherwise the start time of the contribution itself. """ return self.start_dt_poster or self.start_dt @property def end_dt_display(self): """The displayed end time of the contribution. This is the end time of the poster session if applicable, otherwise the end time of the contribution itself. """ return self.end_dt_poster or self.end_dt @property def duration_display(self): """The displayed duration of the contribution. This is the duration of the poster session if applicable, otherwise the duration of the contribution itself. """ return self.duration_poster or self.duration @property def submitters(self): return { person_link for person_link in self.person_links if person_link.is_submitter } @locator_property def locator(self): return dict(self.event.locator, contrib_id=self.id) @property def verbose_title(self): return f'#{self.friendly_id} ({self.title})' @property def paper(self): return Paper(self) if self._paper_last_revision else None @property def allowed_types_for_editable(self): from indico.modules.events.editing.settings import editable_type_settings if not self.event.has_feature('editing'): return [] submitted_for = {editable.type.name for editable in self.editables} return [ editable_type for editable_type in self.event.editable_types if editable_type not in submitted_for and editable_type_settings[EditableType[editable_type]].get( self.event, 'submission_enabled') ] @property def enabled_editables(self): """Return all submitted editables with enabled types.""" from indico.modules.events.editing.settings import editing_settings if not self.event.has_feature('editing'): return [] enabled_editable_types = editing_settings.get(self.event, 'editable_types') enabled_editables = [ editable for editable in self.editables if editable.type.name in enabled_editable_types ] order = list(EditableType) return sorted(enabled_editables, key=lambda editable: order.index(editable.type)) @property def has_published_editables(self): return any(e.published_revision_id is not None for e in self.enabled_editables) def is_paper_reviewer(self, user): return user in self.paper_content_reviewers or user in self.paper_layout_reviewers def __repr__(self): return format_repr(self, 'id', is_deleted=False, _text=self.title) def can_manage(self, user, permission=None, allow_admin=True, check_parent=True, explicit_permission=False): if super().can_manage(user, permission, allow_admin=allow_admin, check_parent=check_parent, explicit_permission=explicit_permission): return True if (check_parent and self.session_id is not None and self.session.can_manage( user, 'coordinate', allow_admin=allow_admin, explicit_permission=explicit_permission) and session_coordinator_priv_enabled(self.event, 'manage-contributions')): return True return False def get_non_inheriting_objects(self): """Get a set of child objects that do not inherit protection.""" return get_non_inheriting_objects(self) def is_user_associated(self, user, check_abstract=False): if user is None: return False if check_abstract and self.abstract and self.abstract.submitter == user: return True return any(pl.person.user == user for pl in self.person_links if pl.person.user) def can_submit_proceedings(self, user): """Whether the user can submit editables/papers.""" if user is None: return False # The submitter of the original abstract is always authorized if self.abstract and self.abstract.submitter == user: return True # Otherwise only users with submission rights are authorized return self.can_manage(user, 'submit', allow_admin=False, check_parent=False) def get_editable(self, editable_type): """Get the editable of the given type.""" return next((e for e in self.editables if e.type == editable_type), None) def log(self, *args, **kwargs): """Log with prefilled metadata for the contribution.""" self.event.log(*args, meta={'contribution_id': self.id}, **kwargs)
class Session(DescriptionMixin, ColorMixin, ProtectionManagersMixin, LocationMixin, AttachedItemsMixin, AttachedNotesMixin, db.Model): __tablename__ = 'sessions' __auto_table_args = (db.Index(None, 'friendly_id', 'event_id', unique=True), {'schema': 'events'}) location_backref_name = 'sessions' disallowed_protection_modes = frozenset() inheriting_have_acl = True default_colors = ColorTuple('#202020', '#e3f2d3') allow_relationship_preloading = True PRELOAD_EVENT_ATTACHED_ITEMS = True PRELOAD_EVENT_NOTES = True ATTACHMENT_FOLDER_ID_COLUMN = 'session_id' possible_render_modes = {RenderMode.markdown} default_render_mode = RenderMode.markdown @declared_attr def __table_args__(cls): return auto_table_args(cls) id = db.Column( db.Integer, primary_key=True ) #: The human-friendly ID for the session friendly_id = db.Column( db.Integer, nullable=False, default=_get_next_friendly_id ) event_id = db.Column( db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False ) 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_poster = db.Column( db.Boolean, nullable=False, default=False ) is_deleted = db.Column( db.Boolean, nullable=False, default=False ) event_new = 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 ) ) # relationship backrefs: # - attachment_folders (AttachmentFolder.session) # - contributions (Contribution.session) # - legacy_mapping (LegacySessionMapping.session) # - note (EventNote.session) def __init__(self, **kwargs): # explicitly initialize this relationship with None to avoid # an extra query to check whether there is an object associated # when assigning a new one (e.g. during cloning) kwargs.setdefault('note', None) super(Session, self).__init__(**kwargs) @classmethod def preload_acl_entries(cls, event): cls.preload_relationships(cls.query.with_parent(event), 'acl_entries') @property def location_parent(self): return self.event_new @property def protection_parent(self): return self.event_new @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_new.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()) @locator_property def locator(self): return dict(self.event_new.locator, session_id=self.id) def get_non_inheriting_objects(self): """Get a set of child objects that do not inherit protection""" return get_non_inheriting_objects(self) @return_ascii def __repr__(self): return format_repr(self, 'id', is_poster=False, 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_new, '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_new, 'manage-blocks')): return True else: return False
class Contribution(DescriptionMixin, ProtectionManagersMixin, LocationMixin, AttachedItemsMixin, AttachedNotesMixin, PersonLinkDataMixin, AuthorsSpeakersMixin, CustomFieldsMixin, db.Model): __tablename__ = 'contributions' __auto_table_args = (db.Index(None, 'friendly_id', 'event_id', unique=True, postgresql_where=db.text('NOT is_deleted')), db.Index(None, 'event_id', 'track_id'), db.Index(None, 'event_id', 'abstract_id'), db.Index(None, 'abstract_id', unique=True, postgresql_where=db.text('NOT is_deleted')), db.CheckConstraint("session_block_id IS NULL OR session_id IS NOT NULL", 'session_block_if_session'), db.ForeignKeyConstraint(['session_block_id', 'session_id'], ['events.session_blocks.id', 'events.session_blocks.session_id']), {'schema': 'events'}) location_backref_name = 'contributions' disallowed_protection_modes = frozenset() inheriting_have_acl = True possible_render_modes = {RenderMode.html, RenderMode.markdown} default_render_mode = RenderMode.markdown allow_relationship_preloading = True PRELOAD_EVENT_ATTACHED_ITEMS = True PRELOAD_EVENT_NOTES = True ATTACHMENT_FOLDER_ID_COLUMN = 'contribution_id' @declared_attr def __table_args__(cls): return auto_table_args(cls) id = db.Column( db.Integer, primary_key=True ) #: The human-friendly ID for the contribution friendly_id = db.Column( db.Integer, nullable=False, default=_get_next_friendly_id ) event_id = db.Column( db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False ) session_id = db.Column( db.Integer, db.ForeignKey('events.sessions.id'), index=True, nullable=True ) session_block_id = db.Column( db.Integer, db.ForeignKey('events.session_blocks.id'), index=True, nullable=True ) track_id = db.Column( db.Integer, db.ForeignKey('events.tracks.id'), index=True, nullable=True ) abstract_id = db.Column( db.Integer, db.ForeignKey('event_abstracts.abstracts.id'), index=True, nullable=True ) type_id = db.Column( db.Integer, db.ForeignKey('events.contribution_types.id'), index=True, nullable=True ) title = db.Column( db.String, nullable=False ) duration = db.Column( db.Interval, nullable=False ) board_number = db.Column( db.String, nullable=False, default='' ) keywords = db.Column( ARRAY(db.String), nullable=False, default=[] ) is_deleted = db.Column( db.Boolean, nullable=False, default=False ) #: The last user-friendly sub-contribution ID _last_friendly_subcontribution_id = db.deferred(db.Column( 'last_friendly_subcontribution_id', db.Integer, nullable=False, default=0 )) event_new = db.relationship( 'Event', lazy=True, backref=db.backref( 'contributions', primaryjoin='(Contribution.event_id == Event.id) & ~Contribution.is_deleted', cascade='all, delete-orphan', lazy=True ) ) session = db.relationship( 'Session', lazy=True, backref=db.backref( 'contributions', primaryjoin='(Contribution.session_id == Session.id) & ~Contribution.is_deleted', lazy=True ) ) session_block = db.relationship( 'SessionBlock', lazy=True, foreign_keys=[session_block_id], backref=db.backref( 'contributions', primaryjoin='(Contribution.session_block_id == SessionBlock.id) & ~Contribution.is_deleted', lazy=True ) ) type = db.relationship( 'ContributionType', lazy=True, backref=db.backref( 'contributions', lazy=True ) ) acl_entries = db.relationship( 'ContributionPrincipal', lazy=True, cascade='all, delete-orphan', collection_class=set, backref='contribution' ) subcontributions = db.relationship( 'SubContribution', lazy=True, primaryjoin='(SubContribution.contribution_id == Contribution.id) & ~SubContribution.is_deleted', order_by='SubContribution.position', cascade='all, delete-orphan', backref=db.backref( 'contribution', primaryjoin='SubContribution.contribution_id == Contribution.id', lazy=True ) ) abstract = db.relationship( 'Abstract', lazy=True, backref=db.backref( 'contribution', primaryjoin='(Contribution.abstract_id == Abstract.id) & ~Contribution.is_deleted', lazy=True, uselist=False ) ) track = db.relationship( 'Track', lazy=True, backref=db.backref( 'contributions', primaryjoin='(Contribution.track_id == Track.id) & ~Contribution.is_deleted', lazy=True ) ) #: External references associated with this contribution references = db.relationship( 'ContributionReference', lazy=True, cascade='all, delete-orphan', backref=db.backref( 'contribution', lazy=True ) ) #: Persons associated with this contribution person_links = db.relationship( 'ContributionPersonLink', lazy=True, cascade='all, delete-orphan', backref=db.backref( 'contribution', lazy=True ) ) #: Data stored in abstract/contribution fields field_values = db.relationship( 'ContributionFieldValue', lazy=True, cascade='all, delete-orphan', backref=db.backref( 'contribution', lazy=True ) ) #: The accepted paper revision _accepted_paper_revision = db.relationship( 'PaperRevision', lazy=True, viewonly=True, uselist=False, primaryjoin=('(PaperRevision._contribution_id == Contribution.id) & (PaperRevision.state == {})' .format(PaperRevisionState.accepted)), ) #: Paper files not submitted for reviewing pending_paper_files = db.relationship( 'PaperFile', lazy=True, viewonly=True, primaryjoin='(PaperFile._contribution_id == Contribution.id) & (PaperFile.revision_id.is_(None))', ) #: Paper reviewing judges paper_judges = db.relationship( 'User', secondary='event_paper_reviewing.judges', collection_class=set, lazy=True, backref=db.backref( 'judge_for_contributions', collection_class=set, lazy=True ) ) #: Paper content reviewers paper_content_reviewers = db.relationship( 'User', secondary='event_paper_reviewing.content_reviewers', collection_class=set, lazy=True, backref=db.backref( 'content_reviewer_for_contributions', collection_class=set, lazy=True ) ) #: Paper layout reviewers paper_layout_reviewers = db.relationship( 'User', secondary='event_paper_reviewing.layout_reviewers', collection_class=set, lazy=True, backref=db.backref( 'layout_reviewer_for_contributions', collection_class=set, lazy=True ) ) @declared_attr def _paper_last_revision(cls): # Incompatible with joinedload subquery = (db.select([db.func.max(PaperRevision.submitted_dt)]) .where(PaperRevision._contribution_id == cls.id) .correlate_except(PaperRevision) .as_scalar()) return db.relationship( 'PaperRevision', uselist=False, lazy=True, viewonly=True, primaryjoin=db.and_(PaperRevision._contribution_id == cls.id, PaperRevision.submitted_dt == subquery) ) # relationship backrefs: # - _paper_files (PaperFile._contribution) # - _paper_revisions (PaperRevision._contribution) # - attachment_folders (AttachmentFolder.contribution) # - legacy_mapping (LegacyContributionMapping.contribution) # - note (EventNote.contribution) # - timetable_entry (TimetableEntry.contribution) # - vc_room_associations (VCRoomEventAssociation.linked_contrib) @declared_attr def is_scheduled(cls): from indico.modules.events.timetable.models.entries import TimetableEntry query = (db.exists([1]) .where(TimetableEntry.contribution_id == cls.id) .correlate_except(TimetableEntry)) return db.column_property(query, deferred=True) @declared_attr def subcontribution_count(cls): from indico.modules.events.contributions.models.subcontributions import SubContribution query = (db.select([db.func.count(SubContribution.id)]) .where((SubContribution.contribution_id == cls.id) & ~SubContribution.is_deleted) .correlate_except(SubContribution)) return db.column_property(query, deferred=True) @declared_attr def _paper_revision_count(cls): query = (db.select([db.func.count(PaperRevision.id)]) .where(PaperRevision._contribution_id == cls.id) .correlate_except(PaperRevision)) return db.column_property(query, deferred=True) def __init__(self, **kwargs): # explicitly initialize those relationships with None to avoid # an extra query to check whether there is an object associated # when assigning a new one (e.g. during cloning) kwargs.setdefault('note', None) kwargs.setdefault('timetable_entry', None) super(Contribution, self).__init__(**kwargs) @classmethod def preload_acl_entries(cls, event): cls.preload_relationships(cls.query.with_parent(event), 'acl_entries') @property def location_parent(self): if self.session_block_id is not None: return self.session_block elif self.session_id is not None: return self.session else: return self.event_new @property def protection_parent(self): return self.session if self.session_id is not None else self.event_new @property def start_dt(self): return self.timetable_entry.start_dt if self.timetable_entry else None @property def end_dt(self): return self.timetable_entry.start_dt + self.duration if self.timetable_entry else None @property def submitters(self): return {person_link for person_link in self.person_links if person_link.is_submitter} @locator_property def locator(self): return dict(self.event_new.locator, contrib_id=self.id) @property def verbose_title(self): return '#{} ({})'.format(self.friendly_id, self.title) @property def paper(self): return Paper(self) if self._paper_last_revision else None def is_paper_reviewer(self, user): return user in self.paper_content_reviewers or user in self.paper_layout_reviewers @return_ascii def __repr__(self): return format_repr(self, 'id', is_deleted=False, _text=self.title) def can_manage(self, user, role=None, allow_admin=True, check_parent=True, explicit_role=False): if super(Contribution, self).can_manage(user, role, allow_admin=allow_admin, check_parent=check_parent, explicit_role=explicit_role): return True if (check_parent and self.session_id is not None and self.session.can_manage(user, 'coordinate', allow_admin=allow_admin, explicit_role=explicit_role) and session_coordinator_priv_enabled(self.event_new, 'manage-contributions')): return True return False def get_non_inheriting_objects(self): """Get a set of child objects that do not inherit protection.""" return get_non_inheriting_objects(self) def is_user_associated(self, user, check_abstract=False): if user is None: return False if check_abstract and self.abstract and self.abstract.submitter == user: return True return any(pl.person.user == user for pl in self.person_links if pl.person.user)
def __table_args__(cls): return (db.Index('ix_uq_groups_name_lower', db.func.lower(cls.name), unique=True), { 'schema': 'users' })
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) 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(SubContribution, self).__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 = 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): return self.contribution.get_manager_list(recursive=recursive) @return_ascii 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, role=None, **kwargs): return self.contribution.can_manage(user, role, **kwargs)
def __table_args__(cls): return (db.Index('ix_uq_labels_title_lower', db.func.lower(cls.title), unique=True), { 'schema': 'events' })
class PaperRevision(ProposalRevisionMixin, RenderModeMixin, db.Model): __tablename__ = 'revisions' __table_args__ = ( db.Index(None, 'contribution_id', unique=True, postgresql_where=db.text('state = {}'.format( PaperRevisionState.accepted))), db.CheckConstraint( '(state IN ({}, {}, {})) = (judge_id IS NOT NULL)'.format( PaperRevisionState.accepted, PaperRevisionState.rejected, PaperRevisionState.to_be_corrected), name='judge_if_judged'), db.CheckConstraint( '(state IN ({}, {}, {})) = (judgment_dt IS NOT NULL)'.format( PaperRevisionState.accepted, PaperRevisionState.rejected, PaperRevisionState.to_be_corrected), name='judgment_dt_if_judged'), { 'schema': 'event_paper_reviewing' }) possible_render_modes = {RenderMode.markdown} default_render_mode = RenderMode.markdown proposal_attr = 'paper' id = db.Column(db.Integer, primary_key=True) state = db.Column(PyIntEnum(PaperRevisionState), nullable=False, default=PaperRevisionState.submitted) _contribution_id = db.Column('contribution_id', db.Integer, db.ForeignKey('events.contributions.id'), index=True, nullable=False) submitter_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=False) submitted_dt = db.Column(UTCDateTime, nullable=False, default=now_utc) judge_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=True) judgment_dt = db.Column(UTCDateTime, nullable=True) _judgment_comment = db.Column('judgment_comment', db.Text, nullable=False, default='') _contribution = db.relationship('Contribution', lazy=True, backref=db.backref( '_paper_revisions', lazy=True, order_by=submitted_dt.asc())) submitter = db.relationship('User', lazy=True, foreign_keys=submitter_id, backref=db.backref('paper_revisions', lazy='dynamic')) judge = db.relationship('User', lazy=True, foreign_keys=judge_id, backref=db.backref('judged_papers', lazy='dynamic')) judgment_comment = RenderModeMixin.create_hybrid_property( '_judgment_comment') # relationship backrefs: # - comments (PaperReviewComment.paper_revision) # - files (PaperFile.paper_revision) # - reviews (PaperReview.revision) def __init__(self, *args, **kwargs): paper = kwargs.pop('paper', None) if paper: kwargs.setdefault('_contribution', paper.contribution) super(PaperRevision, self).__init__(*args, **kwargs) @return_ascii def __repr__(self): return format_repr(self, 'id', '_contribution_id', state=None) @locator_property def locator(self): return dict(self.paper.locator, revision_id=self.id) @property def paper(self): return self._contribution.paper @property def is_last_revision(self): return self == self.paper.last_revision @property def number(self): return self.paper.revisions.index(self) + 1 @paper.setter def paper(self, paper): self._contribution = paper.contribution def get_timeline(self, user=None): comments = [x for x in self.comments if x.can_view(user)] if user else self.comments reviews = [x for x in self.reviews if x.can_view(user)] if user else self.reviews judgment = [ PaperJudgmentProxy(self) ] if self.state == PaperRevisionState.to_be_corrected else [] return sorted(chain(comments, reviews, judgment), key=attrgetter('created_dt')) def get_reviews(self, group=None, user=None): reviews = [] if user and group: reviews = [ x for x in self.reviews if x.group.instance == group and x.user == user ] elif user: reviews = [x for x in self.reviews if x.user == user] elif group: reviews = [x for x in self.reviews if x.group.instance == group] return reviews def get_reviewed_for_groups(self, user, include_reviewed=False): from indico.modules.events.papers.models.reviews import PaperTypeProxy reviewed_for = {x.type for x in self.reviews if x.user == user} if include_reviewed else set() if self.paper.cfp.content_reviewing_enabled and user in self.paper.cfp.content_reviewers: reviewed_for.add(PaperReviewType.content) if self.paper.cfp.layout_reviewing_enabled and user in self.paper.cfp.layout_reviewers: reviewed_for.add(PaperReviewType.layout) return set(map(PaperTypeProxy, reviewed_for)) def has_user_reviewed(self, user, review_type=None): from indico.modules.events.papers.models.reviews import PaperReviewType if review_type: if isinstance(review_type, basestring): review_type = PaperReviewType[review_type] return any(review.user == user and review.type == review_type for review in self.reviews) else: layout_review = next( (review for review in self.reviews if review.user == user and review.type == PaperReviewType.layout), None) content_review = next( (review for review in self.reviews if review.user == user and review.type == PaperReviewType.content), None) if user in self._contribution.paper_layout_reviewers and user in self._contribution.paper_content_reviewers: return bool(layout_review and content_review) elif user in self._contribution.paper_layout_reviewers: return bool(layout_review) elif user in self._contribution.paper_content_reviewers: return bool(content_review) def get_spotlight_file(self): pdf_files = [ paper_file for paper_file in self.files if paper_file.content_type == 'application/pdf' ] return pdf_files[0] if len(pdf_files) == 1 else None
def __auto_table_args(): return (db.Index(None, 'event_id', 'module', 'name'), db.Index(None, 'event_id', 'module'), {'schema': 'events'})
class CategoryRole(db.Model): __tablename__ = 'roles' __table_args__ = (db.CheckConstraint('code = upper(code)', 'uppercase_code'), db.Index(None, 'category_id', 'code', unique=True), { 'schema': 'categories' }) principal_order = 2 principal_type = PrincipalType.category_role id = db.Column(db.Integer, primary_key=True) category_id = db.Column(db.Integer, db.ForeignKey('categories.categories.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) category = db.relationship('Category', lazy=True, backref=db.backref('roles', cascade='all, delete-orphan', lazy=True)) members = db.relationship( 'User', secondary='categories.role_members', lazy=True, collection_class=set, backref=db.backref('category_roles', lazy=True, collection_class=set), ) # relationship backrefs: # - in_attachment_acls (AttachmentPrincipal.category_role) # - in_attachment_folder_acls (AttachmentFolderPrincipal.category_role) # - in_category_acls (CategoryPrincipal.category_role) # - in_contribution_acls (ContributionPrincipal.category_role) # - in_event_acls (EventPrincipal.category_role) # - in_event_settings_acls (EventSettingPrincipal.category_role) # - in_session_acls (SessionPrincipal.category_role) # - in_track_acls (TrackPrincipal.category_role) def __contains__(self, user): return user is not None and self in user.category_roles def __repr__(self): return format_repr(self, 'id', 'code', _text=self.name) @locator_property def locator(self): return dict(self.category.locator, role_id=self.id) @property def identifier(self): return f'CategoryRole:{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} @staticmethod def get_category_roles(cat): """Get the category roles available for the specified category.""" return CategoryRole.query.join(cat.chain_query.subquery()).order_by( CategoryRole.code).all() @staticmethod def get_category_role_by_id(cat, id): """ Get a category role in the context of the specified category. If the role is not defined in the category or one of its parents, it is considered non-existing. """ return CategoryRole.query.filter_by(id=id).join( cat.chain_query.subquery()).first()
class EventReminder(db.Model): """Email reminders for events.""" __tablename__ = 'reminders' __table_args__ = (db.Index(None, 'scheduled_dt', postgresql_where=db.text('not is_sent')), {'schema': 'events'}) #: The ID of the reminder 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 who created the reminder creator_id = db.Column( db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=False ) #: The date/time when the reminder was created created_dt = db.Column( UTCDateTime, nullable=False, default=now_utc ) #: The date/time when the reminder should be sent scheduled_dt = db.Column( UTCDateTime, nullable=False ) #: If the reminder has been sent is_sent = db.Column( db.Boolean, nullable=False, default=False ) #: How long before the event start the reminder should be sent #: This is needed to update the `scheduled_dt` when changing the #: start time of the event. event_start_delta = db.Column( db.Interval, nullable=True ) #: The recipients of the notification recipients = db.Column( ARRAY(db.String), nullable=False, default=[] ) #: If the notification should also be sent to all event participants send_to_participants = db.Column( db.Boolean, nullable=False, default=False ) #: If the notification should also be sent to all event speakers send_to_speakers = db.Column( db.Boolean, nullable=False, default=False ) #: If the notification should include a summary of the event's schedule. include_summary = db.Column( db.Boolean, nullable=False, default=False ) #: If the notification should include the event's description. include_description = db.Column( db.Boolean, nullable=False, default=False ) #: If the notification should include the event's iCalendar file. attach_ical = db.Column( db.Boolean, nullable=False, default=True ) #: The address to use as Reply-To in the notification email. reply_to_address = db.Column( db.String, nullable=False ) #: Custom message to include in the email message = db.Column( db.String, nullable=False, default='' ) #: The user who created the reminder creator = db.relationship( 'User', lazy=True, backref=db.backref( 'event_reminders', lazy='dynamic' ) ) #: The Event this reminder is associated with event = db.relationship( 'Event', lazy=True, backref=db.backref( 'reminders', lazy='dynamic' ) ) @property def locator(self): return dict(self.event.locator, reminder_id=self.id) @property def all_recipients(self): """Return all recipients of the notifications. This includes both explicit recipients and, if enabled, participants/speakers of the event. """ recipients = set(self.recipients) if self.send_to_participants: recipients.update(reg.email for reg in Registration.get_all_for_event(self.event)) if self.send_to_speakers: recipients.update(person_link.email for person_link in self.event.person_links) # contribution/sub-contribution speakers are present only in meetings and conferences if self.event.type != EventType.lecture: contrib_speakers = ( ContributionPersonLink.query .filter( ContributionPersonLink.is_speaker, ContributionPersonLink.contribution.has(is_deleted=False, event=self.event) ) .all() ) subcontrib_speakers = ( SubContributionPersonLink.query .filter( SubContributionPersonLink.is_speaker, SubContributionPersonLink.subcontribution.has( db.and_( ~SubContribution.is_deleted, SubContribution.contribution.has(is_deleted=False, event=self.event) ) ) ) .all() ) recipients.update(speaker.email for speaker in contrib_speakers) recipients.update(speaker.email for speaker in subcontrib_speakers) recipients.discard('') # just in case there was an empty email address somewhere return recipients @hybrid_property def is_relative(self): """Return if the reminder is relative to the event time.""" return self.event_start_delta is not None @is_relative.expression def is_relative(self): return self.event_start_delta.isnot(None) @property def is_overdue(self): return not self.is_sent and self.scheduled_dt <= now_utc() def _make_email(self, recipient, template, attachments): email_params = { 'to_list': recipient, 'from_address': self.reply_to_address, 'template': template, 'attachments': attachments } extra_params = signals.event.reminder.before_reminder_make_email.send(self, **email_params) for param in values_from_signal(extra_params, as_list=True): email_params.update(param) return make_email(**email_params) def send(self): """Send the reminder to its recipients.""" self.is_sent = True recipients = self.all_recipients if not recipients: logger.info('Notification %s has no recipients; not sending anything', self) return email_tpl = make_reminder_email(self.event, self.include_summary, self.include_description, self.message) attachments = [] if self.attach_ical: event_ical = event_to_ical(self.event, skip_access_check=True, method='REQUEST', organizer=(core_settings.get('site_title'), config.NO_REPLY_EMAIL)) attachments.append(MIMECalendar('event.ics', event_ical)) for recipient in recipients: email = self._make_email(recipient, email_tpl, attachments) send_email(email, self.event, 'Reminder', self.creator, log_metadata={'reminder_id': self.id}) def __repr__(self): return format_repr(self, 'id', 'event_id', 'scheduled_dt', is_sent=False)