class UserSetting(JSONSettingsBase, db.Model): """User-specific settings""" __table_args__ = (db.Index(None, 'user_id', 'module', 'name'), db.Index(None, 'user_id', 'module'), db.UniqueConstraint('user_id', 'module', 'name'), db.CheckConstraint('module = lower(module)', 'lowercase_module'), db.CheckConstraint('name = lower(name)', 'lowercase_name'), { 'schema': 'users' }) user_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), nullable=False, index=True) user = db.relationship('User', lazy=True, backref=db.backref('_all_settings', lazy='dynamic', cascade='all, delete-orphan')) @return_ascii def __repr__(self): return '<UserSetting({}, {}, {}, {!r})>'.format( self.user_id, self.module, self.name, self.value)
def __table_args__(cls): return (db.Index('ix_uq_applications_name_lower', db.func.lower(cls.name), unique=True), db.Index(None, cls.system_app_type, unique=True, postgresql_where=db.text( 'system_app_type != {}'.format( SystemAppType.none.value))), { 'schema': 'oauth' })
def __table_args__(cls): return (db.Index('ix_uq_contribution_types_event_id_name_lower', cls.event_id, db.func.lower(cls.name), unique=True), { 'schema': 'events' })
def __auto_table_args(cls): args = [ db.Index('ix_{}_title_fts'.format(cls.__tablename__), db.func.to_tsvector('simple', cls.title), postgresql_using='gin') ] if cls.title_required: args.append(db.CheckConstraint("title != ''", 'valid_title')) return tuple(args)
def _make_uniques(allowed_link_types, extra_criteria=None): for link_type in allowed_link_types: where = ['link_type = {}'.format(link_type.value)] if extra_criteria is not None: where += list(extra_criteria) yield db.Index(None, *_columns_for_types[link_type], unique=True, postgresql_where=db.text(' AND '.join(where)))
def __table_args__(cls): return (db.Index('ix_timetable_entries_start_dt_desc', cls.start_dt.desc()), _make_check(TimetableEntryType.SESSION_BLOCK, 'session_block_id'), _make_check(TimetableEntryType.CONTRIBUTION, 'contribution_id'), _make_check(TimetableEntryType.BREAK, 'break_id'), db.CheckConstraint( "type != {} OR parent_id IS NULL".format( TimetableEntryType.SESSION_BLOCK), 'valid_parent'), { 'schema': 'events' })
class UserEmail(db.Model): __tablename__ = 'emails' __table_args__ = ( db.CheckConstraint('email = lower(email)', 'lowercase_email'), db.Index(None, 'email', unique=True, postgresql_where=db.text('NOT is_user_deleted')), db.Index( None, 'user_id', unique=True, postgresql_where=db.text('is_primary AND NOT is_user_deleted')), { 'schema': 'users' }) #: the unique id of the email address id = db.Column(db.Integer, primary_key=True) #: the id of the associated user user_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), nullable=False, index=True) #: the email address email = db.Column(db.String, nullable=False, index=True) #: if the email is the user's primary email is_primary = db.Column(db.Boolean, nullable=False, default=False) #: if the user is marked as deleted (e.g. due to a merge). DO NOT use this flag when actually deleting an email is_user_deleted = db.Column(db.Boolean, nullable=False, default=False) # relationship backrefs: # - user (User._all_emails) @return_ascii def __repr__(self): return '<UserEmail({}, {}, {})>'.format(self.id, self.email, self.is_primary, self.user)
def __auto_table_args(): return (db.Index(None, 'event_id', 'module', 'name'), db.Index(None, 'event_id', 'module'), { 'schema': 'events' })
class SubContribution(DescriptionMixin, AttachedItemsMixin, AttachedNotesMixin, db.Model): __tablename__ = 'subcontributions' __table_args__ = (db.Index(None, 'friendly_id', 'contribution_id', unique=True), { 'schema': 'events' }) PRELOAD_EVENT_ATTACHED_ITEMS = True PRELOAD_EVENT_NOTES = True ATTACHMENT_FOLDER_ID_COLUMN = 'subcontribution_id' possible_render_modes = {RenderMode.html, RenderMode.markdown} default_render_mode = RenderMode.markdown id = db.Column(db.Integer, primary_key=True) #: The human-friendly ID for the sub-contribution friendly_id = db.Column(db.Integer, nullable=False, default=_get_next_friendly_id) contribution_id = db.Column(db.Integer, db.ForeignKey('events.contributions.id'), index=True, nullable=False) position = db.Column(db.Integer, nullable=False, default=_get_next_position) title = db.Column(db.String, nullable=False) duration = db.Column(db.Interval, nullable=False) is_deleted = db.Column(db.Boolean, nullable=False, default=False) #: External references associated with this contribution references = db.relationship('SubContributionReference', lazy=True, cascade='all, delete-orphan', backref=db.backref('subcontribution', lazy=True)) #: Persons associated with this contribution person_links = db.relationship('SubContributionPersonLink', lazy=True, cascade='all, delete-orphan', backref=db.backref('subcontribution', lazy=True)) # relationship backrefs: # - attachment_folders (AttachmentFolder.subcontribution) # - contribution (Contribution.subcontributions) # - legacy_mapping (LegacySubContributionMapping.subcontribution) # - note (EventNote.subcontribution) def __init__(self, **kwargs): # explicitly initialize this relationship with None to avoid # an extra query to check whether there is an object associated # when assigning a new one (e.g. during cloning) kwargs.setdefault('note', None) super(SubContribution, self).__init__(**kwargs) @property def event(self): return self.contribution.event @locator_property def locator(self): return dict(self.contribution.locator, subcontrib_id=self.id) @property def is_protected(self): return self.contribution.is_protected @property def session(self): """Convenience property so all event entities have it""" return self.contribution.session if self.contribution.session_id is not None else None @property def timetable_entry(self): """Convenience property so all event entities have it""" return self.contribution.timetable_entry @property def speakers(self): return self.person_links @speakers.setter def speakers(self, value): self.person_links = value.keys() @property def location_parent(self): return self.contribution def get_access_list(self): return self.contribution.get_access_list() def get_manager_list(self, recursive=False): return self.contribution.get_manager_list(recursive=recursive) @return_ascii def __repr__(self): return format_repr(self, 'id', is_deleted=False, _text=self.title) def can_access(self, user, **kwargs): return self.contribution.can_access(user, **kwargs) def can_manage(self, user, role=None, **kwargs): return self.contribution.can_manage(user, role, **kwargs)
def __table_args__(cls): return (db.Index('ix_uq_groups_name_lower', db.func.lower(cls.name), unique=True), { 'schema': 'users' })
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 fossir.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), { 'schema': 'events' }) location_backref_name = 'sessions' disallowed_protection_modes = frozenset() inheriting_have_acl = True default_colors = ColorTuple('#202020', '#e3f2d3') allow_relationship_preloading = True PRELOAD_EVENT_ATTACHED_ITEMS = True PRELOAD_EVENT_NOTES = True ATTACHMENT_FOLDER_ID_COLUMN = 'session_id' possible_render_modes = {RenderMode.markdown} default_render_mode = RenderMode.markdown @declared_attr def __table_args__(cls): return auto_table_args(cls) id = db.Column(db.Integer, primary_key=True) #: The human-friendly ID for the session friendly_id = db.Column(db.Integer, nullable=False, default=_get_next_friendly_id) event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False) title = db.Column(db.String, nullable=False) code = db.Column(db.String, nullable=False, default='') default_contribution_duration = db.Column(db.Interval, nullable=False, default=timedelta(minutes=20)) is_poster = db.Column(db.Boolean, nullable=False, default=False) is_deleted = db.Column(db.Boolean, nullable=False, default=False) event = db.relationship( 'Event', lazy=True, backref=db.backref( 'sessions', primaryjoin='(Session.event_id == Event.id) & ~Session.is_deleted', cascade='all, delete-orphan', lazy=True)) acl_entries = db.relationship('SessionPrincipal', lazy=True, cascade='all, delete-orphan', collection_class=set, backref='session') blocks = db.relationship('SessionBlock', lazy=True, cascade='all, delete-orphan', backref=db.backref('session', lazy=False)) # relationship backrefs: # - attachment_folders (AttachmentFolder.session) # - contributions (Contribution.session) # - legacy_mapping (LegacySessionMapping.session) # - note (EventNote.session) def __init__(self, **kwargs): # explicitly initialize this relationship with None to avoid # an extra query to check whether there is an object associated # when assigning a new one (e.g. during cloning) kwargs.setdefault('note', None) super(Session, self).__init__(**kwargs) @classmethod def preload_acl_entries(cls, event): cls.preload_relationships(cls.query.with_parent(event), 'acl_entries') @property def location_parent(self): return self.event @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 fossir.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 fossir.modules.events.sessions.models.blocks import SessionBlock from fossir.modules.events.sessions.models.persons import SessionBlockPersonLink return (SessionBlockPersonLink.query.join(SessionBlock).filter( SessionBlock.session_id == self.id).distinct( SessionBlockPersonLink.person_id).all()) @locator_property def locator(self): return dict(self.event.locator, session_id=self.id) def get_non_inheriting_objects(self): """Get a set of child objects that do not inherit protection""" return get_non_inheriting_objects(self) @return_ascii def __repr__(self): return format_repr(self, 'id', is_poster=False, is_deleted=False, _text=self.title) def can_manage_contributions(self, user, allow_admin=True): """Check whether a user can manage contributions within the session.""" from fossir.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 fossir.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 APIKey(db.Model): """API keys for users""" __tablename__ = 'api_keys' __table_args__ = (db.Index(None, 'user_id', unique=True, postgresql_where=db.text('is_active')), { 'schema': 'users' }) #: api key id id = db.Column(db.Integer, primary_key=True) #: unique api key for a user token = db.Column(UUID, nullable=False, unique=True, default=lambda: unicode(uuid4())) #: secret key used for signed requests secret = db.Column(UUID, nullable=False, default=lambda: unicode(uuid4())) #: ID of the user associated with the key user_id = db.Column( db.Integer, db.ForeignKey('users.users.id'), nullable=False, index=True, ) #: if the key is the currently active key for the user is_active = db.Column(db.Boolean, nullable=False, default=True) #: if the key has been blocked by an admin is_blocked = db.Column(db.Boolean, nullable=False, default=False) #: if persistent signatures are allowed is_persistent_allowed = db.Column(db.Boolean, nullable=False, default=False) #: the time when the key has been created created_dt = db.Column(UTCDateTime, nullable=False, default=now_utc) #: the last time when the key has been used last_used_dt = db.Column(UTCDateTime, nullable=True) #: the last ip address from which the key has been used last_used_ip = db.Column(INET, nullable=True) #: the last URI this key was used with last_used_uri = db.Column(db.String, nullable=True) #: if the last use was from an authenticated request last_used_auth = db.Column(db.Boolean, nullable=True) #: the number of times the key has been used use_count = db.Column(db.Integer, nullable=False, default=0) #: the user associated with this API key user = db.relationship('User', lazy=False) @return_ascii def __repr__(self): return '<APIKey({}, {}, {})>'.format(self.token, self.user_id, self.last_used_dt or 'never') def register_used(self, ip, uri, authenticated): """Updates the last used information""" self.last_used_dt = now_utc() self.last_used_ip = ip self.last_used_uri = uri self.last_used_auth = authenticated self.use_count = APIKey.use_count + 1
class MenuEntry(MenuEntryMixin, db.Model): __tablename__ = 'menu_entries' __table_args__ = ( db.CheckConstraint( '(type IN ({type.internal_link.value}, {type.plugin_link.value}) AND name IS NOT NULL) OR ' '(type NOT IN ({type.internal_link.value}, {type.plugin_link.value}) and name IS NULL)' .format(type=MenuEntryType), 'valid_name'), db.CheckConstraint( '(type = {type.user_link.value}) = (link_url IS NOT NULL)'.format( type=MenuEntryType), 'valid_link_url'), db.CheckConstraint( '(type = {type.page.value} AND page_id IS NOT NULL) OR' ' (type != {type.page.value} AND page_id IS NULL)'.format( type=MenuEntryType), 'valid_page_id'), db.CheckConstraint( '(type = {type.plugin_link.value} AND plugin IS NOT NULL) OR' ' (type != {type.plugin_link.value} AND plugin IS NULL)'.format( type=MenuEntryType), 'valid_plugin'), db.CheckConstraint( '(type = {type.separator.value} AND title IS NULL) OR' ' (type IN ({type.user_link.value}, {type.page.value}) AND title IS NOT NULL) OR' ' (type NOT IN ({type.separator.value}, {type.user_link.value}, {type.page.value}))' .format(type=MenuEntryType), 'valid_title'), db.CheckConstraint("title != ''", 'title_not_empty'), db.Index( None, 'event_id', 'name', unique=True, postgresql_where=db.text( '(type = {type.internal_link.value} OR type = {type.plugin_link.value})' .format(type=MenuEntryType))), { 'schema': 'events' }) #: The ID of the menu entry id = db.Column(db.Integer, primary_key=True) #: The ID of the parent menu entry (NULL if root menu entry) parent_id = db.Column( db.Integer, db.ForeignKey('events.menu_entries.id'), index=True, nullable=True, ) #: The ID of the event which contains the menu event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False) #: Whether the entry is visible in the event's menu is_enabled = db.Column(db.Boolean, nullable=False, default=True) #: The title of the menu entry (to be displayed to the user) title = db.Column( db.String, nullable=True, ) #: The name of the menu entry (to uniquely identify a default entry for a given event) name = db.Column(db.String, nullable=True) #: The relative position of the entry in the menu position = db.Column(db.Integer, nullable=False, default=_get_next_position) #: Whether the menu entry should be opened in a new tab or window new_tab = db.Column(db.Boolean, nullable=False, default=False) #: The target URL of a custom link link_url = db.Column(db.String, nullable=True, default=None) #: The name of the plugin from which the entry comes from (NULL if the entry does not come from a plugin) plugin = db.Column(db.String, nullable=True) #: The page ID if the entry is a page page_id = db.Column(db.Integer, db.ForeignKey('events.pages.id'), nullable=True, index=True, default=None) #: The type of the menu entry type = db.Column(PyIntEnum(MenuEntryType), nullable=False) #: The Event containing the menu entry event = 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
class EventReminder(db.Model): """Email reminders for events""" __tablename__ = 'reminders' __table_args__ = (db.Index(None, 'scheduled_dt', postgresql_where=db.text('not is_sent')), { 'schema': 'events' }) #: The ID of the reminder id = db.Column(db.Integer, primary_key=True) #: The ID of the event event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False) #: The ID of the user who created the reminder creator_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=False) #: The date/time when the reminder was created created_dt = db.Column(UTCDateTime, nullable=False, default=now_utc) #: The date/time when the reminder should be sent scheduled_dt = db.Column(UTCDateTime, nullable=False) #: If the reminder has been sent is_sent = db.Column(db.Boolean, nullable=False, default=False) #: How long before the event start the reminder should be sent #: This is needed to update the `scheduled_dt` when changing the #: start time of the event. event_start_delta = db.Column(db.Interval, nullable=True) #: The recipients of the notification recipients = db.Column(ARRAY(db.String), nullable=False, default=[]) #: If the notification should also be sent to all event participants send_to_participants = db.Column(db.Boolean, nullable=False, default=False) #: If the notification should include a summary of the event's schedule. include_summary = db.Column(db.Boolean, nullable=False, default=False) #: The address to use as Reply-To in the notification email. reply_to_address = db.Column(db.String, nullable=False) #: Custom message to include in the email message = db.Column(db.String, nullable=False, default='') #: The user who created the reminder creator = db.relationship('User', lazy=True, backref=db.backref('event_reminders', lazy='dynamic')) #: The Event this reminder is associated with event = db.relationship('Event', lazy=True, backref=db.backref('reminders', lazy='dynamic')) @property def locator(self): return dict(self.event.locator, reminder_id=self.id) @property def all_recipients(self): """Returns all recipients of the notifications. This includes both explicit recipients and, if enabled, participants of the event. """ recipients = set(self.recipients) if self.send_to_participants: recipients.update( reg.email for reg in Registration.get_all_for_event(self.event)) recipients.discard( '') # just in case there was an empty email address somewhere return recipients @hybrid_property def is_relative(self): """Returns if the reminder is relative to the event time""" return self.event_start_delta is not None @is_relative.expression def is_relative(self): return self.event_start_delta != None # NOQA @property def is_overdue(self): return not self.is_sent and self.scheduled_dt <= now_utc() def send(self): """Sends the reminder to its recipients.""" self.is_sent = True recipients = self.all_recipients if not recipients: logger.info( 'Notification %s has no recipients; not sending anything', self) return email_tpl = make_reminder_email(self.event, self.include_summary, self.message) email = make_email(bcc_list=recipients, from_address=self.reply_to_address, template=email_tpl) send_email(email, self.event, 'Reminder', self.creator) @return_ascii def __repr__(self): return format_repr(self, 'id', 'event_id', 'scheduled_dt', is_sent=False)
class VCRoom(db.Model): __tablename__ = 'vc_rooms' __table_args__ = (db.Index(None, 'data', postgresql_using='gin'), {'schema': 'events'}) #: Videoconference room ID id = db.Column( db.Integer, primary_key=True ) #: Type of the videoconference room type = db.Column( db.String, nullable=False ) #: Name of the videoconference room name = db.Column( db.String, nullable=False ) #: Status of the videoconference room status = db.Column( PyIntEnum(VCRoomStatus), nullable=False ) #: ID of the creator created_by_id = db.Column( db.Integer, db.ForeignKey('users.users.id'), nullable=False, index=True ) #: Creation timestamp of the videoconference room created_dt = db.Column( UTCDateTime, nullable=False, default=now_utc ) #: Modification timestamp of the videoconference room modified_dt = db.Column( UTCDateTime ) #: videoconference plugin-specific data data = db.Column( JSONB, nullable=False ) #: The user who created the videoconference room created_by_user = db.relationship( 'User', lazy=True, backref=db.backref( 'vc_rooms', lazy='dynamic' ) ) # relationship backrefs: # - events (VCRoomEventAssociation.vc_room) @property def plugin(self): from fossir.modules.vc.util import get_vc_plugins return get_vc_plugins().get(self.type) @property def locator(self): return {'vc_room_id': self.id, 'service': self.type} @return_ascii def __repr__(self): return '<VCRoom({}, {}, {})>'.format(self.id, self.name, self.type)
class Contribution(DescriptionMixin, ProtectionManagersMixin, LocationMixin, AttachedItemsMixin, AttachedNotesMixin, PersonLinkDataMixin, AuthorsSpeakersMixin, CustomFieldsMixin, db.Model): __tablename__ = 'contributions' __auto_table_args = ( db.Index(None, 'friendly_id', 'event_id', unique=True, postgresql_where=db.text('NOT is_deleted')), db.Index(None, 'event_id', 'track_id'), db.Index(None, 'event_id', 'abstract_id'), db.Index(None, 'abstract_id', unique=True, postgresql_where=db.text('NOT is_deleted')), db.CheckConstraint( "session_block_id IS NULL OR session_id IS NOT NULL", 'session_block_if_session'), db.ForeignKeyConstraint( ['session_block_id', 'session_id'], ['events.session_blocks.id', 'events.session_blocks.session_id']), { 'schema': 'events' }) location_backref_name = 'contributions' disallowed_protection_modes = frozenset() inheriting_have_acl = True possible_render_modes = {RenderMode.html, RenderMode.markdown} default_render_mode = RenderMode.markdown allow_relationship_preloading = True PRELOAD_EVENT_ATTACHED_ITEMS = True PRELOAD_EVENT_NOTES = True ATTACHMENT_FOLDER_ID_COLUMN = 'contribution_id' @declared_attr def __table_args__(cls): return auto_table_args(cls) id = db.Column(db.Integer, primary_key=True) #: The human-friendly ID for the contribution friendly_id = db.Column(db.Integer, nullable=False, default=_get_next_friendly_id) event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False) session_id = db.Column(db.Integer, db.ForeignKey('events.sessions.id'), index=True, nullable=True) session_block_id = db.Column(db.Integer, db.ForeignKey('events.session_blocks.id'), index=True, nullable=True) track_id = db.Column(db.Integer, db.ForeignKey('events.tracks.id', ondelete='SET NULL'), index=True, nullable=True) abstract_id = db.Column(db.Integer, db.ForeignKey('event_abstracts.abstracts.id'), index=True, nullable=True) type_id = db.Column(db.Integer, db.ForeignKey('events.contribution_types.id'), index=True, nullable=True) title = db.Column(db.String, nullable=False) duration = db.Column(db.Interval, nullable=False) board_number = db.Column(db.String, nullable=False, default='') keywords = db.Column(ARRAY(db.String), nullable=False, default=[]) is_deleted = db.Column(db.Boolean, nullable=False, default=False) #: The last user-friendly sub-contribution ID _last_friendly_subcontribution_id = db.deferred( db.Column('last_friendly_subcontribution_id', db.Integer, nullable=False, default=0)) event = db.relationship( 'Event', lazy=True, backref=db.backref( 'contributions', primaryjoin= '(Contribution.event_id == Event.id) & ~Contribution.is_deleted', cascade='all, delete-orphan', lazy=True)) session = db.relationship( 'Session', lazy=True, backref=db.backref( 'contributions', primaryjoin= '(Contribution.session_id == Session.id) & ~Contribution.is_deleted', lazy=True)) session_block = db.relationship( 'SessionBlock', lazy=True, foreign_keys=[session_block_id], backref=db.backref( 'contributions', primaryjoin= '(Contribution.session_block_id == SessionBlock.id) & ~Contribution.is_deleted', lazy=True)) type = db.relationship('ContributionType', lazy=True, backref=db.backref('contributions', lazy=True)) acl_entries = db.relationship('ContributionPrincipal', lazy=True, cascade='all, delete-orphan', collection_class=set, backref='contribution') subcontributions = db.relationship( 'SubContribution', lazy=True, primaryjoin= '(SubContribution.contribution_id == Contribution.id) & ~SubContribution.is_deleted', order_by='SubContribution.position', cascade='all, delete-orphan', backref=db.backref( 'contribution', primaryjoin='SubContribution.contribution_id == Contribution.id', lazy=True)) abstract = db.relationship( 'Abstract', lazy=True, backref=db.backref( 'contribution', primaryjoin= '(Contribution.abstract_id == Abstract.id) & ~Contribution.is_deleted', lazy=True, uselist=False)) track = db.relationship( 'Track', lazy=True, backref=db.backref( 'contributions', primaryjoin= '(Contribution.track_id == Track.id) & ~Contribution.is_deleted', lazy=True, passive_deletes=True)) #: External references associated with this contribution references = db.relationship('ContributionReference', lazy=True, cascade='all, delete-orphan', backref=db.backref('contribution', lazy=True)) #: Persons associated with this contribution person_links = db.relationship('ContributionPersonLink', lazy=True, cascade='all, delete-orphan', backref=db.backref('contribution', lazy=True)) #: Data stored in abstract/contribution fields field_values = db.relationship('ContributionFieldValue', lazy=True, cascade='all, delete-orphan', backref=db.backref('contribution', lazy=True)) #: The accepted paper revision _accepted_paper_revision = db.relationship( 'PaperRevision', lazy=True, viewonly=True, uselist=False, primaryjoin= ('(PaperRevision._contribution_id == Contribution.id) & (PaperRevision.state == {})' .format(PaperRevisionState.accepted)), ) #: Paper files not submitted for reviewing pending_paper_files = db.relationship( 'PaperFile', lazy=True, viewonly=True, primaryjoin= '(PaperFile._contribution_id == Contribution.id) & (PaperFile.revision_id.is_(None))', ) #: Paper reviewing judges paper_judges = db.relationship('User', secondary='event_paper_reviewing.judges', collection_class=set, lazy=True, backref=db.backref( 'judge_for_contributions', collection_class=set, lazy=True)) #: Paper content reviewers paper_content_reviewers = db.relationship( 'User', secondary='event_paper_reviewing.content_reviewers', collection_class=set, lazy=True, backref=db.backref('content_reviewer_for_contributions', collection_class=set, lazy=True)) #: Paper layout reviewers paper_layout_reviewers = db.relationship( 'User', secondary='event_paper_reviewing.layout_reviewers', collection_class=set, lazy=True, backref=db.backref('layout_reviewer_for_contributions', collection_class=set, lazy=True)) @declared_attr def _paper_last_revision(cls): # Incompatible with joinedload subquery = (db.select([ db.func.max(PaperRevision.submitted_dt) ]).where(PaperRevision._contribution_id == cls.id).correlate_except( PaperRevision).as_scalar()) return db.relationship('PaperRevision', uselist=False, lazy=True, viewonly=True, primaryjoin=db.and_( PaperRevision._contribution_id == cls.id, PaperRevision.submitted_dt == subquery)) # relationship backrefs: # - _paper_files (PaperFile._contribution) # - _paper_revisions (PaperRevision._contribution) # - attachment_folders (AttachmentFolder.contribution) # - legacy_mapping (LegacyContributionMapping.contribution) # - note (EventNote.contribution) # - timetable_entry (TimetableEntry.contribution) # - vc_room_associations (VCRoomEventAssociation.linked_contrib) @declared_attr def is_scheduled(cls): from fossir.modules.events.timetable.models.entries import TimetableEntry query = (db.exists([1]).where(TimetableEntry.contribution_id == cls.id).correlate_except(TimetableEntry)) return db.column_property(query, deferred=True) @declared_attr def subcontribution_count(cls): from fossir.modules.events.contributions.models.subcontributions import SubContribution query = (db.select([db.func.count(SubContribution.id)]).where( (SubContribution.contribution_id == cls.id) & ~SubContribution.is_deleted).correlate_except(SubContribution)) return db.column_property(query, deferred=True) @declared_attr def _paper_revision_count(cls): query = (db.select([db.func.count(PaperRevision.id) ]).where(PaperRevision._contribution_id == cls.id).correlate_except(PaperRevision)) return db.column_property(query, deferred=True) def __init__(self, **kwargs): # explicitly initialize those relationships with None to avoid # an extra query to check whether there is an object associated # when assigning a new one (e.g. during cloning) kwargs.setdefault('note', None) kwargs.setdefault('timetable_entry', None) super(Contribution, self).__init__(**kwargs) @classmethod def preload_acl_entries(cls, event): cls.preload_relationships(cls.query.with_parent(event), 'acl_entries') @property def location_parent(self): if self.session_block_id is not None: return self.session_block elif self.session_id is not None: return self.session else: return self.event @property def protection_parent(self): return self.session if self.session_id is not None else self.event @property def start_dt(self): return self.timetable_entry.start_dt if self.timetable_entry else None @property def end_dt(self): return self.timetable_entry.start_dt + self.duration if self.timetable_entry else None @property def start_dt_poster(self): if self.session and self.session.is_poster and self.timetable_entry and self.timetable_entry.parent: return self.timetable_entry.parent.start_dt @property def end_dt_poster(self): if self.session and self.session.is_poster and self.timetable_entry and self.timetable_entry.parent: return self.timetable_entry.parent.end_dt @property def duration_poster(self): if self.session and self.session.is_poster and self.timetable_entry and self.timetable_entry.parent: return self.timetable_entry.parent.duration @property def start_dt_display(self): """The displayed start time of the contribution. This is the start time of the poster session if applicable, otherwise the start time of the contribution itself. """ return self.start_dt_poster or self.start_dt @property def end_dt_display(self): """The displayed end time of the contribution. This is the end time of the poster session if applicable, otherwise the end time of the contribution itself. """ return self.end_dt_poster or self.end_dt @property def duration_display(self): """The displayed duration of the contribution. This is the duration of the poster session if applicable, otherwise the duration of the contribution itself. """ return self.duration_poster or self.duration @property def submitters(self): return { person_link for person_link in self.person_links if person_link.is_submitter } @locator_property def locator(self): return dict(self.event.locator, contrib_id=self.id) @property def verbose_title(self): return '#{} ({})'.format(self.friendly_id, self.title) @property def paper(self): return Paper(self) if self._paper_last_revision else None def is_paper_reviewer(self, user): return user in self.paper_content_reviewers or user in self.paper_layout_reviewers @return_ascii def __repr__(self): return format_repr(self, 'id', is_deleted=False, _text=self.title) def can_manage(self, user, role=None, allow_admin=True, check_parent=True, explicit_role=False): if super(Contribution, self).can_manage(user, role, allow_admin=allow_admin, check_parent=check_parent, explicit_role=explicit_role): return True if (check_parent and self.session_id is not None and self.session.can_manage(user, 'coordinate', allow_admin=allow_admin, explicit_role=explicit_role) and session_coordinator_priv_enabled(self.event, 'manage-contributions')): return True return False def get_non_inheriting_objects(self): """Get a set of child objects that do not inherit protection.""" return get_non_inheriting_objects(self) def is_user_associated(self, user, check_abstract=False): if user is None: return False if check_abstract and self.abstract and self.abstract.submitter == user: return True return any(pl.person.user == user for pl in self.person_links if pl.person.user)
class RegistrationForm(db.Model): """A registration form for an event""" __tablename__ = 'forms' __table_args__ = ( db.Index( 'ix_uq_forms_participation', 'event_id', unique=True, postgresql_where=db.text('is_participation AND NOT is_deleted')), db.UniqueConstraint( 'id', 'event_id'), # useless but needed for the registrations fkey { 'schema': 'event_registration' }) #: The ID of the object id = db.Column(db.Integer, primary_key=True) #: The ID of the event event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False) #: The title of the registration form title = db.Column(db.String, nullable=False) #: Whether it's the 'Participants' form of a meeting/lecture is_participation = db.Column(db.Boolean, nullable=False, default=False) # An introduction text for users introduction = db.Column(db.Text, nullable=False, default='') #: Contact information for registrants contact_info = db.Column(db.String, nullable=False, default='') #: Datetime when the registration form is open start_dt = db.Column(UTCDateTime, nullable=True) #: Datetime when the registration form is closed end_dt = db.Column(UTCDateTime, nullable=True) #: Whether registration modifications are allowed modification_mode = db.Column(PyIntEnum(ModificationMode), nullable=False, default=ModificationMode.not_allowed) #: Datetime when the modification period is over modification_end_dt = db.Column(UTCDateTime, nullable=True) #: Whether the registration has been marked as deleted is_deleted = db.Column(db.Boolean, nullable=False, default=False) #: Whether users must be logged in to register require_login = db.Column(db.Boolean, nullable=False, default=False) #: Whether registrations must be associated with an fossir account require_user = db.Column(db.Boolean, nullable=False, default=False) #: Maximum number of registrations allowed registration_limit = db.Column(db.Integer, nullable=True) #: Whether registrations should be displayed in the participant list publish_registrations_enabled = db.Column(db.Boolean, nullable=False, default=False) #: Whether to display the number of registrations publish_registration_count = db.Column(db.Boolean, nullable=False, default=False) #: Whether checked-in status should be displayed in the event pages and participant list publish_checkin_enabled = db.Column(db.Boolean, nullable=False, default=False) #: Whether registrations must be approved by a manager moderation_enabled = db.Column(db.Boolean, nullable=False, default=False) #: The base fee users have to pay when registering base_price = db.Column( db.Numeric(8, 2), # max. 999999.99 nullable=False, default=0) #: Currency for prices in the registration form currency = db.Column(db.String, nullable=False) #: Notifications sender address notification_sender_address = db.Column(db.String, nullable=True) #: Custom message to include in emails for pending registrations message_pending = db.Column(db.Text, nullable=False, default='') #: Custom message to include in emails for unpaid registrations message_unpaid = db.Column(db.Text, nullable=False, default='') #: Custom message to include in emails for complete registrations message_complete = db.Column(db.Text, nullable=False, default='') #: Whether the manager notifications for this event are enabled manager_notifications_enabled = db.Column(db.Boolean, nullable=False, default=False) #: List of emails that should receive management notifications manager_notification_recipients = db.Column(ARRAY(db.String), nullable=False, default=[]) #: Whether tickets are enabled for this form tickets_enabled = db.Column(db.Boolean, nullable=False, default=False) #: Whether to send tickets by e-mail ticket_on_email = db.Column(db.Boolean, nullable=False, default=True) #: Whether to show a ticket download link on the event homepage ticket_on_event_page = db.Column(db.Boolean, nullable=False, default=True) #: Whether to show a ticket download link on the registration summary page ticket_on_summary_page = db.Column(db.Boolean, nullable=False, default=True) #: The ID of the template used to generate tickets ticket_template_id = db.Column(db.Integer, db.ForeignKey(DesignerTemplate.id), nullable=True, index=True) #: The Event containing this registration form event = db.relationship( 'Event', lazy=True, backref=db.backref( 'registration_forms', primaryjoin= '(RegistrationForm.event_id == Event.id) & ~RegistrationForm.is_deleted', cascade='all, delete-orphan', lazy=True)) #: The template used to generate tickets ticket_template = db.relationship('DesignerTemplate', lazy=True, foreign_keys=ticket_template_id, backref=db.backref('ticket_for_regforms', lazy=True)) # The items (sections, text, fields) in the form form_items = db.relationship('RegistrationFormItem', lazy=True, cascade='all, delete-orphan', order_by='RegistrationFormItem.position', backref=db.backref('registration_form', lazy=True)) #: The registrations associated with this form registrations = db.relationship( 'Registration', lazy=True, cascade='all, delete-orphan', foreign_keys=[Registration.registration_form_id], backref=db.backref('registration_form', lazy=True)) #: The registration invitations associated with this form invitations = db.relationship('RegistrationInvitation', lazy=True, cascade='all, delete-orphan', backref=db.backref('registration_form', lazy=True)) @hybrid_property def has_ended(self): return self.end_dt is not None and self.end_dt <= now_utc() @has_ended.expression def has_ended(cls): return cls.end_dt.isnot(None) & (cls.end_dt <= now_utc()) @hybrid_property def has_started(self): return self.start_dt is not None and self.start_dt <= now_utc() @has_started.expression def has_started(cls): return cls.start_dt.isnot(None) & (cls.start_dt <= now_utc()) @hybrid_property def is_modification_open(self): end_dt = self.modification_end_dt if self.modification_end_dt else self.end_dt return now_utc() <= end_dt if end_dt else True @is_modification_open.expression def is_modification_open(self): now = now_utc() return now <= db.func.coalesce(self.modification_end_dt, self.end_dt, now) @hybrid_property def is_open(self): return not self.is_deleted and self.has_started and not self.has_ended @is_open.expression def is_open(cls): return ~cls.is_deleted & cls.has_started & ~cls.has_ended @hybrid_property def is_scheduled(self): return not self.is_deleted and self.start_dt is not None @is_scheduled.expression def is_scheduled(cls): return ~cls.is_deleted & cls.start_dt.isnot(None) @property def locator(self): return dict(self.event.locator, reg_form_id=self.id) @property def active_fields(self): return [ field for field in self.form_items if (field.is_field and field.is_enabled and not field.is_deleted and field.parent.is_enabled and not field.parent.is_deleted) ] @property def sections(self): return [x for x in self.form_items if x.is_section] @property def disabled_sections(self): return [ x for x in self.sections if not x.is_visible and not x.is_deleted ] @property def limit_reached(self): return self.registration_limit and len( self.active_registrations) >= self.registration_limit @property def is_active(self): return self.is_open and not self.limit_reached @property @memoize_request def active_registrations(self): return (Registration.query.with_parent(self).filter( Registration.is_active).options(subqueryload('data')).all()) @property def sender_address(self): contact_email = self.event.contact_emails[ 0] if self.event.contact_emails else None return self.notification_sender_address or contact_email @return_ascii def __repr__(self): return '<RegistrationForm({}, {}, {})>'.format(self.id, self.event_id, self.title) def is_modification_allowed(self, registration): """Checks whether a registration may be modified""" if not registration.is_active: return False elif self.modification_mode == ModificationMode.allowed_always: return True elif self.modification_mode == ModificationMode.allowed_until_payment: return not registration.is_paid else: return False def can_submit(self, user): return self.is_active and (not self.require_login or user) @memoize_request def get_registration(self, user=None, uuid=None, email=None): """Retrieves registrations for this registration form by user or uuid""" if (bool(user) + bool(uuid) + bool(email)) != 1: raise ValueError( "Exactly one of `user`, `uuid` and `email` must be specified") if user: return user.registrations.filter_by(registration_form=self).filter( Registration.is_active).first() if uuid: try: UUID(hex=uuid) except ValueError: raise BadRequest('Malformed registration token') return Registration.query.with_parent(self).filter_by( uuid=uuid).filter(Registration.is_active).first() if email: return Registration.query.with_parent(self).filter_by( email=email).filter(Registration.is_active).first() def render_base_price(self): return format_currency(self.base_price, self.currency, locale=session.lang or 'en_GB') def get_personal_data_field_id(self, personal_data_type): """Returns the field id corresponding to the personal data field with the given name.""" for field in self.active_fields: if (isinstance(field, RegistrationFormPersonalDataField) and field.personal_data_type == personal_data_type): return field.id
class User(PersonMixin, db.Model): """fossir 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.Index(None, 'is_system', unique=True, postgresql_where=db.text('is_system')), db.CheckConstraint( 'NOT is_system OR (NOT is_blocked AND NOT is_pending AND NOT is_deleted)', 'valid_system_user'), db.CheckConstraint('id != merged_into_id', 'not_merged_self'), db.CheckConstraint( "is_pending OR (first_name != '' AND last_name != '')", 'not_pending_proper_names'), { 'schema': 'users' }) #: the unique id of the user id = db.Column(db.Integer, primary_key=True) #: the first name of the user first_name = db.Column(db.String, nullable=False, index=True) #: the last/family name of the user last_name = db.Column(db.String, nullable=False, index=True) # the title of the user - you usually want the `title` property! _title = db.Column('title', PyIntEnum(UserTitle), nullable=False, default=UserTitle.none) #: the phone number of the user phone = db.Column(db.String, nullable=False, default='') #: the address of the user address = db.Column(db.Text, nullable=False, default='') #: the id of the user this user has been merged into merged_into_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), nullable=True) #: if the user is the default system user is_system = db.Column(db.Boolean, nullable=False, default=False) #: if the user is an administrator with unrestricted access to everything is_admin = db.Column(db.Boolean, nullable=False, default=False, index=True) #: if the user has been blocked is_blocked = db.Column(db.Boolean, nullable=False, default=False) #: if the user is pending (e.g. never logged in, only added to some list) is_pending = db.Column(db.Boolean, nullable=False, default=False) #: if the user is deleted (e.g. due to a merge) is_deleted = db.Column('is_deleted', db.Boolean, nullable=False, default=False) _affiliation = db.relationship('UserAffiliation', lazy=False, uselist=False, cascade='all, delete-orphan', backref=db.backref('user', lazy=True)) _primary_email = db.relationship( 'UserEmail', lazy=False, uselist=False, cascade='all, delete-orphan', primaryjoin='(User.id == UserEmail.user_id) & UserEmail.is_primary') _secondary_emails = db.relationship( 'UserEmail', lazy=True, cascade='all, delete-orphan', collection_class=set, primaryjoin='(User.id == UserEmail.user_id) & ~UserEmail.is_primary') _all_emails = db.relationship('UserEmail', lazy=True, viewonly=True, primaryjoin='User.id == UserEmail.user_id', collection_class=set, backref=db.backref('user', lazy=False)) #: the affiliation of the user affiliation = association_proxy('_affiliation', 'name', creator=lambda v: UserAffiliation(name=v)) #: the primary email address of the user email = association_proxy( '_primary_email', 'email', creator=lambda v: UserEmail(email=v, is_primary=True)) #: any additional emails the user might have secondary_emails = association_proxy('_secondary_emails', 'email', creator=lambda v: UserEmail(email=v)) #: all emails of the user. read-only; use it only for searching by email! also, do not use it between #: modifying `email` or `secondary_emails` and a session expire/commit! all_emails = association_proxy('_all_emails', 'email') # read-only! #: the user this user has been merged into merged_into_user = db.relationship( 'User', lazy=True, backref=db.backref('merged_from_users', lazy=True), remote_side='User.id', ) #: the users's favorite users favorite_users = db.relationship( 'User', secondary=favorite_user_table, primaryjoin=id == favorite_user_table.c.user_id, secondaryjoin=(id == favorite_user_table.c.target_id) & ~is_deleted, lazy=True, collection_class=set, backref=db.backref('favorite_of', lazy=True, collection_class=set), ) #: the users's favorite categories favorite_categories = db.relationship( 'Category', secondary=favorite_category_table, lazy=True, collection_class=set, backref=db.backref('favorite_of', lazy=True, collection_class=set), ) #: the user's category suggestions suggested_categories = db.relationship( 'SuggestedCategory', lazy='dynamic', order_by='SuggestedCategory.score.desc()', cascade='all, delete-orphan', backref=db.backref('user', lazy=True)) #: the active API key of the user api_key = db.relationship( 'APIKey', lazy=True, uselist=False, cascade='all, delete-orphan', primaryjoin='(User.id == APIKey.user_id) & APIKey.is_active', back_populates='user') #: the previous API keys of the user old_api_keys = db.relationship( 'APIKey', lazy=True, cascade='all, delete-orphan', order_by='APIKey.created_dt.desc()', primaryjoin='(User.id == APIKey.user_id) & ~APIKey.is_active', back_populates='user') #: the identities used by this user identities = db.relationship('Identity', lazy=True, cascade='all, delete-orphan', collection_class=set, backref=db.backref('user', lazy=False)) # relationship backrefs: # - _all_settings (UserSetting.user) # - abstract_comments (AbstractComment.user) # - abstract_email_log_entries (AbstractEmailLogEntry.user) # - abstract_reviewer_for_tracks (Track.abstract_reviewers) # - abstract_reviews (AbstractReview.user) # - abstracts (Abstract.submitter) # - agreements (Agreement.user) # - attachment_files (AttachmentFile.user) # - attachments (Attachment.user) # - blockings (Blocking.created_by_user) # - content_reviewer_for_contributions (Contribution.paper_content_reviewers) # - convener_for_tracks (Track.conveners) # - created_events (Event.creator) # - event_log_entries (EventLogEntry.user) # - event_notes_revisions (EventNoteRevision.user) # - event_persons (EventPerson.user) # - event_reminders (EventReminder.creator) # - 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) # - local_groups (LocalGroup.members) # - merged_from_users (User.merged_into_user) # - modified_abstract_comments (AbstractComment.modified_by) # - modified_abstracts (Abstract.modified_by) # - modified_review_comments (PaperReviewComment.modified_by) # - oauth_tokens (OAuthToken.user) # - owned_rooms (Room.owner) # - paper_competences (PaperCompetence.user) # - paper_reviews (PaperReview.user) # - paper_revisions (PaperRevision.submitter) # - registrations (Registration.user) # - requests_created (Request.created_by_user) # - requests_processed (Request.processed_by_user) # - reservations (Reservation.created_by_user) # - reservations_booked_for (Reservation.booked_for_user) # - review_comments (PaperReviewComment.user) # - static_sites (StaticSite.creator) # - survey_submissions (SurveySubmission.user) # - vc_rooms (VCRoom.created_by_user) @staticmethod def get_system_user(): return User.query.filter_by(is_system=True).one() @property def as_principal(self): """The serializable principal identifier of this user""" return 'User', self.id @property def as_avatar(self): # TODO: remove this after DB is free of Avatars from fossir.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 fossir.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 != 'fossir'} @property def local_identities(self): """The local identities of the user""" return {x for x in self.identities if x.provider == 'fossir'} @property def local_identity(self): """The main (most recently used) local identity""" identities = sorted(self.local_identities, key=attrgetter('safe_last_login_dt'), reverse=True) return identities[0] if identities else None @property def secondary_local_identities(self): """The local identities of the user except the main one""" return self.local_identities - {self.local_identity} @locator_property def locator(self): return {'user_id': self.id} @cached_property def settings(self): """Returns the user settings proxy for this user""" from fossir.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 'fossir.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
class RegistrationFormItem(db.Model): """Generic registration form item""" __tablename__ = 'form_items' __table_args__ = ( db.CheckConstraint( "(input_type IS NULL) = (type NOT IN ({t.field}, {t.field_pd}))". format(t=RegistrationFormItemType), name='valid_input'), db.CheckConstraint("NOT is_manager_only OR type = {type}".format( type=RegistrationFormItemType.section), name='valid_manager_only'), db.CheckConstraint( "(type IN ({t.section}, {t.section_pd})) = (parent_id IS NULL)". format(t=RegistrationFormItemType), name='top_level_sections'), db.CheckConstraint( "(type != {type}) = (personal_data_type IS NULL)".format( type=RegistrationFormItemType.field_pd), name='pd_field_type'), db.CheckConstraint( "NOT is_deleted OR (type NOT IN ({t.section_pd}, {t.field_pd}))". format(t=RegistrationFormItemType), name='pd_not_deleted'), db.CheckConstraint("is_enabled OR type != {type}".format( type=RegistrationFormItemType.section_pd), name='pd_section_enabled'), db.CheckConstraint( "is_enabled OR type != {type} OR personal_data_type NOT IN " "({pt.email}, {pt.first_name}, {pt.last_name})".format( type=RegistrationFormItemType.field_pd, pt=PersonalDataType), name='pd_field_enabled'), db.CheckConstraint( "is_required OR type != {type} OR personal_data_type NOT IN " "({pt.email}, {pt.first_name}, {pt.last_name})".format( type=RegistrationFormItemType.field_pd, pt=PersonalDataType), name='pd_field_required'), db.CheckConstraint( "current_data_id IS NULL OR type IN ({t.field}, {t.field_pd})". format(t=RegistrationFormItemType), name='current_data_id_only_field'), db.Index('ix_uq_form_items_pd_section', 'registration_form_id', unique=True, postgresql_where=db.text('type = {type}'.format( type=RegistrationFormItemType.section_pd))), db.Index('ix_uq_form_items_pd_field', 'registration_form_id', 'personal_data_type', unique=True, postgresql_where=db.text('type = {type}'.format( type=RegistrationFormItemType.field_pd))), { 'schema': 'event_registration' }) __mapper_args__ = {'polymorphic_on': 'type', 'polymorphic_identity': None} #: The ID of the object id = db.Column(db.Integer, primary_key=True) #: The ID of the registration form registration_form_id = db.Column( db.Integer, db.ForeignKey('event_registration.forms.id'), index=True, nullable=False) #: The type of the registration form item type = db.Column(PyIntEnum(RegistrationFormItemType), nullable=False) #: The type of a personal data field personal_data_type = db.Column(PyIntEnum(PersonalDataType), nullable=True) #: The ID of the parent form item parent_id = db.Column(db.Integer, db.ForeignKey('event_registration.form_items.id'), index=True, nullable=True) position = db.Column(db.Integer, nullable=False, default=_get_next_position) #: The title of this field title = db.Column(db.String, nullable=False) #: Description of this field description = db.Column(db.String, nullable=False, default='') #: Whether the field is enabled is_enabled = db.Column(db.Boolean, nullable=False, default=True) #: Whether field has been "deleted" is_deleted = db.Column(db.Boolean, nullable=False, default=False) #: determines if the field is mandatory is_required = db.Column(db.Boolean, nullable=False, default=False) #: if the section is only accessible to managers is_manager_only = db.Column(db.Boolean, nullable=False, default=False) #: input type of this field input_type = db.Column(db.String, nullable=True) #: unversioned field data data = db.Column(JSON, nullable=False, default=lambda: None) #: The ID of the latest data current_data_id = db.Column(db.Integer, db.ForeignKey( 'event_registration.form_field_data.id', use_alter=True), index=True, nullable=True) #: The latest value of the field current_data = db.relationship( 'RegistrationFormFieldData', primaryjoin= 'RegistrationFormItem.current_data_id == RegistrationFormFieldData.id', foreign_keys=current_data_id, lazy=True, post_update=True) #: The list of all versions of the field data data_versions = db.relationship( 'RegistrationFormFieldData', primaryjoin= 'RegistrationFormItem.id == RegistrationFormFieldData.field_id', foreign_keys='RegistrationFormFieldData.field_id', lazy=True, cascade='all, delete-orphan', backref=db.backref('field', lazy=False)) # The children of the item and the parent backref children = db.relationship('RegistrationFormItem', lazy=True, order_by='RegistrationFormItem.position', backref=db.backref('parent', lazy=False, remote_side=[id])) # relationship backrefs: # - parent (RegistrationFormItem.children) # - registration_form (RegistrationForm.form_items) @property def view_data(self): """Returns object with data that Angular can understand""" return dict(id=self.id, description=self.description, position=self.position) @hybrid_property def is_section(self): return self.type in { RegistrationFormItemType.section, RegistrationFormItemType.section_pd } @is_section.expression def is_section(cls): return cls.type.in_([ RegistrationFormItemType.section, RegistrationFormItemType.section_pd ]) @hybrid_property def is_field(self): return self.type in { RegistrationFormItemType.field, RegistrationFormItemType.field_pd } @is_field.expression def is_field(cls): return cls.type.in_([ RegistrationFormItemType.field, RegistrationFormItemType.field_pd ]) @hybrid_property def is_visible(self): return self.is_enabled and not self.is_deleted and ( self.parent_id is None or self.parent.is_visible) @is_visible.expression def is_visible(cls): sections = aliased(RegistrationFormSection) query = (db.session.query(literal(True)).filter( sections.id == cls.parent_id).filter(~sections.is_deleted).filter( sections.is_enabled).exists()) return cls.is_enabled & ~cls.is_deleted & ( (cls.parent_id == None) | query) # noqa @return_ascii def __repr__(self): return format_repr(self, 'id', 'registration_form_id', is_enabled=True, is_deleted=False, is_manager_only=False, _text=self.title)
class PaperRevision(ProposalRevisionMixin, RenderModeMixin, db.Model): __tablename__ = 'revisions' __table_args__ = ( db.Index(None, 'contribution_id', unique=True, postgresql_where=db.text('state = {}'.format( PaperRevisionState.accepted))), db.UniqueConstraint('contribution_id', 'submitted_dt'), db.CheckConstraint( '(state IN ({}, {}, {})) = (judge_id IS NOT NULL)'.format( PaperRevisionState.accepted, PaperRevisionState.rejected, PaperRevisionState.to_be_corrected), name='judge_if_judged'), db.CheckConstraint( '(state IN ({}, {}, {})) = (judgment_dt IS NOT NULL)'.format( PaperRevisionState.accepted, PaperRevisionState.rejected, PaperRevisionState.to_be_corrected), name='judgment_dt_if_judged'), { 'schema': 'event_paper_reviewing' }) possible_render_modes = {RenderMode.markdown} default_render_mode = RenderMode.markdown proposal_attr = 'paper' id = db.Column(db.Integer, primary_key=True) state = db.Column(PyIntEnum(PaperRevisionState), nullable=False, default=PaperRevisionState.submitted) _contribution_id = db.Column('contribution_id', db.Integer, db.ForeignKey('events.contributions.id'), index=True, nullable=False) submitter_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=False) submitted_dt = db.Column(UTCDateTime, nullable=False, default=now_utc) judge_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=True) judgment_dt = db.Column(UTCDateTime, nullable=True) _judgment_comment = db.Column('judgment_comment', db.Text, nullable=False, default='') _contribution = db.relationship('Contribution', lazy=True, backref=db.backref( '_paper_revisions', lazy=True, order_by=submitted_dt.asc())) submitter = db.relationship('User', lazy=True, foreign_keys=submitter_id, backref=db.backref('paper_revisions', lazy='dynamic')) judge = db.relationship('User', lazy=True, foreign_keys=judge_id, backref=db.backref('judged_papers', lazy='dynamic')) judgment_comment = RenderModeMixin.create_hybrid_property( '_judgment_comment') # relationship backrefs: # - comments (PaperReviewComment.paper_revision) # - files (PaperFile.paper_revision) # - reviews (PaperReview.revision) def __init__(self, *args, **kwargs): paper = kwargs.pop('paper', None) if paper: kwargs.setdefault('_contribution', paper.contribution) super(PaperRevision, self).__init__(*args, **kwargs) @return_ascii def __repr__(self): return format_repr(self, 'id', '_contribution_id', state=None) @locator_property def locator(self): return dict(self.paper.locator, revision_id=self.id) @property def paper(self): return self._contribution.paper @property def is_last_revision(self): return self == self.paper.last_revision @property def number(self): return self.paper.revisions.index(self) + 1 @paper.setter def paper(self, paper): self._contribution = paper.contribution def get_timeline(self, user=None): comments = [x for x in self.comments if x.can_view(user)] if user else self.comments reviews = [x for x in self.reviews if x.can_view(user)] if user else self.reviews judgment = [ PaperJudgmentProxy(self) ] if self.state == PaperRevisionState.to_be_corrected else [] return sorted(chain(comments, reviews, judgment), key=attrgetter('created_dt')) def get_reviews(self, group=None, user=None): reviews = [] if user and group: reviews = [ x for x in self.reviews if x.group.instance == group and x.user == user ] elif user: reviews = [x for x in self.reviews if x.user == user] elif group: reviews = [x for x in self.reviews if x.group.instance == group] return reviews def get_reviewed_for_groups(self, user, include_reviewed=False): from fossir.modules.events.papers.models.reviews import PaperTypeProxy reviewed_for = {x.type for x in self.reviews if x.user == user} if include_reviewed else set() if self.paper.cfp.content_reviewing_enabled and user in self.paper.cfp.content_reviewers: reviewed_for.add(PaperReviewType.content) if self.paper.cfp.layout_reviewing_enabled and user in self.paper.cfp.layout_reviewers: reviewed_for.add(PaperReviewType.layout) return set(map(PaperTypeProxy, reviewed_for)) def has_user_reviewed(self, user, review_type=None): from fossir.modules.events.papers.models.reviews import PaperReviewType if review_type: if isinstance(review_type, basestring): review_type = PaperReviewType[review_type] return any(review.user == user and review.type == review_type for review in self.reviews) else: layout_review = next( (review for review in self.reviews if review.user == user and review.type == PaperReviewType.layout), None) content_review = next( (review for review in self.reviews if review.user == user and review.type == PaperReviewType.content), None) if user in self._contribution.paper_layout_reviewers and user in self._contribution.paper_content_reviewers: return bool(layout_review and content_review) elif user in self._contribution.paper_layout_reviewers: return bool(layout_review) elif user in self._contribution.paper_content_reviewers: return bool(content_review) def get_spotlight_file(self): pdf_files = [ paper_file for paper_file in self.files if paper_file.content_type == 'application/pdf' ] return pdf_files[0] if len(pdf_files) == 1 else None
def __table_args__(cls): return (db.Index('ix_uq_reference_types_name_lower', db.func.lower(cls.name), unique=True), { 'schema': 'fossir' })
class Abstract(ProposalMixin, ProposalRevisionMixin, DescriptionMixin, CustomFieldsMixin, AuthorsSpeakersMixin, db.Model): """Represents an abstract that can be associated to a Contribution.""" __tablename__ = 'abstracts' __auto_table_args = ( db.Index(None, 'friendly_id', 'event_id', unique=True, postgresql_where=db.text('NOT is_deleted')), db.CheckConstraint( '(state = {}) OR (accepted_track_id IS NULL)'.format( AbstractState.accepted), name='accepted_track_id_only_accepted'), db.CheckConstraint( '(state = {}) OR (accepted_contrib_type_id IS NULL)'.format( AbstractState.accepted), name='accepted_contrib_type_id_only_accepted'), db.CheckConstraint( '(state = {}) = (merged_into_id IS NOT NULL)'.format( AbstractState.merged), name='merged_into_id_only_merged'), db.CheckConstraint( '(state = {}) = (duplicate_of_id IS NOT NULL)'.format( AbstractState.duplicate), name='duplicate_of_id_only_duplicate'), db.CheckConstraint( '(state IN ({}, {}, {}, {})) = (judge_id IS NOT NULL)'.format( AbstractState.accepted, AbstractState.rejected, AbstractState.merged, AbstractState.duplicate), name='judge_if_judged'), db.CheckConstraint( '(state IN ({}, {}, {}, {})) = (judgment_dt IS NOT NULL)'.format( AbstractState.accepted, AbstractState.rejected, AbstractState.merged, AbstractState.duplicate), name='judgment_dt_if_judged'), { '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 @declared_attr def __table_args__(cls): return auto_table_args(cls) id = db.Column(db.Integer, primary_key=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) @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 @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, role='track_convener', explicit_role=True): return False elif self.event in user.global_convener_for_events: return True elif user.convener_for_tracks & 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 (fossir-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, role='abstract_reviewer', explicit_role=True): return False elif self.event in user.global_abstract_reviewer_for_events: return True elif user.abstract_reviewer_for_tracks & 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 is_manager = self.event.can_manage(user) if not self.user_owns(user) and not is_manager: return False elif is_manager and self.public_state in ( AbstractPublicState.under_review, AbstractPublicState.withdrawn): return True elif (self.public_state == AbstractPublicState.awaiting and (is_manager or self.event.cfa.can_edit_abstracts(user))): return True else: 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)).join( AbstractReviewRating.review).join( AbstractReviewRating.question).filter( AbstractReview.abstract == self, ~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 in user.global_abstract_reviewer_for_events: return self.reviewed_for_tracks | already_reviewed return (self.reviewed_for_tracks & user.abstract_reviewer_for_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)
class VCRoomEventAssociation(db.Model): __tablename__ = 'vc_room_events' __table_args__ = tuple(_make_checks()) + (db.Index(None, 'data', postgresql_using='gin'), {'schema': 'events'}) #: Association ID id = db.Column( db.Integer, primary_key=True ) #: ID of the event event_id = db.Column( db.Integer, db.ForeignKey('events.events.id'), index=True, autoincrement=False, nullable=False ) #: ID of the videoconference room vc_room_id = db.Column( db.Integer, db.ForeignKey('events.vc_rooms.id'), index=True, nullable=False ) #: Type of the object the vc_room is linked to link_type = db.Column( PyIntEnum(VCRoomLinkType), nullable=False ) linked_event_id = db.Column( db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=True ) session_block_id = db.Column( db.Integer, db.ForeignKey('events.session_blocks.id'), index=True, nullable=True ) contribution_id = db.Column( db.Integer, db.ForeignKey('events.contributions.id'), index=True, nullable=True ) #: If the vc room should be shown on the event page show = db.Column( db.Boolean, nullable=False, default=False ) #: videoconference plugin-specific data data = db.Column( JSONB, nullable=False ) #: The associated :class:VCRoom vc_room = db.relationship( 'VCRoom', lazy=False, backref=db.backref('events', cascade='all, delete-orphan') ) #: The associated Event event = db.relationship( 'Event', foreign_keys=event_id, lazy=True, backref=db.backref( 'all_vc_room_associations', lazy='dynamic' ) ) #: The linked event (if the VC room is attached to the event itself) linked_event = db.relationship( 'Event', foreign_keys=linked_event_id, lazy=True, backref=db.backref( 'vc_room_associations', lazy=True ) ) #: The linked contribution (if the VC room is attached to a contribution) linked_contrib = db.relationship( 'Contribution', lazy=True, backref=db.backref( 'vc_room_associations', lazy=True ) ) #: The linked session block (if the VC room is attached to a block) linked_block = db.relationship( 'SessionBlock', lazy=True, backref=db.backref( 'vc_room_associations', lazy=True ) ) @classmethod def register_link_events(cls): event_mapping = {cls.linked_block: lambda x: x.event, cls.linked_contrib: lambda x: x.event, cls.linked_event: lambda x: x} type_mapping = {cls.linked_event: VCRoomLinkType.event, cls.linked_block: VCRoomLinkType.block, cls.linked_contrib: VCRoomLinkType.contribution} def _set_link_type(link_type, target, value, *unused): if value is not None: target.link_type = link_type def _set_event_obj(fn, target, value, *unused): if value is not None: event = fn(value) assert event is not None target.event = event for rel, fn in event_mapping.iteritems(): if rel is not None: listen(rel, 'set', partial(_set_event_obj, fn)) for rel, link_type in type_mapping.iteritems(): if rel is not None: listen(rel, 'set', partial(_set_link_type, link_type)) @property def locator(self): return dict(self.event.locator, service=self.vc_room.type, event_vc_room_id=self.id) @hybrid_property def link_object(self): if self.link_type == VCRoomLinkType.event: return self.linked_event elif self.link_type == VCRoomLinkType.contribution: return self.linked_contrib else: return self.linked_block @link_object.setter def link_object(self, obj): self.linked_event = self.linked_contrib = self.linked_block = None if isinstance(obj, db.m.Event): self.linked_event = obj elif isinstance(obj, db.m.Contribution): self.linked_contrib = obj elif isinstance(obj, db.m.SessionBlock): self.linked_block = obj else: raise TypeError('Unexpected object: {}'.format(obj)) @link_object.comparator def link_object(cls): return _LinkObjectComparator(cls) @return_ascii def __repr__(self): return '<VCRoomEventAssociation({}, {})>'.format(self.event_id, self.vc_room) @classmethod 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 fossir Event :param only_linked_to_event: only retrieve the vc rooms linked to the whole event :param kwargs: extra kwargs to pass to ``find()`` """ if only_linked_to_event: kwargs['link_type'] = int(VCRoomLinkType.event) query = event.all_vc_room_associations if kwargs: query = query.filter_by(**kwargs) if not include_hidden: query = query.filter(cls.show) if not include_deleted: query = query.filter(VCRoom.status != VCRoomStatus.deleted).join(VCRoom) return query @classmethod @memoize_request def get_linked_for_event(cls, event): """Get a dict mapping link objects to event vc rooms""" return {vcr.link_object: vcr for vcr in cls.find_for_event(event)} def delete(self, user, delete_all=False): """Deletes a VC room from an event If the room is not used anywhere else, the room itself is also deleted. :param user: the user performing the deletion :param delete_all: if True, the room is detached from all events and deleted. """ vc_room = self.vc_room if delete_all: for assoc in vc_room.events[:]: Logger.get('modules.vc').info("Detaching VC room {} from event {} ({})".format( vc_room, assoc.event, assoc.link_object) ) vc_room.events.remove(assoc) else: Logger.get('modules.vc').info("Detaching VC room {} from event {} ({})".format( vc_room, self.event, self.link_object) ) vc_room.events.remove(self) db.session.flush() if not vc_room.events: Logger.get('modules.vc').info("Deleting VC room {}".format(vc_room)) if vc_room.status != VCRoomStatus.deleted: vc_room.plugin.delete_room(vc_room, self.event) notify_deleted(vc_room.plugin, vc_room, self, self.event, user) db.session.delete(vc_room)
def __auto_table_args(): return (db.Index(None, 'category_id', 'module', 'name'), db.Index(None, 'category_id', 'module'), db.UniqueConstraint('category_id', 'module', 'name'), { 'schema': 'categories' })
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)), {'schema': 'roombooking'})
def __table_args__(cls): return (db.Index('ix_uq_ip_network_groups_name_lower', db.func.lower(cls.name), unique=True), { 'schema': 'fossir' })