def is_deleted(cls): return db.Column(db.Boolean, nullable=False, default=False)
class ContributionField(db.Model): __tablename__ = 'contribution_fields' __table_args__ = (db.UniqueConstraint('event_id', 'legacy_id'), { 'schema': 'events' }) id = db.Column(db.Integer, primary_key=True) event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False) legacy_id = db.Column(db.String, nullable=True) position = db.Column(db.Integer, nullable=False, default=_get_next_position) title = db.Column(db.String, nullable=False) description = db.Column(db.Text, nullable=False, default='') is_required = db.Column(db.Boolean, nullable=False, default=False) is_active = db.Column(db.Boolean, nullable=False, default=True) is_user_editable = db.Column(db.Boolean, nullable=False, default=True) visibility = db.Column(PyIntEnum(ContributionFieldVisibility), nullable=False, default=ContributionFieldVisibility.public) field_type = db.Column(db.String, nullable=True) field_data = db.Column(JSONB, nullable=False, default={}) event = db.relationship('Event', lazy=True, backref=db.backref('contribution_fields', order_by=position, cascade='all, delete-orphan', lazy='dynamic')) # relationship backrefs: # - abstract_values (AbstractFieldValue.contribution_field) # - contribution_values (ContributionFieldValue.contribution_field) def _get_field(self, management=False): from indico.modules.events.contributions import get_contrib_field_types try: impl = get_contrib_field_types()[self.field_type] except KeyError: return None return impl(self, management=management) @property def field(self): return self._get_field() @property def mgmt_field(self): return self._get_field(management=True) @property def is_public(self): return self.visibility == ContributionFieldVisibility.public @property def filter_choices(self): return { x['id']: x['option'] for x in self.field_data.get('options', {}) } @return_ascii def __repr__(self): return format_repr(self, 'id', 'field_type', is_required=False, is_active=True, _text=self.title) @locator_property def locator(self): return dict(self.event.locator, contrib_field_id=self.id)
def content_type(cls): """The MIME type of the file""" return db.Column( db.String, nullable=not cls.file_required )
def subcontribution_id(cls): return db.Column( db.String, nullable=True )
class Registration(db.Model): """Somebody's registration for an event through a registration form""" __tablename__ = 'registrations' __table_args__ = (db.CheckConstraint('email = lower(email)', 'lowercase_email'), db.Index(None, 'friendly_id', 'event_id', unique=True, postgresql_where=db.text('NOT is_deleted')), db.Index(None, 'registration_form_id', 'user_id', unique=True, postgresql_where=db.text('NOT is_deleted AND (state NOT IN (3, 4))')), db.Index(None, 'registration_form_id', 'email', unique=True, postgresql_where=db.text('NOT is_deleted AND (state NOT IN (3, 4))')), db.ForeignKeyConstraint(['event_id', 'registration_form_id'], ['event_registration.forms.event_id', 'event_registration.forms.id']), {'schema': 'event_registration'}) #: The ID of the object id = db.Column( db.Integer, primary_key=True ) #: The unguessable ID for the object uuid = db.Column( UUID, index=True, unique=True, nullable=False, default=lambda: unicode(uuid4()) ) #: The human-friendly ID for the object friendly_id = db.Column( db.Integer, nullable=False, default=_get_next_friendly_id ) #: The ID of the event event_id = db.Column( db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False ) #: The ID of the registration form registration_form_id = db.Column( db.Integer, db.ForeignKey('event_registration.forms.id'), index=True, nullable=False ) #: The ID of the user who registered user_id = db.Column( db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=True ) #: The ID of the latest payment transaction associated with this registration transaction_id = db.Column( db.Integer, db.ForeignKey('events.payment_transactions.id'), index=True, unique=True, nullable=True ) #: The state a registration is in state = db.Column( PyIntEnum(RegistrationState), nullable=False, ) #: The base registration fee (that is not specific to form items) base_price = db.Column( db.Numeric(8, 2), # max. 999999.99 nullable=False, default=0 ) #: The price modifier applied to the final calculated price price_adjustment = db.Column( db.Numeric(8, 2), # max. 999999.99 nullable=False, default=0 ) #: Registration price currency currency = db.Column( db.String, nullable=False ) #: The date/time when the registration was recorded submitted_dt = db.Column( UTCDateTime, nullable=False, default=now_utc, ) #: The email of the registrant email = db.Column( db.String, nullable=False ) #: The first name of the registrant first_name = db.Column( db.String, nullable=False ) #: The last name of the registrant last_name = db.Column( db.String, nullable=False ) #: If the registration has been deleted is_deleted = db.Column( db.Boolean, nullable=False, default=False ) #: The unique token used in tickets ticket_uuid = db.Column( UUID, index=True, unique=True, nullable=False, default=lambda: unicode(uuid4()) ) #: Whether the person has checked in. Setting this also sets or clears #: `checked_in_dt`. checked_in = db.Column( db.Boolean, nullable=False, default=False ) #: The date/time when the person has checked in checked_in_dt = db.Column( UTCDateTime, nullable=True ) #: The Event containing this registration event = db.relationship( 'Event', lazy=True, backref=db.backref( 'registrations', lazy='dynamic' ) ) # The user linked to this registration user = db.relationship( 'User', lazy=True, backref=db.backref( 'registrations', lazy='dynamic' # XXX: a delete-orphan cascade here would delete registrations when NULLing the user ) ) #: The latest payment transaction associated with this registration transaction = db.relationship( 'PaymentTransaction', lazy=True, foreign_keys=[transaction_id], post_update=True ) #: The registration this data is associated with data = db.relationship( 'RegistrationData', lazy=True, cascade='all, delete-orphan', backref=db.backref( 'registration', lazy=True ) ) # relationship backrefs: # - invitation (RegistrationInvitation.registration) # - legacy_mapping (LegacyRegistrationMapping.registration) # - registration_form (RegistrationForm.registrations) # - transactions (PaymentTransaction.registration) @classmethod def get_all_for_event(cls, event): """Retrieve all registrations in all registration forms of an event.""" from indico.modules.events.registration.models.forms import RegistrationForm return Registration.find_all(Registration.is_active, ~RegistrationForm.is_deleted, RegistrationForm.event_id == event.id, _join=Registration.registration_form) @hybrid_property def is_active(self): return not self.is_cancelled and not self.is_deleted @is_active.expression def is_active(cls): return ~cls.is_cancelled & ~cls.is_deleted @hybrid_property def is_cancelled(self): return self.state in (RegistrationState.rejected, RegistrationState.withdrawn) @is_cancelled.expression def is_cancelled(self): return self.state.in_((RegistrationState.rejected, RegistrationState.withdrawn)) @locator_property def locator(self): return dict(self.registration_form.locator, registration_id=self.id) @locator.registrant def locator(self): """A locator suitable for 'display' pages. It includes the UUID of the registration unless the current request doesn't contain the uuid and the registration is tied to the currently logged-in user. """ loc = self.registration_form.locator if (not self.user or not has_request_context() or self.user != session.user or request.args.get('token') == self.uuid): loc['token'] = self.uuid return loc @locator.uuid def locator(self): """A locator that uses uuid instead of id""" return dict(self.registration_form.locator, token=self.uuid) @property def can_be_modified(self): regform = self.registration_form return regform.is_modification_open and regform.is_modification_allowed(self) @property def data_by_field(self): return {x.field_data.field_id: x for x in self.data} @property def billable_data(self): return [data for data in self.data if data.price] @property def full_name(self): """Returns the user's name in 'Firstname Lastname' notation.""" return self.get_full_name(last_name_first=False) @property def display_full_name(self): """Return the full name using the user's preferred name format.""" return format_display_full_name(session.user, self) @property def is_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, _skip_moderation=False): """Update the state of the registration for a given action The accepted kwargs are the possible actions. ``True`` means that the action occured and ``False`` that it was reverted. """ if sum(action is not None for action in (approved, paid, rejected)) > 1: raise Exception("More than one action specified") initial_state = self.state regform = self.registration_form invitation = self.invitation moderation_required = (regform.moderation_enabled and not _skip_moderation and (not invitation or not invitation.skip_moderation)) with db.session.no_autoflush: payment_required = regform.event.has_feature('payment') and self.price if self.state == RegistrationState.pending: if approved and payment_required: self.state = RegistrationState.unpaid elif approved: self.state = RegistrationState.complete elif rejected: self.state = RegistrationState.rejected elif self.state == RegistrationState.unpaid: if paid: self.state = RegistrationState.complete elif approved is False: self.state = RegistrationState.pending elif self.state == RegistrationState.complete: if approved is False and payment_required is False and moderation_required: self.state = RegistrationState.pending elif paid is False and payment_required: self.state = RegistrationState.unpaid if self.state != initial_state: signals.event.registration_state_updated.send(self, previous_state=initial_state)
class Session(DescriptionMixin, ColorMixin, ProtectionManagersMixin, LocationMixin, AttachedItemsMixin, AttachedNotesMixin, db.Model): __tablename__ = 'sessions' __auto_table_args = (db.Index(None, 'friendly_id', 'event_id', unique=True, postgresql_where=db.text('NOT is_deleted')), { 'schema': 'events' }) location_backref_name = 'sessions' disallowed_protection_modes = frozenset() inheriting_have_acl = True default_colors = ColorTuple('#202020', '#e3f2d3') allow_relationship_preloading = True PRELOAD_EVENT_ATTACHED_ITEMS = True PRELOAD_EVENT_NOTES = True ATTACHMENT_FOLDER_ID_COLUMN = 'session_id' possible_render_modes = {RenderMode.markdown} default_render_mode = RenderMode.markdown @declared_attr def __table_args__(cls): return auto_table_args(cls) id = db.Column(db.Integer, primary_key=True) #: The human-friendly ID for the session friendly_id = db.Column(db.Integer, nullable=False, default=_get_next_friendly_id) event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False) type_id = db.Column(db.Integer, db.ForeignKey('events.session_types.id'), index=True, nullable=True) title = db.Column(db.String, nullable=False) code = db.Column(db.String, nullable=False, default='') default_contribution_duration = db.Column(db.Interval, nullable=False, default=timedelta(minutes=20)) is_deleted = db.Column(db.Boolean, nullable=False, default=False) event = db.relationship( 'Event', lazy=True, backref=db.backref( 'sessions', primaryjoin='(Session.event_id == Event.id) & ~Session.is_deleted', cascade='all, delete-orphan', lazy=True)) acl_entries = db.relationship('SessionPrincipal', lazy=True, cascade='all, delete-orphan', collection_class=set, backref='session') blocks = db.relationship('SessionBlock', lazy=True, cascade='all, delete-orphan', backref=db.backref('session', lazy=False)) type = db.relationship('SessionType', lazy=True, backref=db.backref('sessions', lazy=True)) # relationship backrefs: # - attachment_folders (AttachmentFolder.session) # - contributions (Contribution.session) # - default_for_tracks (Track.default_session) # - legacy_mapping (LegacySessionMapping.session) # - note (EventNote.session) def __init__(self, **kwargs): # explicitly initialize this relationship with None to avoid # an extra query to check whether there is an object associated # when assigning a new one (e.g. during cloning) kwargs.setdefault('note', None) super().__init__(**kwargs) @classmethod def preload_acl_entries(cls, event): cls.preload_relationships(cls.query.with_parent(event), 'acl_entries') @property def location_parent(self): return self.event @property def protection_parent(self): return self.event @property def session(self): """Convenience property so all event entities have it.""" return self @property @memoize_request def start_dt(self): from indico.modules.events.sessions.models.blocks import SessionBlock start_dt = (self.event.timetable_entries.with_entities( TimetableEntry.start_dt).join('session_block').filter( TimetableEntry.type == TimetableEntryType.SESSION_BLOCK, SessionBlock.session == self).order_by( TimetableEntry.start_dt).first()) return start_dt[0] if start_dt else None @property @memoize_request def end_dt(self): sorted_blocks = sorted(self.blocks, key=attrgetter('timetable_entry.end_dt'), reverse=True) return sorted_blocks[ 0].timetable_entry.end_dt if sorted_blocks else None @property @memoize_request def conveners(self): from indico.modules.events.sessions.models.blocks import SessionBlock from indico.modules.events.sessions.models.persons import SessionBlockPersonLink return (SessionBlockPersonLink.query.join(SessionBlock).filter( SessionBlock.session_id == self.id).distinct( SessionBlockPersonLink.person_id).all()) @property def is_poster(self): return self.type.is_poster if self.type else False @locator_property def locator(self): return dict(self.event.locator, session_id=self.id) def get_non_inheriting_objects(self): """Get a set of child objects that do not inherit protection.""" return get_non_inheriting_objects(self) def __repr__(self): return format_repr(self, 'id', is_deleted=False, _text=self.title) def can_manage_contributions(self, user, allow_admin=True): """Check whether a user can manage contributions within the session.""" from indico.modules.events.sessions.util import session_coordinator_priv_enabled if user is None: return False elif self.session.can_manage(user, allow_admin=allow_admin): return True elif (self.session.can_manage(user, 'coordinate') and session_coordinator_priv_enabled(self.event, 'manage-contributions')): return True else: return False def can_manage_blocks(self, user, allow_admin=True): """Check whether a user can manage session blocks. This only applies to the blocks themselves, not to contributions inside them. """ from indico.modules.events.sessions.util import session_coordinator_priv_enabled if user is None: return False # full session manager can always manage blocks. this also includes event managers and higher. elif self.session.can_manage(user, allow_admin=allow_admin): return True # session coordiator if block management is allowed elif (self.session.can_manage(user, 'coordinate') and session_coordinator_priv_enabled(self.event, 'manage-blocks')): return True else: return False
class Abstract(ProposalMixin, ProposalRevisionMixin, DescriptionMixin, CustomFieldsMixin, AuthorsSpeakersMixin, db.Model): """An abstract that can be associated to a Contribution.""" __tablename__ = 'abstracts' __auto_table_args = ( db.Index(None, 'friendly_id', 'event_id', unique=True, postgresql_where=db.text('NOT is_deleted')), db.CheckConstraint( '(state = {}) OR (accepted_track_id IS NULL)'.format( AbstractState.accepted), name='accepted_track_id_only_accepted'), db.CheckConstraint( '(state = {}) OR (accepted_contrib_type_id IS NULL)'.format( AbstractState.accepted), name='accepted_contrib_type_id_only_accepted'), db.CheckConstraint( '(state = {}) = (merged_into_id IS NOT NULL)'.format( AbstractState.merged), name='merged_into_id_only_merged'), db.CheckConstraint( '(state = {}) = (duplicate_of_id IS NOT NULL)'.format( AbstractState.duplicate), name='duplicate_of_id_only_duplicate'), db.CheckConstraint( '(state IN ({}, {}, {}, {})) = (judge_id IS NOT NULL)'.format( AbstractState.accepted, AbstractState.rejected, AbstractState.merged, AbstractState.duplicate), name='judge_if_judged'), db.CheckConstraint( '(state IN ({}, {}, {}, {})) = (judgment_dt IS NOT NULL)'.format( AbstractState.accepted, AbstractState.rejected, AbstractState.merged, AbstractState.duplicate), name='judgment_dt_if_judged'), db.CheckConstraint('(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)
class Category(SearchableTitleMixin, DescriptionMixin, ProtectionManagersMixin, AttachedItemsMixin, db.Model): """An Indico category""" __tablename__ = 'categories' disallowed_protection_modes = frozenset() inheriting_have_acl = True possible_render_modes = {RenderMode.markdown} default_render_mode = RenderMode.markdown allow_no_access_contact = True ATTACHMENT_FOLDER_ID_COLUMN = 'category_id' @strict_classproperty @classmethod def __auto_table_args(cls): return (db.CheckConstraint( "(icon IS NULL) = (icon_metadata::text = 'null')", 'valid_icon'), db.CheckConstraint( "(logo IS NULL) = (logo_metadata::text = 'null')", 'valid_logo'), db.CheckConstraint("(parent_id IS NULL) = (id = 0)", 'valid_parent'), db.CheckConstraint("(id != 0) OR NOT is_deleted", 'root_not_deleted'), db.CheckConstraint( "(id != 0) OR (protection_mode != {})".format( ProtectionMode.inheriting), 'root_not_inheriting'), db.CheckConstraint('visibility IS NULL OR visibility > 0', 'valid_visibility'), { 'schema': 'categories' }) @declared_attr def __table_args__(cls): return auto_table_args(cls) id = db.Column(db.Integer, primary_key=True) parent_id = db.Column(db.Integer, db.ForeignKey('categories.categories.id'), index=True, nullable=True) is_deleted = db.Column(db.Boolean, nullable=False, default=False) position = db.Column(db.Integer, nullable=False, default=_get_next_position) visibility = db.Column(db.Integer, nullable=True, default=None) icon_metadata = db.Column(JSONB, nullable=False, default=lambda: None) icon = db.deferred(db.Column(db.LargeBinary, nullable=True)) logo_metadata = db.Column(JSONB, nullable=False, default=lambda: None) logo = db.deferred(db.Column(db.LargeBinary, nullable=True)) timezone = db.Column(db.String, nullable=False, default=lambda: config.DEFAULT_TIMEZONE) default_event_themes = db.Column(JSONB, nullable=False, default=_get_default_event_themes) event_creation_restricted = db.Column(db.Boolean, nullable=False, default=True) event_creation_notification_emails = db.Column(ARRAY(db.String), nullable=False, default=[]) event_message_mode = db.Column(PyIntEnum(EventMessageMode), nullable=False, default=EventMessageMode.disabled) _event_message = db.Column('event_message', db.Text, nullable=False, default='') suggestions_disabled = db.Column(db.Boolean, nullable=False, default=False) notify_managers = db.Column(db.Boolean, nullable=False, default=False) default_ticket_template_id = db.Column( db.ForeignKey('indico.designer_templates.id'), nullable=True, index=True) children = db.relationship( 'Category', order_by='Category.position', primaryjoin=(id == db.remote(parent_id)) & ~db.remote(is_deleted), lazy=True, backref=db.backref('parent', primaryjoin=(db.remote(id) == parent_id), lazy=True)) acl_entries = db.relationship('CategoryPrincipal', backref='category', cascade='all, delete-orphan', collection_class=set) default_ticket_template = db.relationship( 'DesignerTemplate', lazy=True, foreign_keys=default_ticket_template_id, backref='default_ticket_template_of') # column properties: # - deep_events_count # relationship backrefs: # - attachment_folders (AttachmentFolder.category) # - designer_templates (DesignerTemplate.category) # - events (Event.category) # - favorite_of (User.favorite_categories) # - legacy_mapping (LegacyCategoryMapping.category) # - parent (Category.children) # - roles (CategoryRole.category) # - settings (CategorySetting.category) # - suggestions (SuggestedCategory.category) @hybrid_property def event_message(self): return MarkdownText(self._event_message) @event_message.setter def event_message(self, value): self._event_message = value @event_message.expression def event_message(cls): return cls._event_message @return_ascii def __repr__(self): return format_repr(self, 'id', is_deleted=False, _text=text_to_repr(self.title, max_length=75)) @property def protection_parent(self): return self.parent if not self.is_root else None @locator_property def locator(self): return {'category_id': self.id} @classmethod def get_root(cls): """Get the root category""" return cls.query.filter(cls.is_root).one() @property def url(self): return url_for('categories.display', self) @property def has_only_events(self): return self.has_events and not self.children @hybrid_property def is_root(self): return self.parent_id is None @is_root.expression def is_root(cls): return cls.parent_id.is_(None) @property def is_empty(self): return not self.deep_children_count and not self.deep_events_count @property def has_icon(self): return self.icon_metadata is not None @property def has_effective_icon(self): return self.effective_icon_data['metadata'] is not None @property def has_logo(self): return self.logo_metadata is not None @property def tzinfo(self): return pytz.timezone(self.timezone) @property def display_tzinfo(self): """The tzinfo of the category or the one specified by the user""" return get_display_tz(self, as_timezone=True) def can_create_events(self, user): """Check whether the user can create events in the category.""" # if creation is not restricted anyone who can access the category # can also create events in it, otherwise only people with the # creation role can return user and ( (not self.event_creation_restricted and self.can_access(user)) or self.can_manage(user, permission='create')) def move(self, target): """Move the category into another category.""" assert not self.is_root old_parent = self.parent self.position = (max(x.position for x in target.children) + 1) if target.children else 1 self.parent = target db.session.flush() signals.category.moved.send(self, old_parent=old_parent) @classmethod def get_tree_cte(cls, col='id'): """Create a CTE for the category tree. The CTE contains the following columns: - ``id`` -- the category id - ``path`` -- an array containing the path from the root to the category itself - ``is_deleted`` -- whether the category is deleted :param col: The name of the column to use in the path or a callable receiving the category alias that must return the expression used for the 'path' retrieved by the CTE. """ cat_alias = db.aliased(cls) if callable(col): path_column = col(cat_alias) else: path_column = getattr(cat_alias, col) cte_query = (select([ cat_alias.id, array([path_column]).label('path'), cat_alias.is_deleted ]).where(cat_alias.parent_id.is_(None)).cte(recursive=True)) rec_query = (select([ cat_alias.id, cte_query.c.path.op('||')(path_column), cte_query.c.is_deleted | cat_alias.is_deleted ]).where(cat_alias.parent_id == cte_query.c.id)) return cte_query.union_all(rec_query) @classmethod def get_protection_cte(cls): cat_alias = db.aliased(cls) cte_query = (select([cat_alias.id, cat_alias.protection_mode]).where( cat_alias.parent_id.is_(None)).cte(recursive=True)) rec_query = (select([ cat_alias.id, db.case( {ProtectionMode.inheriting.value: cte_query.c.protection_mode}, else_=cat_alias.protection_mode, value=cat_alias.protection_mode) ]).where(cat_alias.parent_id == cte_query.c.id)) return cte_query.union_all(rec_query) def get_protection_parent_cte(self): cte_query = (select([ Category.id, db.cast(literal(None), db.Integer).label('protection_parent') ]).where(Category.id == self.id).cte(recursive=True)) rec_query = (select([ Category.id, db.case( { ProtectionMode.inheriting.value: func.coalesce(cte_query.c.protection_parent, self.id) }, else_=Category.id, value=Category.protection_mode) ]).where(Category.parent_id == cte_query.c.id)) return cte_query.union_all(rec_query) @classmethod def get_icon_data_cte(cls): cat_alias = db.aliased(cls) cte_query = (select([ cat_alias.id, cat_alias.id.label('source_id'), cat_alias.icon_metadata ]).where(cat_alias.parent_id.is_(None)).cte(recursive=True)) rec_query = (select([ cat_alias.id, db.case({'null': cte_query.c.source_id}, else_=cat_alias.id, value=db.func.jsonb_typeof(cat_alias.icon_metadata)), db.case({'null': cte_query.c.icon_metadata}, else_=cat_alias.icon_metadata, value=db.func.jsonb_typeof(cat_alias.icon_metadata)) ]).where(cat_alias.parent_id == cte_query.c.id)) return cte_query.union_all(rec_query) @property def deep_children_query(self): """Get a query object for all subcategories. This includes subcategories at any level of nesting. """ cte = Category.get_tree_cte() return (Category.query.join(cte, Category.id == cte.c.id).filter( cte.c.path.contains([self.id]), cte.c.id != self.id, ~cte.c.is_deleted)) @staticmethod def _get_chain_query(start_criterion): cte_query = (select([ Category.id, Category.parent_id, literal(0).label('level') ]).where(start_criterion).cte('category_chain', recursive=True)) parent_query = (select([ Category.id, Category.parent_id, cte_query.c.level + 1 ]).where(Category.id == cte_query.c.parent_id)) cte_query = cte_query.union_all(parent_query) return Category.query.join(cte_query, Category.id == cte_query.c.id).order_by( cte_query.c.level.desc()) @property def chain_query(self): """Get a query object for the category chain. The query retrieves the root category first and then all the intermediate categories up to (and including) this category. """ return self._get_chain_query(Category.id == self.id) @property def parent_chain_query(self): """Get a query object for the category's parent chain. The query retrieves the root category first and then all the intermediate categories up to (excluding) this category. """ return self._get_chain_query(Category.id == self.parent_id) def nth_parent(self, n_categs, fail_on_overflow=True): """Return the nth parent of the category. :param n_categs: the number of categories to go up :param fail_on_overflow: whether to fail if we try to go above the root category :return: `Category` object or None (only if ``fail_on_overflow`` is not set) """ if n_categs == 0: return self chain = self.parent_chain_query.all() assert n_categs >= 0 if n_categs > len(chain): if fail_on_overflow: raise IndexError("Root category has no parent!") else: return None return chain[::-1][n_categs - 1] def is_descendant_of(self, categ): return categ != self and self.parent_chain_query.filter( Category.id == categ.id).has_rows() @property def visibility_horizon_query(self): """Get a query object that returns the highest category this one is visible from.""" cte_query = (select([ Category.id, Category.parent_id, db.case([(Category.visibility.is_(None), None)], else_=(Category.visibility - 1)).label('n'), literal(0).label('level') ]).where(Category.id == self.id).cte('visibility_horizon', recursive=True)) parent_query = (select([ Category.id, Category.parent_id, db.case([ (Category.visibility.is_(None) & cte_query.c.n.is_(None), None) ], else_=db.func.least(Category.visibility, cte_query.c.n) - 1), cte_query.c.level + 1 ]).where( db.and_(Category.id == cte_query.c.parent_id, (cte_query.c.n > 0) | cte_query.c.n.is_(None)))) cte_query = cte_query.union_all(parent_query) return db.session.query(cte_query.c.id, cte_query.c.n).order_by( cte_query.c.level.desc()).limit(1) @property def own_visibility_horizon(self): """Get the highest category this one would like to be visible from (configured visibility).""" if self.visibility is None: return Category.get_root() else: return self.nth_parent(self.visibility - 1) @property def real_visibility_horizon(self): """Get the highest category this one is actually visible from (as limited by categories above).""" horizon_id, final_visibility = self.visibility_horizon_query.one() if final_visibility is not None and final_visibility < 0: return None # Category is invisible return Category.get(horizon_id) @staticmethod def get_visible_categories_cte(category_id): """ Get a sqlalchemy select for the visible categories within the given category, including the category itself. """ cte_query = (select([ Category.id, literal(0).label('level') ]).where((Category.id == category_id) & (Category.visibility.is_(None) | (Category.visibility > 0))).cte(recursive=True)) parent_query = (select([Category.id, cte_query.c.level + 1]).where( db.and_( Category.parent_id == cte_query.c.id, db.or_(Category.visibility.is_(None), Category.visibility > cte_query.c.level + 1)))) return cte_query.union_all(parent_query) @property def visible_categories_query(self): """ Get a query object for the visible categories within this category, including the category itself. """ cte_query = Category.get_visible_categories_cte(self.id) return Category.query.join(cte_query, Category.id == cte_query.c.id) @property def icon_url(self): """Get the HTTP URL of the icon.""" return url_for('categories.display_icon', self, slug=self.icon_metadata['hash']) @property def effective_icon_url(self): """Get the HTTP URL of the icon (possibly inherited).""" data = self.effective_icon_data return url_for('categories.display_icon', category_id=data['source_id'], slug=data['metadata']['hash']) @property def logo_url(self): """Get the HTTP URL of the logo.""" return url_for('categories.display_logo', self, slug=self.logo_metadata['hash'])
class DesignerTemplate(db.Model): __tablename__ = 'designer_templates' __table_args__ = (db.CheckConstraint( '(event_id IS NULL) != (category_id IS NULL)', 'event_xor_category_id_null'), { 'schema': 'indico' }) id = db.Column(db.Integer, primary_key=True) type = db.Column(PyIntEnum(TemplateType), nullable=False) title = db.Column(db.String, nullable=False) event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=True) category_id = db.Column(db.Integer, db.ForeignKey('categories.categories.id'), index=True, nullable=True) data = db.Column(JSONB, nullable=False) background_image_id = db.Column( db.Integer, db.ForeignKey('indico.designer_image_files.id'), index=False, nullable=True) backside_template_id = db.Column( db.ForeignKey('indico.designer_templates.id'), index=True, nullable=True) is_clonable = db.Column(db.Boolean, nullable=False, default=True) is_system_template = db.Column(db.Boolean, nullable=False, default=False) category = db.relationship('Category', lazy=True, foreign_keys=category_id, backref=db.backref('designer_templates', cascade='all, delete-orphan', lazy=True)) event = db.relationship('Event', lazy=True, backref=db.backref('designer_templates', cascade='all, delete-orphan', lazy=True)) background_image = db.relationship('DesignerImageFile', lazy=True, foreign_keys=background_image_id, post_update=True) backside_template = db.relationship('DesignerTemplate', lazy=True, remote_side=id, backref='backside_template_of') # relationship backrefs: # - backside_template_of (DesignerTemplate.backside_template) # - default_badge_template_of (Category.default_badge_template) # - default_ticket_template_of (Category.default_ticket_template) # - images (DesignerImageFile.template) # - ticket_for_regforms (RegistrationForm.ticket_template) def __init__(self, **kwargs): data = kwargs.pop('data', None) tpl_type = kwargs.get('type') if data is None: data = {'items': [], 'background_position': 'stretch'} size = DEFAULT_CONFIG[tpl_type]['tpl_size'] data.update({'width': size[0], 'height': size[1]}) super().__init__(data=data, **kwargs) @hybrid_property def owner(self): return self.event if self.event else self.category @owner.comparator def owner(cls): return _OwnerComparator(cls) @locator_property def locator(self): return dict(self.owner.locator, template_id=self.id) @property def is_ticket(self): placeholders = get_placeholders('designer-fields') if any(placeholders[item['type']].is_ticket for item in self.data['items'] if item['type'] in placeholders): return True elif self.backside_template and self.backside_template.is_ticket: return True else: return False def __repr__(self): return format_repr(self, 'id', 'event_id', 'category_id', _text=self.title)
class VCRoomEventAssociation(db.Model): __tablename__ = 'vc_room_events' __table_args__ = tuple(_make_checks()) + (db.Index( None, 'data', postgresql_using='gin'), { 'schema': 'events' }) #: Association ID id = db.Column(db.Integer, primary_key=True) #: ID of the event event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, autoincrement=False, nullable=False) #: ID of the videoconference room vc_room_id = db.Column(db.Integer, db.ForeignKey('events.vc_rooms.id'), index=True, nullable=False) #: Type of the object the vc_room is linked to link_type = db.Column(PyIntEnum(VCRoomLinkType), nullable=False) linked_event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=True) session_block_id = db.Column(db.Integer, db.ForeignKey('events.session_blocks.id'), index=True, nullable=True) contribution_id = db.Column(db.Integer, db.ForeignKey('events.contributions.id'), index=True, nullable=True) #: If the vc room should be shown on the event page show = db.Column(db.Boolean, nullable=False, default=False) #: videoconference plugin-specific data data = db.Column(JSONB, nullable=False) #: The associated :class:VCRoom vc_room = db.relationship('VCRoom', lazy=False, backref=db.backref('events', cascade='all, delete-orphan')) #: The associated Event event_new = db.relationship('Event', foreign_keys=event_id, lazy=True, backref=db.backref('all_vc_room_associations', lazy='dynamic')) #: The linked event (if the VC room is attached to the event itself) linked_event = db.relationship('Event', foreign_keys=linked_event_id, lazy=True, backref=db.backref('vc_room_associations', lazy=True)) #: The linked contribution (if the VC room is attached to a contribution) linked_contrib = db.relationship('Contribution', lazy=True, backref=db.backref('vc_room_associations', lazy=True)) #: The linked session block (if the VC room is attached to a block) linked_block = db.relationship('SessionBlock', lazy=True, backref=db.backref('vc_room_associations', lazy=True)) @classmethod def register_link_events(cls): event_mapping = { cls.linked_block: lambda x: x.event_new, cls.linked_contrib: lambda x: x.event_new, cls.linked_event: lambda x: x } type_mapping = { cls.linked_event: VCRoomLinkType.event, cls.linked_block: VCRoomLinkType.block, cls.linked_contrib: VCRoomLinkType.contribution } def _set_link_type(link_type, target, value, *unused): if value is not None: target.link_type = link_type def _set_event_obj(fn, target, value, *unused): if value is not None: event = fn(value) assert event is not None target.event_new = event for rel, fn in event_mapping.iteritems(): if rel is not None: listen(rel, 'set', partial(_set_event_obj, fn)) for rel, link_type in type_mapping.iteritems(): if rel is not None: listen(rel, 'set', partial(_set_link_type, link_type)) @property def locator(self): return dict(self.event_new.locator, service=self.vc_room.type, event_vc_room_id=self.id) @hybrid_property def link_object(self): if self.link_type == VCRoomLinkType.event: return self.linked_event elif self.link_type == VCRoomLinkType.contribution: return self.linked_contrib else: return self.linked_block @link_object.setter def link_object(self, obj): self.linked_event = self.linked_contrib = self.linked_block = None if isinstance(obj, db.m.Event): self.linked_event = obj elif isinstance(obj, db.m.Contribution): self.linked_contrib = obj elif isinstance(obj, db.m.SessionBlock): self.linked_block = obj else: raise TypeError('Unexpected object: {}'.format(obj)) @link_object.comparator def link_object(cls): return _LinkObjectComparator(cls) @return_ascii def __repr__(self): return '<VCRoomEventAssociation({}, {})>'.format( self.event_id, self.vc_room) @classmethod @unify_event_args def find_for_event(cls, event, include_hidden=False, include_deleted=False, only_linked_to_event=False, **kwargs): """Returns a Query that retrieves the videoconference rooms for an event :param event: an indico Event :param only_linked_to_event: only retrieve the vc rooms linked to the whole event :param kwargs: extra kwargs to pass to ``find()`` """ if only_linked_to_event: kwargs['link_type'] = int(VCRoomLinkType.event) query = event.all_vc_room_associations if kwargs: query = query.filter_by(**kwargs) if not include_hidden: query = query.filter(cls.show) if not include_deleted: query = query.filter( VCRoom.status != VCRoomStatus.deleted).join(VCRoom) return query @classmethod @memoize_request def get_linked_for_event(cls, event): """Get a dict mapping link objects to event vc rooms""" return {vcr.link_object: vcr for vcr in cls.find_for_event(event)} def delete(self, user, delete_all=False): """Deletes a VC room from an event If the room is not used anywhere else, the room itself is also deleted. :param user: the user performing the deletion :param delete_all: if True, the room is detached from all events and deleted. """ vc_room = self.vc_room if delete_all: for assoc in vc_room.events[:]: Logger.get('modules.vc').info( "Detaching VC room {} from event {} ({})".format( vc_room, assoc.event_new, assoc.link_object)) vc_room.events.remove(assoc) else: Logger.get('modules.vc').info( "Detaching VC room {} from event {} ({})".format( vc_room, self.event_new, self.link_object)) vc_room.events.remove(self) db.session.flush() if not vc_room.events: Logger.get('modules.vc').info( "Deleting VC room {}".format(vc_room)) if vc_room.status != VCRoomStatus.deleted: vc_room.plugin.delete_room(vc_room, self.event_new) notify_deleted(vc_room.plugin, vc_room, self, self.event_new, user) db.session.delete(vc_room)
class 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) #: Whether the menu entry should be viewable only by registered users registered_only = 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 = 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).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).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).filter( MenuEntry.position > position, MenuEntry.parent == parent)) for sibling in new_siblings: sibling.position += 1 self.parent = parent self.position = position + 1
return '<LocalGroup({}, {})>'.format(self.id, self.name) @property def proxy(self): """Returns a GroupProxy wrapping this group""" from indico.modules.groups import GroupProxy return GroupProxy(self.id, _group=self) group_members_table = db.Table( 'group_members', db.metadata, db.Column( 'group_id', db.Integer, db.ForeignKey('users.groups.id'), primary_key=True, nullable=False, index=True ), db.Column( 'user_id', db.Integer, db.ForeignKey('users.users.id'), primary_key=True, nullable=False, index=True ), schema='users' )
class Agreement(db.Model): """Agreements between a person and Indico""" __tablename__ = 'agreements' __table_args__ = (db.UniqueConstraint('event_id', 'type', 'identifier'), { 'schema': 'events' }) #: Entry ID id = db.Column(db.Integer, primary_key=True) #: Entry universally unique ID uuid = db.Column(db.String, nullable=False) #: ID of the event event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), nullable=False, index=True) #: Type of agreement type = db.Column(db.String, nullable=False) #: Unique identifier within the event and type identifier = db.Column(db.String, nullable=False) #: Email of the person agreeing person_email = db.Column(db.String, nullable=True) #: Full name of the person agreeing person_name = db.Column(db.String, nullable=False) #: A :class:`AgreementState` state = db.Column(PyIntEnum(AgreementState), default=AgreementState.pending, nullable=False) #: The date and time the agreement was created timestamp = db.Column(UTCDateTime, default=now_utc, nullable=False) #: ID of a linked user user_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=True) #: The date and time the agreement was signed signed_dt = db.Column(UTCDateTime) #: The IP from which the agreement was signed signed_from_ip = db.Column(db.String) #: Explanation as to why the agreement was accepted/rejected reason = db.Column(db.String) #: Attachment attachment = db.deferred(db.Column(db.LargeBinary)) #: Filename and extension of the attachment attachment_filename = db.Column(db.String) #: Definition-specific data of the agreement data = db.Column(JSON) #: The user this agreement is linked to user = db.relationship('User', lazy=False, backref=db.backref('agreements', lazy='dynamic')) #: The Event this agreement is associated with event = db.relationship('Event', lazy=True, backref=db.backref('agreements', lazy='dynamic')) @hybrid_property def accepted(self): return self.state in { AgreementState.accepted, AgreementState.accepted_on_behalf } @accepted.expression def accepted(self): return self.state.in_( (AgreementState.accepted, AgreementState.accepted_on_behalf)) @hybrid_property def pending(self): return self.state == AgreementState.pending @hybrid_property def rejected(self): return self.state in { AgreementState.rejected, AgreementState.rejected_on_behalf } @rejected.expression def rejected(self): return self.state.in_( (AgreementState.rejected, AgreementState.rejected_on_behalf)) @hybrid_property def signed_on_behalf(self): return self.state in { AgreementState.accepted_on_behalf, AgreementState.rejected_on_behalf } @signed_on_behalf.expression def signed_on_behalf(self): return self.state.in_((AgreementState.accepted_on_behalf, AgreementState.rejected_on_behalf)) @property def definition(self): from indico.modules.events.agreements.util import get_agreement_definitions return get_agreement_definitions().get(self.type) @property def locator(self): return {'confId': self.event_id, 'id': self.id} @return_ascii def __repr__(self): state = self.state.name if self.state is not None else None return '<Agreement({}, {}, {}, {}, {}, {})>'.format( self.id, self.event_id, self.type, self.identifier, self.person_email, state) @staticmethod def create_from_data(event, type_, person): agreement = Agreement(event=event, type=type_, state=AgreementState.pending, uuid=str(uuid4())) agreement.identifier = person.identifier agreement.person_email = person.email agreement.person_name = person.name if person.user: agreement.user = person.user agreement.data = person.data return agreement def accept(self, from_ip, reason=None, on_behalf=False): self.state = AgreementState.accepted if not on_behalf else AgreementState.accepted_on_behalf self.signed_from_ip = from_ip self.reason = reason self.signed_dt = now_utc() self.definition.handle_accepted(self) def reject(self, from_ip, reason=None, on_behalf=False): self.state = AgreementState.rejected if not on_behalf else AgreementState.rejected_on_behalf self.signed_from_ip = from_ip self.reason = reason self.signed_dt = now_utc() self.definition.handle_rejected(self) def reset(self): self.definition.handle_reset(self) self.state = AgreementState.pending self.attachment = None self.attachment_filename = None self.reason = None self.signed_dt = None self.signed_from_ip = None def render(self, form, **kwargs): definition = self.definition if definition is None: raise ServiceUnavailable( 'This agreement type is currently not available.') return definition.render_form(self, form, **kwargs) def belongs_to(self, person): return self.identifier == person.identifier def is_orphan(self): definition = self.definition if definition is None: raise ServiceUnavailable( 'This agreement type is currently not available.') return definition.is_agreement_orphan(self.event, self)
# - revisions (EditingRevision.tags) @property def verbose_title(self): """Properly formatted title, including tag code.""" return f'{self.code}: {self.title}' def __repr__(self): return format_repr(self, 'id', 'event_id', system=False, _text=self.title) db.Table( 'revision_tags', db.metadata, db.Column( 'revision_id', db.ForeignKey('event_editing.revisions.id'), primary_key=True, autoincrement=False, index=True ), db.Column( 'tag_id', db.ForeignKey('event_editing.tags.id'), primary_key=True, autoincrement=False, index=True ), schema='event_editing' )
class EventNoteRevision(db.Model): __tablename__ = 'note_revisions' __table_args__ = {'schema': 'events'} #: The ID of the revision id = db.Column( db.Integer, primary_key=True ) #: The ID of the associated note note_id = db.Column( db.Integer, db.ForeignKey('events.notes.id'), nullable=False, index=True ) #: The user who created the revision user_id = db.Column( db.Integer, db.ForeignKey('users.users.id'), nullable=False, index=True ) #: The date/time when the revision was created created_dt = db.Column( UTCDateTime, nullable=False, default=now_utc ) #: How the note is rendered render_mode = db.Column( PyIntEnum(RenderMode), nullable=False ) #: The raw source of the note as provided by the user source = db.Column( db.Text, nullable=False ) #: The rendered HTML of the note html = db.Column( db.Text, nullable=False ) #: The user who created the revision user = db.relationship( 'User', lazy=True, backref=db.backref( 'event_notes_revisions', lazy='dynamic' ) ) # relationship backrefs: # - note (EventNote.revisions) def __repr__(self): render_mode = self.render_mode.name if self.render_mode is not None else None source = text_to_repr(self.source, html=True) return '<EventNoteRevision({}, {}, {}, {}): "{}">'.format(self.id, self.note_id, render_mode, self.created_dt, source)
class SurveyItem(db.Model): __tablename__ = 'items' __table_args__ = (db.CheckConstraint("type != {type} OR (" "title IS NOT NULL AND " "is_required IS NOT NULL AND " "field_type IS NOT NULL AND " "parent_id IS NOT NULL AND " "display_as_section IS NULL)" .format(type=SurveyItemType.question), 'valid_question'), db.CheckConstraint("type != {type} OR (" "title IS NOT NULL AND " "is_required IS NULL AND " "field_type IS NULL AND " "field_data::text = '{{}}' AND " "parent_id IS NULL AND " "display_as_section IS NOT NULL)" .format(type=SurveyItemType.section), 'valid_section'), db.CheckConstraint("type != {type} OR (" "title IS NULL AND " "is_required IS NULL AND " "field_type IS NULL AND " "field_data::text = '{{}}' AND " "parent_id IS NOT NULL AND " "display_as_section IS NULL)" .format(type=SurveyItemType.text), 'valid_text'), {'schema': 'event_surveys'}) __mapper_args__ = { 'polymorphic_on': 'type', 'polymorphic_identity': None } #: The ID of the item id = db.Column( db.Integer, primary_key=True ) #: The ID of the survey survey_id = db.Column( db.Integer, db.ForeignKey('event_surveys.surveys.id'), index=True, nullable=False, ) #: The ID of the parent section item (NULL for top-level items, i.e. sections) parent_id = db.Column( db.Integer, db.ForeignKey('event_surveys.items.id'), index=True, nullable=True, ) #: The position of the item in the survey form position = db.Column( db.Integer, nullable=False, default=_get_next_position ) #: The type of the survey item type = db.Column( PyIntEnum(SurveyItemType), nullable=False ) #: The title of the item title = db.Column( db.String, nullable=True, default=_get_item_default_title ) #: The description of the item description = db.Column( db.Text, nullable=False, default='' ) #: If a section should be rendered as a section display_as_section = db.Column( db.Boolean, nullable=True ) # The following columns are only used for SurveyQuestion objects, but by # specifying them here we can access them withouy an extra query when we # query SurveyItem objects directly instead of going through a subclass. # This is done e.g. when using the Survey.top_level_items relationship. #: If the question must be answered (wtforms DataRequired) is_required = db.Column( db.Boolean, nullable=True ) #: The type of the field used for the question field_type = db.Column( db.String, nullable=True ) #: Field-specific data (such as choices for multi-select fields) field_data = db.Column( JSON, nullable=False, default={} ) # relationship backrefs: # - parent (SurveySection.children) # - survey (Survey.items) def to_dict(self): """Return a json-serializable representation of this object. Subclasses must add their own data to the dict. """ return {'type': self.type.name, 'title': self.title, 'description': self.description}
class 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_network = False principal_order = 0 principal_type = PrincipalType.user __tablename__ = 'users' __table_args__ = ( 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 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) # - favorite_of (User.favorite_users) # - 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_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) # - legacy_paper_reviewing_roles (LegacyPaperReviewingRole.user) # - 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) @property def as_principal(self): """The serializable principal identifier of this user""" return 'User', 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_css(self): from indico.modules.users.util import get_color_for_username return 'background-color: {};'.format( get_color_for_username(self.full_name)) @property def external_identities(self): """The external identities of the user""" return {x for x in self.identities if x.provider != 'indico'} @property def local_identities(self): """The local identities of the user""" return {x for x in self.identities if x.provider == 'indico'} @property def local_identity(self): """The main (most recently used) local identity""" identities = sorted(self.local_identities, key=attrgetter('safe_last_login_dt'), reverse=True) return identities[0] if identities else None @property def secondary_local_identities(self): """The local identities of the user except the main one""" return self.local_identities - {self.local_identity} @property def is_janitor(self): return self.id == Config.getInstance().getJanitorUserId() @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 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
def event_id(cls): return db.Column(db.Integer, db.ForeignKey('events.events.id'), nullable=False, index=True)
class EventNote(LinkMixin, db.Model): __tablename__ = 'notes' allowed_link_types = LinkMixin.allowed_link_types - { LinkType.category, LinkType.session_block } unique_links = True events_backref_name = 'all_notes' link_backref_name = 'note' @strict_classproperty @classmethod def __auto_table_args(cls): return (make_fts_index(cls, 'html'), {'schema': 'events'}) @declared_attr def __table_args__(cls): return auto_table_args(cls) #: The ID of the note id = db.Column(db.Integer, primary_key=True) #: If the note has been deleted is_deleted = db.Column(db.Boolean, nullable=False, default=False) #: The rendered HTML of the note html = db.Column(db.Text, nullable=False) #: The ID of the current revision current_revision_id = db.Column( db.Integer, db.ForeignKey('events.note_revisions.id', use_alter=True), nullable=True # needed for post_update :( ) #: The list of all revisions for the note revisions = db.relationship( 'EventNoteRevision', primaryjoin=lambda: EventNote.id == EventNoteRevision.note_id, foreign_keys=lambda: EventNoteRevision.note_id, lazy=True, cascade='all, delete-orphan', order_by=lambda: EventNoteRevision.created_dt.desc(), backref=db.backref('note', lazy=False)) #: The currently active revision of the note current_revision = db.relationship( 'EventNoteRevision', primaryjoin=lambda: EventNote.current_revision_id == EventNoteRevision. id, foreign_keys=current_revision_id, lazy=True, post_update=True) @locator_property def locator(self): return self.object.locator @classmethod def get_for_linked_object(cls, linked_object, preload_event=True): """Get the note for the given object. This only returns a note that hasn't been deleted. :param linked_object: An event, session, contribution or subcontribution. :param preload_event: If all notes for the same event should be pre-loaded and cached in the app context. """ event = linked_object.event try: return g.event_notes[event].get(linked_object) except (AttributeError, KeyError): if not preload_event: return linked_object.note if linked_object.note and not linked_object.note.is_deleted else None if 'event_notes' not in g: g.event_notes = {} query = (event.all_notes.filter_by(is_deleted=False).options( joinedload(EventNote.linked_event), joinedload(EventNote.session), joinedload(EventNote.contribution), joinedload(EventNote.subcontribution))) g.event_notes[event] = {n.object: n for n in query} return g.event_notes[event].get(linked_object) @classmethod def get_or_create(cls, linked_object): """Get the note for the given object or creates a new one. If there is an existing note for the object, it will be returned even. Otherwise a new note is created. """ note = cls.query.filter_by(object=linked_object).first() if note is None: note = cls(object=linked_object) return note def delete(self, user): """Mark the note as deleted and adds a new empty revision.""" self.create_revision(self.current_revision.render_mode, '', user) self.is_deleted = True def create_revision(self, render_mode, source, user): """Create a new revision if needed and marks it as undeleted if it was. Any change to the render mode or the source causes a new revision to be created. The user is not taken into account since a user "modifying" a note without changing things is not really a change. """ self.is_deleted = False with db.session.no_autoflush: current = self.current_revision if current is not None and current.render_mode == render_mode and current.source == source: return current self.current_revision = EventNoteRevision(render_mode=render_mode, source=source, user=user) return self.current_revision @classmethod def html_matches(cls, search_string, exact=False): """Check whether the html content matches a search string. To be used in a SQLAlchemy `filter` call. :param search_string: A string to search for :param exact: Whether to search for the exact string """ return fts_matches(cls.html, search_string, exact=exact) def __repr__(self): return '<EventNote({}, current_revision={}{}, {})>'.format( self.id, self.current_revision_id, ', is_deleted=True' if self.is_deleted else '', self.link_repr)
class Location(db.Model): __tablename__ = 'locations' __table_args__ = {'schema': 'roombooking'} # TODO: Turn this into a proper admin setting working_time_periods = ((time(8, 30), time(12, 30)), (time(13, 30), time(17, 30))) @classproperty @classmethod def working_time_start(cls): return cls.working_time_periods[0][0] @classproperty @classmethod def working_time_end(cls): return cls.working_time_periods[-1][1] id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String, nullable=False, unique=True, index=True) is_default = db.Column(db.Boolean, nullable=False, default=False) map_url_template = db.Column(db.String, nullable=False, default='') _room_name_format = db.Column('room_name_format', db.String, nullable=False, default=u'%1$s/%2$s-%3$s') #: The format used to display room names (with placeholders) @hybrid_property def room_name_format(self): """Translate Postgres' format syntax (e.g. `%1$s/%2$s-%3$s`) to Python's.""" placeholders = ['building', 'floor', 'number'] return re.sub(r'%(\d)\$s', lambda m: '{%s}' % placeholders[int(m.group(1)) - 1], self._room_name_format) @room_name_format.expression def room_name_format(cls): return cls._room_name_format @room_name_format.setter def room_name_format(self, value): self._room_name_format = value.format(building='%1$s', floor='%2$s', number='%3$s') rooms = db.relationship('Room', backref='location', cascade='all, delete-orphan', lazy=True) # relationship backrefs: # - breaks (Break.own_venue) # - contributions (Contribution.own_venue) # - events (Event.own_venue) # - session_blocks (SessionBlock.own_venue) # - sessions (Session.own_venue) @return_ascii def __repr__(self): return format_repr(self, 'id', 'name') @locator_property def locator(self): return {'locationId': self.name} @property @memoize_request def is_map_available(self): # XXX: Broken due to Aspect refactoring. Will be removed anyway. return False @classproperty @classmethod @memoize_request def default_location(cls): return cls.query.filter_by(is_default=True).first() def set_default(self): if self.is_default: return (Location.query.filter(Location.is_default | (Location.id == self.id)).update( { 'is_default': func.not_( Location.is_default) }, synchronize_session='fetch')) def get_buildings(self): building_rooms = defaultdict(list) for room in self.rooms: building_rooms[room.building].append(room) buildings = [] for building_name, rooms in building_rooms.iteritems(): room_with_lat_lon = next( (r for r in rooms if r.longitude and r.latitude), None) if not room_with_lat_lon: continue buildings.append({ 'number': building_name, 'title': _(u'Building {}'.format(building_name)), 'longitude': room_with_lat_lon.longitude, 'latitude': room_with_lat_lon.latitude, 'rooms': [r.to_serializable('__public_exhaustive__') for r in rooms] }) return buildings
def session_id(cls): return db.Column( db.String, nullable=True )
class Location(db.Model): __tablename__ = 'locations' __table_args__ = (db.Index(None, 'name', unique=True, postgresql_where=db.text('NOT is_deleted')), {'schema': 'roombooking'}) id = db.Column( db.Integer, primary_key=True ) name = db.Column( db.String, nullable=False, ) map_url_template = db.Column( db.String, nullable=False, default='' ) _room_name_format = db.Column( 'room_name_format', db.String, nullable=False, default='%1$s/%2$s-%3$s' ) is_deleted = db.Column( db.Boolean, nullable=False, default=False, ) #: The format used to display room names (with placeholders) @hybrid_property def room_name_format(self): """Translate Postgres' format syntax (e.g. `%1$s/%2$s-%3$s`) to Python's.""" placeholders = ['building', 'floor', 'number', 'site'] return re.sub( r'%(\d)\$s', lambda m: '{%s}' % placeholders[int(m.group(1)) - 1], self._room_name_format ) @room_name_format.expression def room_name_format(cls): return cls._room_name_format @room_name_format.setter def room_name_format(self, value): self._room_name_format = value.format( building='%1$s', floor='%2$s', number='%3$s', site='%4$s' ) rooms = db.relationship( 'Room', back_populates='location', cascade='all, delete-orphan', primaryjoin='(Room.location_id == Location.id) & ~Room.is_deleted', lazy=True ) # relationship backrefs: # - breaks (Break.own_venue) # - contributions (Contribution.own_venue) # - events (Event.own_venue) # - session_blocks (SessionBlock.own_venue) # - sessions (Session.own_venue) def __repr__(self): return format_repr(self, 'id', 'name', is_deleted=False)
class RegistrationData(StoredFileMixin, db.Model): """Data entry within a registration for a field in a registration form""" __tablename__ = 'registration_data' __table_args__ = {'schema': 'event_registration'} # StoredFileMixin settings add_file_date_column = False file_required = False #: The ID of the registration registration_id = db.Column( db.Integer, db.ForeignKey('event_registration.registrations.id'), primary_key=True, autoincrement=False ) #: The ID of the field data field_data_id = db.Column( db.Integer, db.ForeignKey('event_registration.form_field_data.id'), primary_key=True, autoincrement=False ) #: The submitted data for the field data = db.Column( JSONB, default=lambda: None, nullable=False ) #: The associated field data object field_data = db.relationship( 'RegistrationFormFieldData', lazy=True, backref=db.backref( 'registration_data', lazy=True, cascade='all, delete-orphan' ) ) # relationship backrefs: # - registration (Registration.data) @locator_property def locator(self): # a normal locator doesn't make much sense raise NotImplementedError @locator.file def locator(self): """A locator that points to the associated file.""" if not self.filename: raise Exception('The file locator is only available if there is a file.') return dict(self.registration.locator, field_data_id=self.field_data_id, filename=self.filename) @property def friendly_data(self): return self.get_friendly_data() @property def search_data(self): return self.get_friendly_data(for_search=True) def get_friendly_data(self, **kwargs): return self.field_data.field.get_friendly_data(self, **kwargs) @property def price(self): return self.field_data.field.calculate_price(self) @property def summary_data(self): return {'data': self.friendly_data, 'price': self.price} @property def user_data(self): return self.filename if self.field_data.field.input_type == 'file' else self.data def _set_file(self, file_info): # in case we are deleting/replacing a file self.storage_backend = None self.storage_file_id = None self.filename = None self.content_type = None self.size = None if file_info is not None: self.filename = file_info['name'] self.content_type = file_info['content_type'] self.save(file_info['data']) file = property(fset=_set_file) del _set_file @return_ascii def __repr__(self): return '<RegistrationData({}, {}): {}>'.format(self.registration_id, self.field_data_id, self.data) def _build_storage_path(self): self.registration.registration_form.assign_id() self.registration.assign_id() path_segments = ['event', strict_unicode(self.registration.event_id), 'registrations', strict_unicode(self.registration.registration_form.id), strict_unicode(self.registration.id)] assert None not in path_segments # add timestamp in case someone uploads the same file again filename = '{}-{}-{}'.format(self.field_data.field_id, int(time.time()), self.filename) path = posixpath.join(*(path_segments + [filename])) return config.ATTACHMENT_STORAGE, path def render_price(self): return format_currency(self.price, self.registration.currency, locale=session.lang or 'en_GB')
class Reservation(Serializer, db.Model): __tablename__ = 'reservations' __public__ = [] __calendar_public__ = [ 'id', ('booked_for_name', 'bookedForName'), ('booking_reason', 'reason'), ('external_details_url', 'bookingUrl') ] __api_public__ = [ 'id', ('start_dt', 'startDT'), ('end_dt', 'endDT'), 'repeat_frequency', 'repeat_interval', ('booked_for_name', 'bookedForName'), ('external_details_url', 'bookingUrl'), ('booking_reason', 'reason'), ('uses_vc', 'usesAVC'), ('needs_vc_assistance', 'needsAVCSupport'), 'needs_assistance', ('is_accepted', 'isConfirmed'), ('is_accepted', 'isValid'), 'is_cancelled', 'is_rejected', ('location_name', 'location'), ('contact_email', 'booked_for_user_email') ] @declared_attr def __table_args__(cls): return (db.Index('ix_reservations_start_dt_date', cast(cls.start_dt, Date)), db.Index('ix_reservations_end_dt_date', cast(cls.end_dt, Date)), db.Index('ix_reservations_start_dt_time', cast(cls.start_dt, Time)), db.Index('ix_reservations_end_dt_time', cast(cls.end_dt, Time)), db.CheckConstraint("rejection_reason != ''", 'rejection_reason_not_empty'), { 'schema': 'roombooking' }) id = db.Column(db.Integer, primary_key=True) created_dt = db.Column(UTCDateTime, nullable=False, default=now_utc) start_dt = db.Column(db.DateTime, nullable=False, index=True) end_dt = db.Column(db.DateTime, nullable=False, index=True) repeat_frequency = db.Column( PyIntEnum(RepeatFrequency), nullable=False, default=RepeatFrequency.NEVER) # week, month, year, etc. repeat_interval = db.Column(db.SmallInteger, nullable=False, default=0) # 1, 2, 3, etc. booked_for_id = db.Column( db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=True, # Must be nullable for legacy data :( ) booked_for_name = db.Column(db.String, nullable=False) created_by_id = db.Column( db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=True, # Must be nullable for legacy data :( ) room_id = db.Column(db.Integer, db.ForeignKey('roombooking.rooms.id'), nullable=False, index=True) state = db.Column(PyIntEnum(ReservationState), nullable=False, default=ReservationState.accepted) booking_reason = db.Column(db.Text, nullable=False) rejection_reason = db.Column(db.String, nullable=True) uses_vc = db.Column(db.Boolean, nullable=False, default=False) needs_vc_assistance = db.Column(db.Boolean, nullable=False, default=False) needs_assistance = db.Column(db.Boolean, nullable=False, default=False) link_id = db.Column(db.Integer, db.ForeignKey('roombooking.reservation_links.id'), nullable=True, index=True) end_notification_sent = db.Column(db.Boolean, nullable=False, default=False) edit_logs = db.relationship('ReservationEditLog', backref='reservation', cascade='all, delete-orphan', lazy='dynamic') occurrences = db.relationship('ReservationOccurrence', backref='reservation', cascade='all, delete-orphan', lazy='dynamic') #: The user this booking was made for. #: Assigning a user here also updates `booked_for_name`. booked_for_user = db.relationship('User', lazy=False, foreign_keys=[booked_for_id], backref=db.backref( 'reservations_booked_for', lazy='dynamic')) #: The user who created this booking. created_by_user = db.relationship('User', lazy=False, foreign_keys=[created_by_id], backref=db.backref('reservations', lazy='dynamic')) link = db.relationship('ReservationLink', lazy=True, backref=db.backref('reservation', uselist=False)) # relationship backrefs: # - room (Room.reservations) @hybrid_property def is_pending(self): return self.state == ReservationState.pending @hybrid_property def is_accepted(self): return self.state == ReservationState.accepted @hybrid_property def is_cancelled(self): return self.state == ReservationState.cancelled @hybrid_property def is_rejected(self): return self.state == ReservationState.rejected @hybrid_property def is_archived(self): return self.end_dt < datetime.now() @hybrid_property def is_repeating(self): return self.repeat_frequency != RepeatFrequency.NEVER @property def contact_email(self): return self.booked_for_user.email if self.booked_for_user else None @property def contact_phone(self): return self.booked_for_user.phone if self.booked_for_user else None @property def external_details_url(self): return url_for('rooms_new.booking_link', booking_id=self.id, _external=True) @property def location_name(self): return self.room.location_name @property def repetition(self): return self.repeat_frequency, self.repeat_interval @property def status_string(self): parts = [] if self.is_accepted: parts.append(_(u"Valid")) else: if self.is_cancelled: parts.append(_(u"Cancelled")) if self.is_rejected: parts.append(_(u"Rejected")) if not self.is_accepted: parts.append(_(u"Not confirmed")) if self.is_archived: parts.append(_(u"Archived")) else: parts.append(_(u"Live")) return u', '.join(map(unicode, parts)) @property def linked_object(self): return self.link.object if self.link else None @linked_object.setter def linked_object(self, obj): assert self.link is None self.link = ReservationLink(object=obj) @property def event(self): return self.link.event if self.link else None @return_ascii def __repr__(self): return format_repr(self, 'id', 'room_id', 'start_dt', 'end_dt', 'state', _text=self.booking_reason) @classmethod def create_from_data(cls, room, data, user, prebook=None, ignore_admin=False): """Creates a new reservation. :param room: The Room that's being booked. :param data: A dict containing the booking data, usually from a :class:`NewBookingConfirmForm` instance :param user: The :class:`.User` who creates the booking. :param prebook: Instead of determining the booking type from the user's permissions, always use the given mode. """ populate_fields = ('start_dt', 'end_dt', 'repeat_frequency', 'repeat_interval', 'room_id', 'contact_email', 'contact_phone', 'booking_reason', 'needs_assistance', 'uses_vc', 'needs_vc_assistance') if data['repeat_frequency'] == RepeatFrequency.NEVER and data[ 'start_dt'].date() != data['end_dt'].date(): raise ValueError('end_dt != start_dt for non-repeating booking') if prebook is None: prebook = not room.can_book(user, allow_admin=(not ignore_admin)) if prebook and not room.can_prebook( user, allow_admin=(not ignore_admin)): raise NoReportError(u'You cannot book this room') room.check_advance_days(data['end_dt'].date(), user) room.check_bookable_hours(data['start_dt'].time(), data['end_dt'].time(), user) reservation = cls() for field in populate_fields: if field in data: setattr(reservation, field, data[field]) reservation.room = room # if 'room_usage' is not specified, we'll take whatever is passed in 'booked_for_user' reservation.booked_for_user = data['booked_for_user'] if data.get( 'room_usage') != 'current_user' else user reservation.booked_for_name = reservation.booked_for_user.full_name reservation.state = ReservationState.pending if prebook else ReservationState.accepted reservation.created_by_user = user reservation.create_occurrences(True) if not any(occ.is_valid for occ in reservation.occurrences): raise NoReportError(_(u'Reservation has no valid occurrences')) db.session.flush() signals.rb.booking_created.send(reservation) notify_creation(reservation) return reservation @staticmethod def get_with_data(*args, **kwargs): filters = kwargs.pop('filters', None) limit = kwargs.pop('limit', None) offset = kwargs.pop('offset', 0) order = kwargs.pop('order', Reservation.start_dt) limit_per_room = kwargs.pop('limit_per_room', False) occurs_on = kwargs.pop('occurs_on') if kwargs: raise ValueError('Unexpected kwargs: {}'.format(kwargs)) query = Reservation.query.options(joinedload(Reservation.room)) if filters: query = query.filter(*filters) if occurs_on: query = query.filter( Reservation.id.in_( db.session.query( ReservationOccurrence.reservation_id).filter( ReservationOccurrence.date.in_(occurs_on), ReservationOccurrence.is_valid))) if limit_per_room and (limit or offset): query = limit_groups(query, Reservation, Reservation.room_id, order, limit, offset) query = query.order_by(order, Reservation.created_dt) if not limit_per_room: if limit: query = query.limit(limit) if offset: query = query.offset(offset) result = OrderedDict((r.id, {'reservation': r}) for r in query) if 'occurrences' in args: occurrence_data = OrderedMultiDict( db.session.query(ReservationOccurrence.reservation_id, ReservationOccurrence).filter( ReservationOccurrence.reservation_id.in_( result.iterkeys())).order_by( ReservationOccurrence.start_dt)) for id_, data in result.iteritems(): data['occurrences'] = occurrence_data.getlist(id_) return result.values() @staticmethod def find_overlapping_with(room, occurrences, skip_reservation_id=None): return Reservation.find( Reservation.room == room, Reservation.id != skip_reservation_id, ReservationOccurrence.is_valid, ReservationOccurrence.filter_overlap(occurrences), _join=ReservationOccurrence) def accept(self, user): self.state = ReservationState.accepted self.add_edit_log( ReservationEditLog(user_name=user.full_name, info=['Reservation accepted'])) notify_confirmation(self) signals.rb.booking_state_changed.send(self) valid_occurrences = self.occurrences.filter( ReservationOccurrence.is_valid).all() pre_occurrences = ReservationOccurrence.find_overlapping_with( self.room, valid_occurrences, self.id).all() for occurrence in pre_occurrences: if not occurrence.is_valid: continue occurrence.reject( user, u'Rejected due to collision with a confirmed reservation') def reset_approval(self, user): self.state = ReservationState.pending notify_reset_approval(self) self.add_edit_log( ReservationEditLog(user_name=user.full_name, info=['Requiring new approval due to change'])) def cancel(self, user, reason=None, silent=False): self.state = ReservationState.cancelled self.rejection_reason = reason or None self.occurrences.filter_by(is_valid=True).update( { ReservationOccurrence.state: ReservationOccurrenceState.cancelled, ReservationOccurrence.rejection_reason: reason }, synchronize_session='fetch') signals.rb.booking_state_changed.send(self) if not silent: notify_cancellation(self) log_msg = u'Reservation cancelled: {}'.format( reason) if reason else 'Reservation cancelled' self.add_edit_log( ReservationEditLog(user_name=user.full_name, info=[log_msg])) def reject(self, user, reason, silent=False): self.state = ReservationState.rejected self.rejection_reason = reason or None self.occurrences.filter_by(is_valid=True).update( { ReservationOccurrence.state: ReservationOccurrenceState.rejected, ReservationOccurrence.rejection_reason: reason }, synchronize_session='fetch') signals.rb.booking_state_changed.send(self) if not silent: notify_rejection(self) log_msg = u'Reservation rejected: {}'.format(reason) self.add_edit_log( ReservationEditLog(user_name=user.full_name, info=[log_msg])) def add_edit_log(self, edit_log): self.edit_logs.append(edit_log) db.session.flush() def can_accept(self, user, allow_admin=True): if user is None: return False return self.is_pending and self.room.can_moderate( user, allow_admin=allow_admin) def can_reject(self, user, allow_admin=True): if user is None: return False if self.is_rejected or self.is_cancelled: return False return self.room.can_moderate(user, allow_admin=allow_admin) def can_cancel(self, user, allow_admin=True): if user is None: return False if self.is_rejected or self.is_cancelled or self.is_archived: return False return self.is_owned_by(user) or self.is_booked_for(user) or ( allow_admin and rb_is_admin(user)) def can_edit(self, user, allow_admin=True): if user is None: return False if self.is_rejected or self.is_cancelled: return False if self.is_archived and not (allow_admin and rb_is_admin(user)): return False return self.is_owned_by(user) or self.is_booked_for( user) or self.room.can_manage(user, allow_admin=allow_admin) def can_delete(self, user, allow_admin=True): if user is None: return False return allow_admin and rb_is_admin(user) and (self.is_cancelled or self.is_rejected) def create_occurrences(self, skip_conflicts, user=None): ReservationOccurrence.create_series_for_reservation(self) db.session.flush() if user is None: user = self.created_by_user # Check for conflicts with nonbookable periods if not rb_is_admin(user) and not self.room.is_owned_by(user): nonbookable_periods = self.room.nonbookable_periods.filter( NonBookablePeriod.end_dt > self.start_dt) for occurrence in self.occurrences: if not occurrence.is_valid: continue for nbd in nonbookable_periods: if nbd.overlaps(occurrence.start_dt, occurrence.end_dt): if not skip_conflicts: raise ConflictingOccurrences() occurrence.cancel(user, u'Skipped due to nonbookable date', silent=True, propagate=False) break # Check for conflicts with blockings blocked_rooms = self.room.get_blocked_rooms( *(occurrence.start_dt for occurrence in self.occurrences)) for br in blocked_rooms: blocking = br.blocking if blocking.can_be_overridden(user, self.room): continue for occurrence in self.occurrences: if occurrence.is_valid and blocking.is_active_at( occurrence.start_dt.date()): # Cancel OUR occurrence msg = u'Skipped due to collision with a blocking ({})' occurrence.cancel(user, msg.format(blocking.reason), silent=True, propagate=False) # Check for conflicts with other occurrences conflicting_occurrences = self.get_conflicting_occurrences() for occurrence, conflicts in conflicting_occurrences.iteritems(): if not occurrence.is_valid: continue if conflicts['confirmed']: if not skip_conflicts: raise ConflictingOccurrences() # Cancel OUR occurrence msg = u'Skipped due to collision with {} reservation(s)' occurrence.cancel(user, msg.format(len(conflicts['confirmed'])), silent=True, propagate=False) elif conflicts['pending'] and self.is_accepted: # Reject OTHER occurrences for conflict in conflicts['pending']: conflict.reject( user, u'Rejected due to collision with a confirmed reservation' ) def find_excluded_days(self): return self.occurrences.filter(~ReservationOccurrence.is_valid) def find_overlapping(self): occurrences = self.occurrences.filter( ReservationOccurrence.is_valid).all() return Reservation.find_overlapping_with(self.room, occurrences, self.id) @locator_property def locator(self): return {'roomLocation': self.location_name, 'resvID': self.id} def get_conflicting_occurrences(self): valid_occurrences = self.occurrences.filter( ReservationOccurrence.is_valid).all() colliding_occurrences = ReservationOccurrence.find_overlapping_with( self.room, valid_occurrences, self.id).all() conflicts = defaultdict(lambda: dict(confirmed=[], pending=[])) for occurrence in valid_occurrences: for colliding in colliding_occurrences: if occurrence.overlaps(colliding): key = 'confirmed' if colliding.reservation.is_accepted else 'pending' conflicts[occurrence][key].append(colliding) return conflicts def is_booked_for(self, user): return user is not None and self.booked_for_user == user def is_owned_by(self, user): return self.created_by_user == user def modify(self, data, user): """Modifies an existing reservation. :param data: A dict containing the booking data, usually from a :class:`ModifyBookingForm` instance :param user: The :class:`.User` who modifies the booking. """ populate_fields = ('start_dt', 'end_dt', 'repeat_frequency', 'repeat_interval', 'booked_for_user', 'contact_email', 'contact_phone', 'booking_reason', 'needs_assistance', 'uses_vc', 'needs_vc_assistance') # fields affecting occurrences occurrence_fields = { 'start_dt', 'end_dt', 'repeat_frequency', 'repeat_interval' } # fields where date and time are compared separately date_time_fields = {'start_dt', 'end_dt'} # fields for the repetition repetition_fields = {'repeat_frequency', 'repeat_interval'} # pretty names for logging field_names = { 'start_dt/date': u"start date", 'end_dt/date': u"end date", 'start_dt/time': u"start time", 'end_dt/time': u"end time", 'repetition': u"booking type", 'booked_for_user': u"'Booked for' user", 'contact_email': u"contact email", 'contact_phone': u"contact phone number", 'booking_reason': u"booking reason", 'needs_assistance': u"option 'General Assistance'", 'uses_vc': u"option 'Uses Videoconference'", 'needs_vc_assistance': u"option 'Videoconference Setup Assistance'" } self.room.check_advance_days(data['end_dt'].date(), user) self.room.check_bookable_hours(data['start_dt'].time(), data['end_dt'].time(), user) if data['room_usage'] == 'current_user': data['booked_for_user'] = session.user changes = {} update_occurrences = False old_repetition = self.repetition for field in populate_fields: if field not in data: continue old = getattr(self, field) new = data[field] converter = unicode if old != new: # Booked for user updates the (redundant) name if field == 'booked_for_user': old = self.booked_for_name new = self.booked_for_name = data[field].full_name # Apply the change setattr(self, field, data[field]) # If any occurrence-related field changed we need to recreate the occurrences if field in occurrence_fields: update_occurrences = True # Record change for history entry if field in date_time_fields: # The date/time fields create separate entries for the date and time parts if old.date() != new.date(): changes[field + '/date'] = { 'old': old.date(), 'new': new.date(), 'converter': format_date } if old.time() != new.time(): changes[field + '/time'] = { 'old': old.time(), 'new': new.time(), 'converter': format_time } elif field in repetition_fields: # Repetition needs special handling since it consists of two fields but they are tied together # We simply update it whenever we encounter such a change; after the last change we end up with # the correct change data changes['repetition'] = { 'old': old_repetition, 'new': self.repetition, 'converter': lambda x: RepeatMapping.get_message(*x) } else: changes[field] = { 'old': old, 'new': new, 'converter': converter } if not changes: return False # Create a verbose log entry for the modification log = [u'Booking modified'] for field, change in changes.iteritems(): field_title = field_names.get(field, field) converter = change['converter'] old = to_unicode(converter(change['old'])) new = to_unicode(converter(change['new'])) if not old: log.append(u"The {} was set to '{}'".format(field_title, new)) elif not new: log.append(u"The {} was cleared".format(field_title)) else: log.append(u"The {} was changed from '{}' to '{}'".format( field_title, old, new)) self.edit_logs.append( ReservationEditLog(user_name=user.full_name, info=log)) # Recreate all occurrences if necessary if update_occurrences: cols = [ col.name for col in ReservationOccurrence.__table__.columns if not col.primary_key and col.name not in {'start_dt', 'end_dt'} ] old_occurrences = {occ.date: occ for occ in self.occurrences} self.occurrences.delete(synchronize_session='fetch') self.create_occurrences(True, user) db.session.flush() # Restore rejection data etc. for recreated occurrences for occurrence in self.occurrences: old_occurrence = old_occurrences.get(occurrence.date) # Copy data from old occurrence UNLESS the new one is invalid (e.g. because of collisions) # Otherwise we'd end up with valid occurrences ignoring collisions! if old_occurrence and occurrence.is_valid: for col in cols: setattr(occurrence, col, getattr(old_occurrence, col)) # Don't cause new notifications for the entire booking in case of daily repetition if self.repeat_frequency == RepeatFrequency.DAY and all( occ.notification_sent for occ in old_occurrences.itervalues()): for occurrence in self.occurrences: occurrence.notification_sent = True # Sanity check so we don't end up with an "empty" booking if not any(occ.is_valid for occ in self.occurrences): raise NoReportError(_(u'Reservation has no valid occurrences')) notify_modification(self, changes) return True
class VCRoomEventAssociation(db.Model): __tablename__ = 'vc_room_events' __table_args__ = {'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) #: Link type of the vc_room to a event/contribution/session link_type = db.Column(PyIntEnum(VCRoomLinkType), nullable=False) #: Id of the event/contribution/session block the vc_room is linked to link_id = db.Column(db.String, nullable=True) #: If the chatroom should be hidden on the event page show = db.Column(db.Boolean, nullable=False, default=False) #: videoconference plugin-specific data data = db.Column(JSON, nullable=False) #: The associated :class:VCRoom vc_room = db.relationship('VCRoom', lazy=False, backref=db.backref('events', cascade='all, delete-orphan')) #: The associated Event event_new = db.relationship('Event', lazy=True, backref=db.backref('vc_room_associations', lazy='dynamic')) @property def locator(self): return dict(self.event.getLocator(), service=self.vc_room.type, event_vc_room_id=self.id) @property def event(self): return ConferenceHolder().getById(str(self.event_id), True) @event.setter def event(self, event): self.event_id = int(event.getId()) @property def link_object(self): if self.link_type == VCRoomLinkType.event: return self.event elif self.link_type == VCRoomLinkType.contribution: return self.event.getContributionById(self.link_id) else: session_id, slot_id = self.link_id.split(':') sess = self.event.getSessionById(session_id) return sess.getSlotById(slot_id) if sess is not None else None @return_ascii def __repr__(self): return '<VCRoomEventAssociation({}, {})>'.format( self.event_id, self.vc_room) @classmethod @unify_event_args def find_for_event(cls, event, include_hidden=False, include_deleted=False, only_linked_to_event=False, **kwargs): """Returns a Query that retrieves the videoconference rooms for an event :param event: an indico Event :param only_linked_to_event: only retrieve the vc rooms linked to the whole event :param kwargs: extra kwargs to pass to ``find()`` """ if only_linked_to_event: kwargs['link_type'] = int(VCRoomLinkType.event) query = event.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): """Deletes a VC room from an event If the room is not used anywhere else, the room itself is also deleted. :param user: the user performing the deletion """ Logger.get('modules.vc').info( "Detaching VC room {} from event {} ({})".format( self.vc_room, self.event, self.link_object)) db.session.delete(self) db.session.flush() if not self.vc_room.events: Logger.get('modules.vc').info("Deleting VC room {}".format( self.vc_room)) if self.vc_room.status != VCRoomStatus.deleted: self.vc_room.plugin.delete_room(self.vc_room, self.event) notify_deleted(self.vc_room.plugin, self.vc_room, self, self.event, user) db.session.delete(self.vc_room)
@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} role_members_table = db.Table('role_members', db.metadata, db.Column('role_id', db.Integer, db.ForeignKey('events.roles.id'), primary_key=True, nullable=False, index=True), db.Column('user_id', db.Integer, db.ForeignKey('users.users.id'), primary_key=True, nullable=False, index=True), schema='events')
def filename(cls): """The name of the file""" return db.Column( db.String, nullable=not cls.file_required )
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}
def storage_file_id(cls): return db.Column( db.String, nullable=not cls.file_required )
def modified_dt(cls): return db.Column(UTCDateTime, nullable=True)