def event(cls): return db.relationship( 'Event', lazy=True, backref=db.backref( cls.events_backref_name, lazy='dynamic' ) )
def event_new(cls): return db.relationship( 'Event', lazy=True, backref=db.backref( cls.settings_backref_name, lazy='dynamic' ) )
def review(cls): return db.relationship( cls.review_class, lazy=True, backref=db.backref( 'ratings', cascade='all, delete-orphan', lazy=True ) )
def event_new(cls): return db.relationship( 'Event', foreign_keys=cls.event_id, lazy=True, backref=db.backref( cls.events_backref_name, lazy='dynamic' ) )
def own_venue(cls): return db.relationship( 'Location', foreign_keys=[cls.own_venue_id], lazy=True, backref=db.backref( cls.location_backref_name, lazy='dynamic' ) )
def own_room(cls): return db.relationship( 'Room', foreign_keys=[cls.own_room_id], lazy=True, backref=db.backref( cls.location_backref_name, lazy='dynamic' ) )
def contribution_field(cls): return db.relationship( 'ContributionField', lazy=False, backref=db.backref( cls.contribution_field_backref_name, cascade='all, delete-orphan', lazy=True ) )
def reference_type(cls): return db.relationship( 'ReferenceType', lazy=False, backref=db.backref( cls.reference_backref_name, cascade='all, delete-orphan', lazy=True ) )
def user(cls): return db.relationship( 'User', lazy=True, foreign_keys=cls.user_id, backref=db.backref( cls.user_backref_name, primaryjoin='({0}.user_id == User.id) & ~{0}.is_deleted'.format(cls.__name__), lazy='dynamic' ) )
def all_files(cls): return db.relationship( cls.stored_file_class, primaryjoin= lambda: cls.id == getattr(cls.stored_file_class, cls.stored_file_fkey), foreign_keys= lambda: getattr(cls.stored_file_class, cls.stored_file_fkey), lazy=True, cascade='all, delete, delete-orphan', order_by=lambda: cls.stored_file_class.created_dt.desc(), backref=db.backref( getattr(cls.stored_file_class, 'version_of'), lazy=False))
def subcontribution(cls): if LinkType.subcontribution in cls.allowed_link_types: return db.relationship( 'SubContribution', lazy=True, backref=db.backref( cls.link_backref_name, cascade='all, delete-orphan', uselist=(cls.unique_links != True), # noqa lazy=cls.link_backref_lazy ) )
def paper_revision(cls): return db.relationship( 'PaperRevision', lazy=True, backref=db.backref( 'comments', primaryjoin='(PaperReviewComment.revision_id == PaperRevision.id) & ~PaperReviewComment.is_deleted', order_by=cls.created_dt, cascade='all, delete-orphan', lazy=True, ) )
def abstract(cls): return db.relationship( 'Abstract', lazy=True, backref=db.backref( 'comments', primaryjoin='(AbstractComment.abstract_id == Abstract.id) & ~AbstractComment.is_deleted', order_by=cls.created_dt, cascade='all, delete-orphan', lazy=True, ) )
def session_block(cls): if LinkType.session_block in cls.allowed_link_types: return db.relationship( 'SessionBlock', lazy=True, backref=db.backref( cls.link_backref_name, cascade='all, delete-orphan', uselist=(cls.unique_links != True), # noqa lazy=cls.link_backref_lazy ) )
def category(cls): if LinkType.category in cls.allowed_link_types: return db.relationship( 'Category', lazy=True, backref=db.backref( cls.link_backref_name, cascade='all, delete-orphan', uselist=(cls.unique_links != True), # noqa lazy=cls.link_backref_lazy ) )
def event(cls): return db.relationship( 'Event', lazy=True, backref=db.backref( cls.event_backref_name, primaryjoin='({0}.event_id == Event.id) & ~{0}.is_deleted'.format(cls.__name__), order_by=cls.position, cascade='all, delete-orphan', lazy=True ) )
def linked_event(cls): if LinkType.event in cls.allowed_link_types: return db.relationship( 'Event', foreign_keys=cls.linked_event_id, lazy=True, backref=db.backref( cls.link_backref_name, cascade='all, delete-orphan', uselist=(cls.unique_links != True), # noqa lazy=cls.link_backref_lazy ) )
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 Survey(db.Model): __tablename__ = 'surveys' __table_args__ = (db.CheckConstraint("anonymous OR require_user", 'valid_anonymous_user'), {'schema': 'event_surveys'}) #: The ID of the survey id = db.Column( db.Integer, primary_key=True ) #: The ID of the event event_id = db.Column( db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False ) #: The title of the survey title = db.Column( db.String, nullable=False ) uuid = db.Column( UUID, unique=True, nullable=False, default=lambda: unicode(uuid4()) ) # An introduction text for users of the survey introduction = db.Column( db.Text, nullable=False, default='' ) #: Whether submissions will not be linked to a user anonymous = db.Column( db.Boolean, nullable=False, default=False ) #: Whether submissions must be done by logged users require_user = db.Column( db.Boolean, nullable=False, default=True ) # #: Whether the survey is only for selected users private = db.Column( db.Boolean, nullable=False, default=False ) #: Maximum number of submissions allowed submission_limit = db.Column( db.Integer, nullable=True ) #: Datetime when the survey is open start_dt = db.Column( UTCDateTime, nullable=True ) #: Datetime when the survey is closed end_dt = db.Column( UTCDateTime, nullable=True ) #: Whether the survey has been marked as deleted is_deleted = db.Column( db.Boolean, nullable=False, default=False ) #: Whether start notification has been already sent start_notification_sent = db.Column( db.Boolean, nullable=False, default=False ) #: Whether to send survey related notifications to users notifications_enabled = db.Column( db.Boolean, nullable=False, default=False ) #: Whether include Participants / Registrants when sending start notifications notify_participants = db.Column( db.Boolean, nullable=False, default=False ) #: Email addresses to notify about the start of a survey start_notification_emails = db.Column( ARRAY(db.String), nullable=False, default=[] ) #: Email addresses to notify about new submissions new_submission_emails = db.Column( ARRAY(db.String), nullable=False, default=[] ) #: Whether answers can be saved without submitting the survey partial_completion = db.Column( db.Boolean, nullable=False, default=False ) #: The last user-friendly submission ID _last_friendly_submission_id = db.deferred(db.Column( 'last_friendly_submission_id', db.Integer, nullable=False, default=0 )) #: The list of submissions submissions = db.relationship( 'SurveySubmission', cascade='all, delete-orphan', lazy=True, backref=db.backref( 'survey', lazy=True ) ) #: The list of items items = db.relationship( 'SurveyItem', cascade='all, delete-orphan', lazy=True, backref=db.backref( 'survey', lazy=True ) ) #: The list of sections sections = db.relationship( 'SurveySection', lazy=True, viewonly=True, order_by='SurveySection.position' ) #: The list of questions questions = db.relationship( 'SurveyQuestion', lazy=True, viewonly=True ) #: The Event containing this survey event_new = db.relationship( 'Event', lazy=True, backref=db.backref( 'surveys', lazy=True ) ) @hybrid_property def has_ended(self): return self.end_dt is not None and self.end_dt <= now_utc() @has_ended.expression def has_ended(cls): return (cls.end_dt != None) & (cls.end_dt <= now_utc()) # noqa @hybrid_property def has_started(self): return self.start_dt is not None and self.start_dt <= now_utc() @has_started.expression def has_started(cls): return (cls.start_dt != None) & (cls.start_dt <= now_utc()) # noqa @locator_property def locator(self): return {'confId': self.event_id, 'survey_id': self.id} @locator.token def locator(self): """A locator that adds the UUID if the survey is private""" token = self.uuid if self.private else None return dict(self.locator, token=token) @property def state(self): if not self.questions: return SurveyState.not_ready if not self.has_started: return SurveyState.ready_to_open if not self.has_ended: if not self.submissions: return SurveyState.active_and_clean return SurveyState.active_and_answered return SurveyState.finished @property def start_notification_recipients(self): """Returns all recipients of the notifications. This includes both explicit recipients and, if enabled, participants of the event. """ recipients = set(self.start_notification_emails) if self.notify_participants: recipients.update(reg.email for reg in Registration.get_all_for_event(self.event_new)) recipients.discard('') # just in case there was an empty email address somewhere return recipients @hybrid_property def is_active(self): return not self.is_deleted and self.state in {SurveyState.active_and_answered, SurveyState.active_and_clean} @is_active.expression def is_active(cls): return ~cls.is_deleted & cls.questions.any() & cls.has_started & ~cls.has_ended @hybrid_property def is_visible(self): return (not self.is_deleted and self.state in {SurveyState.active_and_answered, SurveyState.active_and_clean, SurveyState.finished}) @is_visible.expression def is_visible(cls): return ~cls.is_deleted & cls.questions.any() & cls.has_started @return_ascii def __repr__(self): return '<Survey({}, {}): {}>'.format(self.id, self.event_id, self.title) def can_submit(self, user): return self.is_active and (not self.require_user or user) def open(self): if self.state != SurveyState.ready_to_open: raise IndicoError("Survey can't be opened") self.start_dt = now_utc() def close(self): if self.state not in (SurveyState.active_and_clean, SurveyState.active_and_answered): raise IndicoError("Survey can't be closed") self.end_dt = now_utc() def send_start_notification(self): if not self.notifications_enabled or self.start_notification_sent or not self.event_new.has_feature('surveys'): return template_module = get_template_module('events/surveys/emails/start_notification_email.txt', survey=self) email = make_email(bcc_list=self.start_notification_recipients, template=template_module) send_email(email, event=self.event_new, module='Surveys') logger.info('Sending start notification for survey %s', self) self.start_notification_sent = True def send_submission_notification(self, submission): if not self.notifications_enabled: return template_module = get_template_module('events/surveys/emails/new_submission_email.txt', submission=submission) email = make_email(bcc_list=self.new_submission_emails, template=template_module) send_email(email, event=self.event_new, module='Surveys') logger.info('Sending submission notification for survey %s', self)
class VCRoomEventAssociation(db.Model): __tablename__ = 'vc_room_events' __table_args__ = tuple(_make_checks()) + (db.Index( None, 'data', postgresql_using='gin'), { 'schema': 'events' }) #: Association ID id = db.Column(db.Integer, primary_key=True) #: ID of the event event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, autoincrement=False, nullable=False) #: ID of the videoconference room vc_room_id = db.Column(db.Integer, db.ForeignKey('events.vc_rooms.id'), index=True, nullable=False) #: Type of the object the vc_room is linked to link_type = db.Column(PyIntEnum(VCRoomLinkType), nullable=False) linked_event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=True) session_block_id = db.Column(db.Integer, db.ForeignKey('events.session_blocks.id'), index=True, nullable=True) contribution_id = db.Column(db.Integer, db.ForeignKey('events.contributions.id'), index=True, nullable=True) #: If the vc room should be shown on the event page show = db.Column(db.Boolean, nullable=False, default=False) #: videoconference plugin-specific data data = db.Column(JSONB, nullable=False) #: The associated :class:VCRoom vc_room = db.relationship('VCRoom', lazy=False, backref=db.backref('events', cascade='all, delete-orphan')) #: The associated Event event = db.relationship('Event', foreign_keys=event_id, lazy=True, backref=db.backref('all_vc_room_associations', lazy='dynamic')) #: The linked event (if the VC room is attached to the event itself) linked_event = db.relationship('Event', foreign_keys=linked_event_id, lazy=True, backref=db.backref('vc_room_associations', lazy=True)) #: The linked contribution (if the VC room is attached to a contribution) linked_contrib = db.relationship('Contribution', lazy=True, backref=db.backref('vc_room_associations', lazy=True)) #: The linked session block (if the VC room is attached to a block) linked_block = db.relationship('SessionBlock', lazy=True, backref=db.backref('vc_room_associations', lazy=True)) @classmethod def register_link_events(cls): event_mapping = { cls.linked_block: lambda x: x.event, cls.linked_contrib: lambda x: x.event, cls.linked_event: lambda x: x } type_mapping = { cls.linked_event: VCRoomLinkType.event, cls.linked_block: VCRoomLinkType.block, cls.linked_contrib: VCRoomLinkType.contribution } def _set_link_type(link_type, target, value, *unused): if value is not None: target.link_type = link_type def _set_event_obj(fn, target, value, *unused): if value is not None: event = fn(value) assert event is not None target.event = event for rel, fn in event_mapping.items(): if rel is not None: listen(rel, 'set', partial(_set_event_obj, fn)) for rel, link_type in type_mapping.items(): if rel is not None: listen(rel, 'set', partial(_set_link_type, link_type)) @property def locator(self): return dict(self.event.locator, service=self.vc_room.type, event_vc_room_id=self.id) @hybrid_property def link_object(self): if self.link_type == VCRoomLinkType.event: return self.linked_event elif self.link_type == VCRoomLinkType.contribution: return self.linked_contrib else: return self.linked_block @link_object.setter def link_object(self, obj): self.linked_event = self.linked_contrib = self.linked_block = None if isinstance(obj, db.m.Event): self.linked_event = obj elif isinstance(obj, db.m.Contribution): self.linked_contrib = obj elif isinstance(obj, db.m.SessionBlock): self.linked_block = obj else: raise TypeError(f'Unexpected object: {obj}') @link_object.comparator def link_object(cls): return _LinkObjectComparator(cls) def __repr__(self): return f'<VCRoomEventAssociation({self.event_id}, {self.vc_room})>' @classmethod def find_for_event(cls, event, include_hidden=False, include_deleted=False, only_linked_to_event=False, **kwargs): """Return a Query that retrieves the videoconference rooms for an event. :param event: an indico Event :param only_linked_to_event: only retrieve the vc rooms linked to the whole event :param kwargs: extra kwargs to pass to ``filter_by()`` """ if only_linked_to_event: kwargs['link_type'] = int(VCRoomLinkType.event) query = event.all_vc_room_associations if kwargs: query = query.filter_by(**kwargs) if not include_hidden: query = query.filter(cls.show) if not include_deleted: query = query.filter( VCRoom.status != VCRoomStatus.deleted).join(VCRoom) return query @classmethod @memoize_request def get_linked_for_event(cls, event): """Get a dict mapping link objects to event vc rooms.""" return {vcr.link_object: vcr for vcr in cls.find_for_event(event)} def delete(self, user, delete_all=False): """Delete a VC room from an event. If the room is not used anywhere else, the room itself is also deleted. :param user: the user performing the deletion :param delete_all: if True, the room is detached from all events and deleted. """ vc_room = self.vc_room if delete_all: for assoc in vc_room.events[:]: Logger.get('modules.vc').info( "Detaching VC room {} from event {} ({})".format( vc_room, assoc.event, assoc.link_object)) vc_room.events.remove(assoc) else: Logger.get('modules.vc').info( "Detaching VC room {} from event {} ({})".format( vc_room, self.event, self.link_object)) vc_room.events.remove(self) db.session.flush() if vc_room.plugin and not vc_room.events: Logger.get('modules.vc').info(f"Deleting VC room {vc_room}") if vc_room.status != VCRoomStatus.deleted: vc_room.plugin.delete_room(vc_room, self.event) notify_deleted(vc_room.plugin, vc_room, self, self.event, user) db.session.delete(vc_room)
def event_new(cls): return db.relationship('Event', lazy=True, backref=db.backref(cls.events_backref_name, lazy='dynamic'))
class IPNetworkGroup(db.Model): __tablename__ = 'ip_network_groups' principal_type = PrincipalType.network principal_order = 1 @declared_attr def __table_args__(cls): return (db.Index('ix_uq_ip_network_groups_name_lower', db.func.lower(cls.name), unique=True), { 'schema': 'indico' }) id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String, nullable=False) description = db.Column(db.Text, nullable=False, default='') #: Whether the network group is hidden in ACL forms hidden = db.Column(db.Boolean, nullable=False, default=False) #: Grants all IPs in the network group read access to all attachments attachment_access_override = db.Column(db.Boolean, nullable=False, default=False) _networks = db.relationship('IPNetwork', lazy=False, cascade='all, delete-orphan', collection_class=set, backref=db.backref('group', lazy=True)) networks = association_proxy('_networks', 'network', creator=lambda v: IPNetwork(network=v)) # relationship backrefs: # - in_category_acls (CategoryPrincipal.ip_network_group) # - in_event_acls (EventPrincipal.ip_network_group) def __repr__(self): return format_repr(self, 'id', 'name', hidden=False, attachment_access_override=False) def __contains__(self, user): # This method is called via ``user in principal`` during ACL checks. # We have to take the IP from the request so if there's no request # (e.g. in the shell) we never grant IP-based access; same if we check # for a different user than the one from the current session. if not has_request_context() or not request.remote_addr: return False if session.user != user: return False return self.contains_ip(str(request.remote_addr)) def contains_ip(self, ip): ip = ip_address(ip) return any(ip in network for network in self.networks) @property def locator(self): return {'network_group_id': self.id}
class RegistrationFormItem(db.Model): """Generic registration form item""" __tablename__ = 'form_items' __table_args__ = ( db.CheckConstraint( "(input_type IS NULL) = (type NOT IN ({t.field}, {t.field_pd}))". format(t=RegistrationFormItemType), name='valid_input'), db.CheckConstraint("NOT is_manager_only OR type = {type}".format( type=RegistrationFormItemType.section), name='valid_manager_only'), db.CheckConstraint( "(type IN ({t.section}, {t.section_pd})) = (parent_id IS NULL)". format(t=RegistrationFormItemType), name='top_level_sections'), db.CheckConstraint( "(type != {type}) = (personal_data_type IS NULL)".format( type=RegistrationFormItemType.field_pd), name='pd_field_type'), db.CheckConstraint( "NOT is_deleted OR (type NOT IN ({t.section_pd}, {t.field_pd}))". format(t=RegistrationFormItemType), name='pd_not_deleted'), db.CheckConstraint("is_enabled OR type != {type}".format( type=RegistrationFormItemType.section_pd), name='pd_section_enabled'), db.CheckConstraint( "is_enabled OR type != {type} OR personal_data_type NOT IN " "({pt.email}, {pt.first_name}, {pt.last_name})".format( type=RegistrationFormItemType.field_pd, pt=PersonalDataType), name='pd_field_enabled'), db.CheckConstraint( "is_required OR type != {type} OR personal_data_type NOT IN " "({pt.email}, {pt.first_name}, {pt.last_name})".format( type=RegistrationFormItemType.field_pd, pt=PersonalDataType), name='pd_field_required'), db.CheckConstraint( "current_data_id IS NULL OR type IN ({t.field}, {t.field_pd})". format(t=RegistrationFormItemType), name='current_data_id_only_field'), db.Index('ix_uq_form_items_pd_section', 'registration_form_id', unique=True, postgresql_where=db.text('type = {type}'.format( type=RegistrationFormItemType.section_pd))), db.Index('ix_uq_form_items_pd_field', 'registration_form_id', 'personal_data_type', unique=True, postgresql_where=db.text('type = {type}'.format( type=RegistrationFormItemType.field_pd))), { 'schema': 'event_registration' }) __mapper_args__ = {'polymorphic_on': 'type', 'polymorphic_identity': None} #: The ID of the object id = db.Column(db.Integer, primary_key=True) #: The ID of the registration form registration_form_id = db.Column( db.Integer, db.ForeignKey('event_registration.forms.id'), index=True, nullable=False) #: The type of the registration form item type = db.Column(PyIntEnum(RegistrationFormItemType), nullable=False) #: The type of a personal data field personal_data_type = db.Column(PyIntEnum(PersonalDataType), nullable=True) #: The ID of the parent form item parent_id = db.Column(db.Integer, db.ForeignKey('event_registration.form_items.id'), index=True, nullable=True) position = db.Column(db.Integer, nullable=False, default=_get_next_position) #: The title of this field title = db.Column(db.String, nullable=False) #: Description of this field description = db.Column(db.String, nullable=False, default='') #: Whether the field is enabled is_enabled = db.Column(db.Boolean, nullable=False, default=True) #: Whether field has been "deleted" is_deleted = db.Column(db.Boolean, nullable=False, default=False) #: determines if the field is mandatory is_required = db.Column(db.Boolean, nullable=False, default=False) #: if the section is only accessible to managers is_manager_only = db.Column(db.Boolean, nullable=False, default=False) #: input type of this field input_type = db.Column(db.String, nullable=True) #: unversioned field data data = db.Column(JSON, nullable=False, default=lambda: None) #: The ID of the latest data current_data_id = db.Column(db.Integer, db.ForeignKey( 'event_registration.form_field_data.id', use_alter=True), index=True, nullable=True) #: The latest value of the field current_data = db.relationship( 'RegistrationFormFieldData', primaryjoin= 'RegistrationFormItem.current_data_id == RegistrationFormFieldData.id', foreign_keys=current_data_id, lazy=True, post_update=True) #: The list of all versions of the field data data_versions = db.relationship( 'RegistrationFormFieldData', primaryjoin= 'RegistrationFormItem.id == RegistrationFormFieldData.field_id', foreign_keys='RegistrationFormFieldData.field_id', lazy=True, cascade='all, delete-orphan', backref=db.backref('field', lazy=False)) # The children of the item and the parent backref children = db.relationship('RegistrationFormItem', lazy=True, order_by='RegistrationFormItem.position', backref=db.backref('parent', lazy=False, remote_side=[id])) # relationship backrefs: # - parent (RegistrationFormItem.children) # - registration_form (RegistrationForm.form_items) @property def view_data(self): """Returns object with data that Angular can understand""" return dict(id=self.id, description=self.description, position=self.position) @hybrid_property def is_section(self): return self.type in { RegistrationFormItemType.section, RegistrationFormItemType.section_pd } @is_section.expression def is_section(cls): return cls.type.in_([ RegistrationFormItemType.section, RegistrationFormItemType.section_pd ]) @hybrid_property def is_field(self): return self.type in { RegistrationFormItemType.field, RegistrationFormItemType.field_pd } @is_field.expression def is_field(cls): return cls.type.in_([ RegistrationFormItemType.field, RegistrationFormItemType.field_pd ]) @hybrid_property def is_visible(self): return self.is_enabled and not self.is_deleted and ( self.parent_id is None or self.parent.is_visible) @is_visible.expression def is_visible(cls): sections = aliased(RegistrationFormSection) query = (db.session.query(literal(True)).filter( sections.id == cls.parent_id).filter(~sections.is_deleted).filter( sections.is_enabled).exists()) return cls.is_enabled & ~cls.is_deleted & ( (cls.parent_id == None) | query) # noqa @return_ascii def __repr__(self): return format_repr(self, 'id', 'registration_form_id', is_enabled=True, is_deleted=False, is_manager_only=False, _text=self.title)
def event_new(cls): return db.relationship("Event", lazy=True, backref=db.backref(cls.settings_backref_name, lazy="dynamic"))
class AttachmentFile(StoredFileMixin, db.Model): __tablename__ = 'files' __table_args__ = {'schema': 'attachments'} version_of = 'attachment' #: The ID of the file id = db.Column(db.Integer, primary_key=True) #: The ID of the associated attachment attachment_id = db.Column(db.Integer, db.ForeignKey('attachments.attachments.id'), nullable=False, index=True) #: The user who uploaded the file user_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), nullable=False, index=True) #: The user who uploaded the file user = db.relationship('User', lazy=True, backref=db.backref('attachment_files', lazy='dynamic')) # relationship backrefs: # - attachment (Attachment.all_files) @property def is_previewable(self): return get_file_previewer(self) is not None @no_autoflush def _build_storage_path(self): folder = self.attachment.folder assert folder.object is not None if folder.link_type == LinkType.category: # category/<id>/... path_segments = ['category', strict_str(folder.category.id)] else: # event/<id>/event/... path_segments = [ 'event', strict_str(folder.event.id), folder.link_type.name ] if folder.link_type == LinkType.session: # event/<id>/session/<session_id>/... path_segments.append(strict_str(folder.session.id)) elif folder.link_type == LinkType.contribution: # event/<id>/contribution/<contribution_id>/... path_segments.append(strict_str(folder.contribution.id)) elif folder.link_type == LinkType.subcontribution: # event/<id>/subcontribution/<subcontribution_id>/... path_segments.append(strict_str(folder.subcontribution.id)) self.attachment.assign_id() self.assign_id() filename = '{}-{}-{}'.format(self.attachment.id, self.id, secure_filename(self.filename, 'file')) path = posixpath.join(*(path_segments + [filename])) return config.ATTACHMENT_STORAGE, path def __repr__(self): return '<AttachmentFile({}, {}, {}, {})>'.format( self.id, self.attachment_id, self.filename, self.content_type)
class Agreement(db.Model): """Agreements between a person and Indico.""" __tablename__ = 'agreements' __table_args__ = (db.UniqueConstraint('event_id', 'type', 'identifier'), {'schema': 'events'}) #: Entry ID id = db.Column( db.Integer, primary_key=True ) #: Entry universally unique ID uuid = db.Column( db.String, nullable=False ) #: ID of the event event_id = db.Column( db.Integer, db.ForeignKey('events.events.id'), nullable=False, index=True ) #: Type of agreement type = db.Column( db.String, nullable=False ) #: Unique identifier within the event and type identifier = db.Column( db.String, nullable=False ) #: Email of the person agreeing person_email = db.Column( db.String, nullable=True ) #: Full name of the person agreeing person_name = db.Column( db.String, nullable=False ) #: A :class:`AgreementState` state = db.Column( PyIntEnum(AgreementState), default=AgreementState.pending, nullable=False ) #: The date and time the agreement was created timestamp = db.Column( UTCDateTime, default=now_utc, nullable=False ) #: ID of a linked user user_id = db.Column( db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=True ) #: The date and time the agreement was signed signed_dt = db.Column( UTCDateTime ) #: The IP from which the agreement was signed signed_from_ip = db.Column( db.String ) #: Explanation as to why the agreement was accepted/rejected reason = db.Column( db.String ) #: Attachment attachment = db.deferred(db.Column( db.LargeBinary )) #: Filename and extension of the attachment attachment_filename = db.Column( db.String ) #: Definition-specific data of the agreement data = db.Column( JSONB ) #: The user this agreement is linked to user = db.relationship( 'User', lazy=False, backref=db.backref( 'agreements', lazy='dynamic' ) ) #: The Event this agreement is associated with event = db.relationship( 'Event', lazy=True, backref=db.backref( 'agreements', lazy='dynamic' ) ) @hybrid_property def accepted(self): return self.state in {AgreementState.accepted, AgreementState.accepted_on_behalf} @accepted.expression def accepted(self): return self.state.in_((AgreementState.accepted, AgreementState.accepted_on_behalf)) @hybrid_property def pending(self): return self.state == AgreementState.pending @hybrid_property def rejected(self): return self.state in {AgreementState.rejected, AgreementState.rejected_on_behalf} @rejected.expression def rejected(self): return self.state.in_((AgreementState.rejected, AgreementState.rejected_on_behalf)) @hybrid_property def signed_on_behalf(self): return self.state in {AgreementState.accepted_on_behalf, AgreementState.rejected_on_behalf} @signed_on_behalf.expression def signed_on_behalf(self): return self.state.in_((AgreementState.accepted_on_behalf, AgreementState.rejected_on_behalf)) @property def definition(self): from indico.modules.events.agreements.util import get_agreement_definitions return get_agreement_definitions().get(self.type) @property def locator(self): return {'confId': self.event_id, 'id': self.id} def __repr__(self): state = self.state.name if self.state is not None else None return '<Agreement({}, {}, {}, {}, {}, {})>'.format(self.id, self.event_id, self.type, self.identifier, self.person_email, state) @staticmethod def create_from_data(event, type_, person): agreement = Agreement(event=event, type=type_, state=AgreementState.pending, uuid=str(uuid4())) agreement.identifier = person.identifier agreement.person_email = person.email agreement.person_name = person.name if person.user: agreement.user = person.user agreement.data = person.data return agreement def accept(self, from_ip, reason=None, on_behalf=False): self.state = AgreementState.accepted if not on_behalf else AgreementState.accepted_on_behalf self.signed_from_ip = from_ip self.reason = reason self.signed_dt = now_utc() self.definition.handle_accepted(self) def reject(self, from_ip, reason=None, on_behalf=False): self.state = AgreementState.rejected if not on_behalf else AgreementState.rejected_on_behalf self.signed_from_ip = from_ip self.reason = reason self.signed_dt = now_utc() self.definition.handle_rejected(self) def reset(self): self.definition.handle_reset(self) self.state = AgreementState.pending self.attachment = None self.attachment_filename = None self.reason = None self.signed_dt = None self.signed_from_ip = None def render(self, form, **kwargs): definition = self.definition if definition is None: raise ServiceUnavailable('This agreement type is currently not available.') return definition.render_form(self, form, **kwargs) def belongs_to(self, person): return self.identifier == person.identifier def is_orphan(self): definition = self.definition if definition is None: raise ServiceUnavailable('This agreement type is currently not available.') return definition.is_agreement_orphan(self.event, self)
def event(cls): return db.relationship('Event', lazy=True, backref=db.backref(cls.settings_backref_name, lazy='dynamic'))
class Category(SearchableTitleMixin, DescriptionMixin, ProtectionManagersMixin, AttachedItemsMixin, db.Model): """An Indico category.""" __tablename__ = 'categories' disallowed_protection_modes = frozenset() inheriting_have_acl = True possible_render_modes = {RenderMode.markdown} default_render_mode = RenderMode.markdown allow_no_access_contact = True ATTACHMENT_FOLDER_ID_COLUMN = 'category_id' @strict_classproperty @classmethod def __auto_table_args(cls): return ( db.CheckConstraint( "(icon IS NULL) = (icon_metadata::text = 'null')", 'valid_icon'), db.CheckConstraint( "(logo IS NULL) = (logo_metadata::text = 'null')", 'valid_logo'), db.CheckConstraint("(parent_id IS NULL) = (id = 0)", 'valid_parent'), db.CheckConstraint("(id != 0) OR NOT is_deleted", 'root_not_deleted'), db.CheckConstraint( f"(id != 0) OR (protection_mode != {ProtectionMode.inheriting})", 'root_not_inheriting'), db.CheckConstraint('visibility IS NULL OR visibility > 0', 'valid_visibility'), { 'schema': 'categories' }) @declared_attr def __table_args__(cls): return auto_table_args(cls) id = db.Column(db.Integer, primary_key=True) parent_id = db.Column(db.Integer, db.ForeignKey('categories.categories.id'), index=True, nullable=True) is_deleted = db.Column(db.Boolean, nullable=False, default=False) position = db.Column(db.Integer, nullable=False, default=_get_next_position) visibility = db.Column(db.Integer, nullable=True, default=None) icon_metadata = db.Column(JSONB, nullable=False, default=lambda: None) icon = db.deferred(db.Column(db.LargeBinary, nullable=True)) logo_metadata = db.Column(JSONB, nullable=False, default=lambda: None) logo = db.deferred(db.Column(db.LargeBinary, nullable=True)) timezone = db.Column(db.String, nullable=False, default=lambda: config.DEFAULT_TIMEZONE) default_event_themes = db.Column(JSONB, nullable=False, default=_get_default_event_themes) event_creation_restricted = db.Column(db.Boolean, nullable=False, default=True) event_creation_notification_emails = db.Column(ARRAY(db.String), nullable=False, default=[]) event_message_mode = db.Column(PyIntEnum(EventMessageMode), nullable=False, default=EventMessageMode.disabled) _event_message = db.Column('event_message', db.Text, nullable=False, default='') suggestions_disabled = db.Column(db.Boolean, nullable=False, default=False) notify_managers = db.Column(db.Boolean, nullable=False, default=False) default_ticket_template_id = db.Column( db.ForeignKey('indico.designer_templates.id'), nullable=True, index=True) default_badge_template_id = db.Column( db.ForeignKey('indico.designer_templates.id'), nullable=True, index=True) children = db.relationship( 'Category', order_by='Category.position', primaryjoin=(id == db.remote(parent_id)) & ~db.remote(is_deleted), lazy=True, backref=db.backref('parent', primaryjoin=(db.remote(id) == parent_id), lazy=True)) acl_entries = db.relationship('CategoryPrincipal', backref='category', cascade='all, delete-orphan', collection_class=set) default_ticket_template = db.relationship( 'DesignerTemplate', lazy=True, foreign_keys=default_ticket_template_id, backref='default_ticket_template_of') default_badge_template = db.relationship( 'DesignerTemplate', lazy=True, foreign_keys=default_badge_template_id, backref='default_badge_template_of') # column properties: # - deep_events_count # relationship backrefs: # - attachment_folders (AttachmentFolder.category) # - designer_templates (DesignerTemplate.category) # - events (Event.category) # - favorite_of (User.favorite_categories) # - legacy_mapping (LegacyCategoryMapping.category) # - parent (Category.children) # - roles (CategoryRole.category) # - settings (CategorySetting.category) # - suggestions (SuggestedCategory.category) @hybrid_property def event_message(self): return MarkdownText(self._event_message) @event_message.setter def event_message(self, value): self._event_message = value @event_message.expression def event_message(cls): return cls._event_message def __repr__(self): return format_repr(self, 'id', is_deleted=False, _text=text_to_repr(self.title, max_length=75)) @property def protection_parent(self): return self.parent if not self.is_root else None @locator_property def locator(self): return {'category_id': self.id} @classmethod def get_root(cls): """Get the root category.""" return cls.query.filter(cls.is_root).one() @property def url(self): return url_for('categories.display', self) @hybrid_property def is_root(self): return self.parent_id is None @is_root.expression def is_root(cls): return cls.parent_id.is_(None) @property def is_empty(self): return not self.deep_children_count and not self.deep_events_count @property def has_icon(self): return self.icon_metadata is not None @property def has_effective_icon(self): return self.effective_icon_data['metadata'] is not None @property def has_logo(self): return self.logo_metadata is not None @property def tzinfo(self): return pytz.timezone(self.timezone) @property def display_tzinfo(self): """The tzinfo of the category or the one specified by the user.""" return get_display_tz(self, as_timezone=True) def can_create_events(self, user): """Check whether the user can create events in the category.""" # if creation is not restricted anyone who can access the category # can also create events in it, otherwise only people with the # creation role can return user and ( (not self.event_creation_restricted and self.can_access(user)) or self.can_manage(user, permission='create')) def move(self, target): """Move the category into another category.""" assert not self.is_root old_parent = self.parent self.position = (max(x.position for x in target.children) + 1) if target.children else 1 self.parent = target db.session.flush() signals.category.moved.send(self, old_parent=old_parent) @classmethod def get_tree_cte(cls, col='id'): """Create a CTE for the category tree. The CTE contains the following columns: - ``id`` -- the category id - ``path`` -- an array containing the path from the root to the category itself - ``is_deleted`` -- whether the category is deleted :param col: The name of the column to use in the path or a callable receiving the category alias that must return the expression used for the 'path' retrieved by the CTE. """ cat_alias = db.aliased(cls) if callable(col): path_column = col(cat_alias) else: path_column = getattr(cat_alias, col) cte_query = (select([ cat_alias.id, array([path_column]).label('path'), cat_alias.is_deleted ]).where(cat_alias.parent_id.is_(None)).cte(recursive=True)) rec_query = (select([ cat_alias.id, cte_query.c.path.op('||')(path_column), cte_query.c.is_deleted | cat_alias.is_deleted ]).where(cat_alias.parent_id == cte_query.c.id)) return cte_query.union_all(rec_query) @classmethod def get_protection_cte(cls): cat_alias = db.aliased(cls) cte_query = (select([cat_alias.id, cat_alias.protection_mode]).where( cat_alias.parent_id.is_(None)).cte(recursive=True)) rec_query = (select([ cat_alias.id, db.case( {ProtectionMode.inheriting.value: cte_query.c.protection_mode}, else_=cat_alias.protection_mode, value=cat_alias.protection_mode) ]).where(cat_alias.parent_id == cte_query.c.id)) return cte_query.union_all(rec_query) def get_protection_parent_cte(self): cte_query = (select([ Category.id, db.cast(literal(None), db.Integer).label('protection_parent') ]).where(Category.id == self.id).cte(recursive=True)) rec_query = (select([ Category.id, db.case( { ProtectionMode.inheriting.value: func.coalesce(cte_query.c.protection_parent, self.id) }, else_=Category.id, value=Category.protection_mode) ]).where(Category.parent_id == cte_query.c.id)) return cte_query.union_all(rec_query) @classmethod def get_icon_data_cte(cls): cat_alias = db.aliased(cls) cte_query = (select([ cat_alias.id, cat_alias.id.label('source_id'), cat_alias.icon_metadata ]).where(cat_alias.parent_id.is_(None)).cte(recursive=True)) rec_query = (select([ cat_alias.id, db.case({'null': cte_query.c.source_id}, else_=cat_alias.id, value=db.func.jsonb_typeof(cat_alias.icon_metadata)), db.case({'null': cte_query.c.icon_metadata}, else_=cat_alias.icon_metadata, value=db.func.jsonb_typeof(cat_alias.icon_metadata)) ]).where(cat_alias.parent_id == cte_query.c.id)) return cte_query.union_all(rec_query) @property def deep_children_query(self): """Get a query object for all subcategories. This includes subcategories at any level of nesting. """ cte = Category.get_tree_cte() return (Category.query.join(cte, Category.id == cte.c.id).filter( cte.c.path.contains([self.id]), cte.c.id != self.id, ~cte.c.is_deleted)) @staticmethod def _get_chain_query(start_criterion): cte_query = (select([ Category.id, Category.parent_id, literal(0).label('level') ]).where(start_criterion).cte('category_chain', recursive=True)) parent_query = (select([ Category.id, Category.parent_id, cte_query.c.level + 1 ]).where(Category.id == cte_query.c.parent_id)) cte_query = cte_query.union_all(parent_query) return Category.query.join(cte_query, Category.id == cte_query.c.id).order_by( cte_query.c.level.desc()) @property def chain_query(self): """Get a query object for the category chain. The query retrieves the root category first and then all the intermediate categories up to (and including) this category. """ return self._get_chain_query(Category.id == self.id) @property def parent_chain_query(self): """Get a query object for the category's parent chain. The query retrieves the root category first and then all the intermediate categories up to (excluding) this category. """ return self._get_chain_query(Category.id == self.parent_id) def nth_parent(self, n_categs, fail_on_overflow=True): """Return the nth parent of the category. :param n_categs: the number of categories to go up :param fail_on_overflow: whether to fail if we try to go above the root category :return: `Category` object or None (only if ``fail_on_overflow`` is not set) """ if n_categs == 0: return self chain = self.parent_chain_query.all() assert n_categs >= 0 if n_categs > len(chain): if fail_on_overflow: raise IndexError("Root category has no parent!") else: return None return chain[::-1][n_categs - 1] def is_descendant_of(self, categ): return categ != self and self.parent_chain_query.filter( Category.id == categ.id).has_rows() @property def visibility_horizon_query(self): """Get a query object that returns the highest category this one is visible from.""" cte_query = (select([ Category.id, Category.parent_id, db.case([(Category.visibility.is_(None), None)], else_=(Category.visibility - 1)).label('n'), literal(0).label('level') ]).where(Category.id == self.id).cte('visibility_horizon', recursive=True)) parent_query = (select([ Category.id, Category.parent_id, db.case([ (Category.visibility.is_(None) & cte_query.c.n.is_(None), None) ], else_=db.func.least(Category.visibility, cte_query.c.n) - 1), cte_query.c.level + 1 ]).where( db.and_(Category.id == cte_query.c.parent_id, (cte_query.c.n > 0) | cte_query.c.n.is_(None)))) cte_query = cte_query.union_all(parent_query) return db.session.query(cte_query.c.id, cte_query.c.n).order_by( cte_query.c.level.desc()).limit(1) @property def own_visibility_horizon(self): """ Get the highest category this one would like to be visible from (configured visibility). """ if self.visibility is None: return Category.get_root() else: return self.nth_parent(self.visibility - 1) @property def real_visibility_horizon(self): """ Get the highest category this one is actually visible from (as limited by categories above). """ horizon_id, final_visibility = self.visibility_horizon_query.one() if final_visibility is not None and final_visibility < 0: return None # Category is invisible return Category.get(horizon_id) @staticmethod def get_visible_categories_cte(category_id): """ Get a sqlalchemy select for the visible categories within the given category, including the category itself. """ cte_query = (select([ Category.id, literal(0).label('level') ]).where((Category.id == category_id) & (Category.visibility.is_(None) | (Category.visibility > 0))).cte(recursive=True)) parent_query = (select([Category.id, cte_query.c.level + 1]).where( db.and_( Category.parent_id == cte_query.c.id, db.or_(Category.visibility.is_(None), Category.visibility > cte_query.c.level + 1)))) return cte_query.union_all(parent_query) @property def visible_categories_query(self): """ Get a query object for the visible categories within this category, including the category itself. """ cte_query = Category.get_visible_categories_cte(self.id) return Category.query.join(cte_query, Category.id == cte_query.c.id) def get_hidden_events(self, user=None): """Get all hidden events within the given category and user.""" from indico.modules.events import Event hidden_events = Event.query.with_parent(self).filter_by( visibility=0).all() return [ event for event in hidden_events if not event.can_display(user) ] @property def icon_url(self): """Get the HTTP URL of the icon.""" return url_for('categories.display_icon', self, slug=self.icon_metadata['hash']) @property def effective_icon_url(self): """Get the HTTP URL of the icon (possibly inherited).""" data = self.effective_icon_data return url_for('categories.display_icon', category_id=data['source_id'], slug=data['metadata']['hash']) @property def logo_url(self): """Get the HTTP URL of the logo.""" return url_for('categories.display_logo', self, slug=self.logo_metadata['hash'])
class RegistrationForm(db.Model): """A registration form for an event""" __tablename__ = 'forms' __table_args__ = ( db.Index( 'ix_uq_forms_participation', 'event_id', unique=True, postgresql_where=db.text('is_participation AND NOT is_deleted')), db.UniqueConstraint( 'id', 'event_id'), # useless but needed for the registrations fkey { 'schema': 'event_registration' }) #: The ID of the object id = db.Column(db.Integer, primary_key=True) #: The ID of the event event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False) #: The title of the registration form title = db.Column(db.String, nullable=False) #: Whether it's the 'Participants' form of a meeting/lecture is_participation = db.Column(db.Boolean, nullable=False, default=False) # An introduction text for users introduction = db.Column(db.Text, nullable=False, default='') #: Contact information for registrants contact_info = db.Column(db.String, nullable=False, default='') #: Datetime when the registration form is open start_dt = db.Column(UTCDateTime, nullable=True) #: Datetime when the registration form is closed end_dt = db.Column(UTCDateTime, nullable=True) #: Whether registration modifications are allowed modification_mode = db.Column(PyIntEnum(ModificationMode), nullable=False, default=ModificationMode.not_allowed) #: Datetime when the modification period is over modification_end_dt = db.Column(UTCDateTime, nullable=True) #: Whether the registration has been marked as deleted is_deleted = db.Column(db.Boolean, nullable=False, default=False) #: Whether users must be logged in to register require_login = db.Column(db.Boolean, nullable=False, default=False) #: Whether registrations must be associated with an Indico account require_user = db.Column(db.Boolean, nullable=False, default=False) #: Maximum number of registrations allowed registration_limit = db.Column(db.Integer, nullable=True) #: Whether registrations should be displayed in the participant list publish_registrations_enabled = db.Column(db.Boolean, nullable=False, default=False) #: Whether to display the number of registrations publish_registration_count = db.Column(db.Boolean, nullable=False, default=False) #: Whether checked-in status should be displayed in the event pages and participant list publish_checkin_enabled = db.Column(db.Boolean, nullable=False, default=False) #: Whether registrations must be approved by a manager moderation_enabled = db.Column(db.Boolean, nullable=False, default=False) #: The base fee users have to pay when registering base_price = db.Column( db.Numeric(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 Editable(db.Model): __tablename__ = 'editables' __table_args__ = (db.UniqueConstraint('contribution_id', 'type'), { 'schema': 'event_editing' }) id = db.Column(db.Integer, primary_key=True) contribution_id = db.Column(db.ForeignKey('events.contributions.id'), index=True, nullable=False) type = db.Column(PyIntEnum(EditableType), nullable=False) editor_id = db.Column(db.ForeignKey('users.users.id'), index=True, nullable=True) published_revision_id = db.Column( db.ForeignKey('event_editing.revisions.id'), index=True, nullable=True) contribution = db.relationship('Contribution', lazy=True, backref=db.backref( 'editables', lazy=True, )) editor = db.relationship('User', lazy=True, backref=db.backref('editor_for_editables', lazy='dynamic')) published_revision = db.relationship( 'EditingRevision', foreign_keys=published_revision_id, lazy=True, ) # relationship backrefs: # - revisions (EditingRevision.editable) def __repr__(self): return format_repr(self, 'id', 'contribution_id', 'type') @locator_property def locator(self): return dict(self.contribution.locator, type=self.type.name) @property def event(self): return self.contribution.event def _has_general_editor_permissions(self, user): """Whether the user has general editor permissions on the Editable. This means that the user has editor permissions for the editable's type, but does not need to be the assigned editor. """ # Editing (and event) managers always have editor-like access return (self.event.can_manage(user, permission='editing_manager') or self.event.can_manage( user, permission=self.type.editor_permission)) def can_see_timeline(self, user): """Whether the user can see the editable's timeline. This is pure read access, without any ability to make changes or leave comments. """ # Anyone with editor access to the editable's type can see the timeline. # Users associated with the editable's contribution can do so as well. return (self._has_general_editor_permissions(user) or self.contribution.can_submit_proceedings(user) or self.contribution.is_user_associated(user, check_abstract=True)) def can_perform_submitter_actions(self, user): """Whether the user can perform any submitter actions. These are actions such as uploading a new revision after having been asked to make changes or approving/rejecting changes made by an editor. """ # If the user can't even see the timeline, we never allow any modifications if not self.can_see_timeline(user): return False # Anyone who can submit new proceedings can also perform submitter actions, # i.e. the abstract submitter and anyone with submission access to the contribution. return self.contribution.can_submit_proceedings(user) def can_perform_editor_actions(self, user): """Whether the user can perform any Editing actions. These are actions usually made by the assigned Editor of the editable, such as making changes, asking the user to make changes, or approving/rejecting the editable. """ from indico.modules.events.editing.settings import editable_type_settings # If the user can't even see the timeline, we never allow any modifications if not self.can_see_timeline(user): return False # Editing/event managers can perform actions when they are the assigned editor # even when editing is disabled in the settings if self.editor == user and self.event.can_manage( user, permission='editing_manager'): return True # Editing needs to be enabled in the settings otherwise if not editable_type_settings[self.type].get(self.event, 'editing_enabled'): return False # Editors need the permission on the editable type and also be the assigned editor if self.editor == user and self.event.can_manage( user, permission=self.type.editor_permission): return True return False def can_use_internal_comments(self, user): """Whether the user can create/see internal comments.""" return self._has_general_editor_permissions(user) def can_comment(self, user): """Whether the user can comment on the editable.""" # We allow any user associated with the contribution to comment, even if they are # not authorized to actually perform submitter actions. return ( self.event.can_manage(user, permission=self.type.editor_permission) or self.event.can_manage(user, permission='editing_manager') or self.contribution.is_user_associated(user, check_abstract=True)) def can_assign_self(self, user): """Whether the user can assign themselves on the editable.""" from indico.modules.events.editing.settings import editable_type_settings type_settings = editable_type_settings[self.type] if self.editor and (self.editor == user or not self.can_unassign(user)): return False return ((self.event.can_manage(user, permission=self.type.editor_permission) and type_settings.get(self.event, 'editing_enabled') and type_settings.get(self.event, 'self_assign_allowed')) or self.event.can_manage(user, permission='editing_manager')) def can_unassign(self, user): """Whether the user can unassign the editor of the editable.""" from indico.modules.events.editing.settings import editable_type_settings type_settings = editable_type_settings[self.type] return (self.event.can_manage(user, permission='editing_manager') or (self.editor == user and self.event.can_manage( user, permission=self.type.editor_permission) and type_settings.get(self.event, 'editing_enabled') and type_settings.get(self.event, 'self_assign_allowed'))) @property def review_conditions_valid(self): from indico.modules.events.editing.models.review_conditions import EditingReviewCondition query = EditingReviewCondition.query.with_parent( self.event).filter_by(type=self.type) review_conditions = [{ft.id for ft in cond.file_types} for cond in query] file_types = {file.file_type_id for file in self.revisions[-1].files} if not review_conditions: return True return any(file_types >= cond for cond in review_conditions) @property def editing_enabled(self): from indico.modules.events.editing.settings import editable_type_settings return editable_type_settings[self.type].get(self.event, 'editing_enabled') @property def external_timeline_url(self): return url_for('event_editing.editable', self, _external=True) @property def timeline_url(self): return url_for('event_editing.editable', self) def log(self, *args, **kwargs): """Log with prefilled metadata for the editable.""" self.event.log(*args, meta={'editable_id': self.id}, **kwargs)
class Reservation(Serializer, db.Model): __tablename__ = 'reservations' __public__ = [] __calendar_public__ = [ 'id', ('booked_for_name', 'bookedForName'), ('booking_reason', 'reason'), ('details_url', 'bookingUrl') ] __api_public__ = [ 'id', ('start_dt', 'startDT'), ('end_dt', 'endDT'), 'repeat_frequency', 'repeat_interval', ('booked_for_name', 'bookedForName'), ('details_url', 'bookingUrl'), ('booking_reason', 'reason'), ('uses_vc', 'usesAVC'), ('needs_vc_assistance', 'needsAVCSupport'), 'needs_assistance', ('is_accepted', 'isConfirmed'), ('is_valid', 'isValid'), 'is_cancelled', 'is_rejected', ('location_name', 'location'), ('contact_email', 'booked_for_user_email') ] @declared_attr def __table_args__(cls): return (db.Index('ix_reservations_start_dt_date', cast(cls.start_dt, Date)), db.Index('ix_reservations_end_dt_date', cast(cls.end_dt, Date)), db.Index('ix_reservations_start_dt_time', cast(cls.start_dt, Time)), db.Index('ix_reservations_end_dt_time', cast(cls.end_dt, Time)), { 'schema': 'roombooking' }) id = db.Column(db.Integer, primary_key=True) created_dt = db.Column(UTCDateTime, nullable=False, default=now_utc) start_dt = db.Column(db.DateTime, nullable=False, index=True) end_dt = db.Column(db.DateTime, nullable=False, index=True) repeat_frequency = db.Column( PyIntEnum(RepeatFrequency), nullable=False, default=RepeatFrequency.NEVER) # week, month, year, etc. repeat_interval = db.Column(db.SmallInteger, nullable=False, default=0) # 1, 2, 3, etc. booked_for_id = db.Column( db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=True, # Must be nullable for legacy data :( ) booked_for_name = db.Column(db.String, nullable=False) created_by_id = db.Column( db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=True, # Must be nullable for legacy data :( ) room_id = db.Column(db.Integer, db.ForeignKey('roombooking.rooms.id'), nullable=False, index=True) is_accepted = db.Column(db.Boolean, nullable=False) is_cancelled = db.Column(db.Boolean, nullable=False, default=False) is_rejected = db.Column(db.Boolean, nullable=False, default=False) booking_reason = db.Column(db.Text, nullable=False) rejection_reason = db.Column(db.String) uses_vc = db.Column(db.Boolean, nullable=False, default=False) needs_vc_assistance = db.Column(db.Boolean, nullable=False, default=False) needs_assistance = db.Column(db.Boolean, nullable=False, default=False) event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), nullable=True, index=True) edit_logs = db.relationship('ReservationEditLog', backref='reservation', cascade='all, delete-orphan', lazy='dynamic') occurrences = db.relationship('ReservationOccurrence', backref='reservation', cascade='all, delete-orphan', lazy='dynamic') #: The user this booking was made for. #: Assigning a user here also updates `booked_for_name`. booked_for_user = db.relationship('User', lazy=False, foreign_keys=[booked_for_id], backref=db.backref( 'reservations_booked_for', lazy='dynamic')) #: The user who created this booking. created_by_user = db.relationship('User', lazy=False, foreign_keys=[created_by_id], backref=db.backref('reservations', lazy='dynamic')) #: The Event this reservation was made for event = db.relationship('Event', lazy=True, backref=db.backref('reservations', lazy='dynamic')) # relationship backrefs: # - room (Room.reservations) @hybrid_property def is_archived(self): return self.end_dt < datetime.now() @hybrid_property def is_pending(self): return not (self.is_accepted or self.is_rejected or self.is_cancelled) @is_pending.expression def is_pending(self): return ~(Reservation.is_accepted | Reservation.is_rejected | Reservation.is_cancelled) @hybrid_property def is_repeating(self): return self.repeat_frequency != RepeatFrequency.NEVER @hybrid_property def is_valid(self): return self.is_accepted and not (self.is_rejected or self.is_cancelled) @is_valid.expression def is_valid(self): return self.is_accepted & ~(self.is_rejected | self.is_cancelled) @property def contact_email(self): return self.booked_for_user.email if self.booked_for_user else None @property def contact_phone(self): return self.booked_for_user.phone if self.booked_for_user else None @property def details_url(self): return url_for('rooms.roomBooking-bookingDetails', self, _external=True) @property def location_name(self): return self.room.location_name @property def repetition(self): return self.repeat_frequency, self.repeat_interval @property def status_string(self): parts = [] if self.is_valid: parts.append(_(u"Valid")) else: if self.is_cancelled: parts.append(_(u"Cancelled")) if self.is_rejected: parts.append(_(u"Rejected")) if not self.is_accepted: parts.append(_(u"Not confirmed")) if self.is_archived: parts.append(_(u"Archived")) else: parts.append(_(u"Live")) return u', '.join(map(unicode, parts)) @return_ascii def __repr__(self): return u'<Reservation({0}, {1}, {2}, {3}, {4})>'.format( self.id, self.room_id, self.booked_for_name, self.start_dt, self.end_dt) @classmethod def create_from_data(cls, room, data, user, prebook=None, ignore_admin=False): """Creates a new reservation. :param room: The Room that's being booked. :param data: A dict containing the booking data, usually from a :class:`NewBookingConfirmForm` instance :param user: The :class:`.User` who creates the booking. :param prebook: Instead of determining the booking type from the user's permissions, always use the given mode. """ populate_fields = ('start_dt', 'end_dt', 'repeat_frequency', 'repeat_interval', 'room_id', 'contact_email', 'contact_phone', 'booking_reason', 'needs_assistance', 'uses_vc', 'needs_vc_assistance', 'event_id') if data['repeat_frequency'] == RepeatFrequency.NEVER and data[ 'start_dt'].date() != data['end_dt'].date(): raise ValueError('end_dt != start_dt for non-repeating booking') if prebook is None: prebook = not room.can_be_booked(user, ignore_admin=ignore_admin) if prebook and not room.can_be_prebooked( user, ignore_admin=ignore_admin): raise NoReportError(u'You cannot book this room') room.check_advance_days(data['end_dt'].date(), user) room.check_bookable_hours(data['start_dt'].time(), data['end_dt'].time(), user) reservation = cls() for field in populate_fields: if field in data: setattr(reservation, field, data[field]) reservation.room = room # if 'room_usage' is not specified, we'll take whatever is passed in 'booked_for_user' reservation.booked_for_user = data['booked_for_user'] if data.get( 'room_usage') != 'current_user' else user reservation.booked_for_name = reservation.booked_for_user.full_name reservation.is_accepted = not prebook reservation.created_by_user = user reservation.create_occurrences(True) if not any(occ.is_valid for occ in reservation.occurrences): raise NoReportError(_(u'Reservation has no valid occurrences')) notify_creation(reservation) return reservation @staticmethod def get_with_data(*args, **kwargs): filters = kwargs.pop('filters', None) limit = kwargs.pop('limit', None) offset = kwargs.pop('offset', 0) order = kwargs.pop('order', Reservation.start_dt) limit_per_room = kwargs.pop('limit_per_room', False) occurs_on = kwargs.pop('occurs_on') if kwargs: raise ValueError('Unexpected kwargs: {}'.format(kwargs)) query = Reservation.query.options(joinedload(Reservation.room)) if filters: query = query.filter(*filters) if occurs_on: query = query.filter( Reservation.id.in_( db.session.query( ReservationOccurrence.reservation_id).filter( ReservationOccurrence.date.in_(occurs_on), ReservationOccurrence.is_valid))) if limit_per_room and (limit or offset): query = limit_groups(query, Reservation, Reservation.room_id, order, limit, offset) query = query.order_by(order, Reservation.created_dt) if not limit_per_room: if limit: query = query.limit(limit) if offset: query = query.offset(offset) result = OrderedDict((r.id, {'reservation': r}) for r in query) if 'occurrences' in args: occurrence_data = OrderedMultiDict( db.session.query(ReservationOccurrence.reservation_id, ReservationOccurrence).filter( ReservationOccurrence.reservation_id.in_( result.iterkeys())).order_by( ReservationOccurrence.start_dt)) for id_, data in result.iteritems(): data['occurrences'] = occurrence_data.getlist(id_) return result.values() @staticmethod def find_overlapping_with(room, occurrences, skip_reservation_id=None): return Reservation.find( Reservation.room == room, Reservation.id != skip_reservation_id, ReservationOccurrence.is_valid, ReservationOccurrence.filter_overlap(occurrences), _join=ReservationOccurrence) @unify_user_args def accept(self, user): self.is_accepted = True self.add_edit_log( ReservationEditLog(user_name=user.full_name, info=['Reservation accepted'])) notify_confirmation(self) valid_occurrences = self.occurrences.filter( ReservationOccurrence.is_valid).all() pre_occurrences = ReservationOccurrence.find_overlapping_with( self.room, valid_occurrences, self.id).all() for occurrence in pre_occurrences: if not occurrence.is_valid: continue occurrence.reject( user, u'Rejected due to collision with a confirmed reservation') @unify_user_args def cancel(self, user, reason=None, silent=False): self.is_cancelled = True self.rejection_reason = reason self.occurrences.filter_by(is_valid=True).update( { 'is_cancelled': True, 'rejection_reason': reason }, synchronize_session='fetch') if not silent: notify_cancellation(self) log_msg = u'Reservation cancelled: {}'.format( reason) if reason else 'Reservation cancelled' self.add_edit_log( ReservationEditLog(user_name=user.full_name, info=[log_msg])) @unify_user_args def reject(self, user, reason, silent=False): self.is_rejected = True self.rejection_reason = reason self.occurrences.filter_by(is_valid=True).update( { 'is_rejected': True, 'rejection_reason': reason }, synchronize_session='fetch') if not silent: notify_rejection(self) log_msg = u'Reservation rejected: {}'.format(reason) self.add_edit_log( ReservationEditLog(user_name=user.full_name, info=[log_msg])) def add_edit_log(self, edit_log): self.edit_logs.append(edit_log) db.session.flush() @unify_user_args def can_be_accepted(self, user): if user is None: return False return rb_is_admin(user) or self.room.is_owned_by(user) @unify_user_args def can_be_cancelled(self, user): if user is None: return False return self.is_owned_by(user) or rb_is_admin( user) or self.is_booked_for(user) @unify_user_args def can_be_deleted(self, user): if user is None: return False return rb_is_admin(user) @unify_user_args def can_be_modified(self, user): if user is None: return False if self.is_rejected or self.is_cancelled: return False if rb_is_admin(user): return True return self.created_by_user == user or self.is_booked_for( user) or self.room.is_owned_by(user) @unify_user_args def can_be_rejected(self, user): if user is None: return False return rb_is_admin(user) or self.room.is_owned_by(user) def create_occurrences(self, skip_conflicts, user=None): ReservationOccurrence.create_series_for_reservation(self) db.session.flush() if user is None: user = self.created_by_user # Check for conflicts with nonbookable periods if not rb_is_admin(user) and not self.room.is_owned_by(user): nonbookable_periods = self.room.nonbookable_periods.filter( NonBookablePeriod.end_dt > self.start_dt) for occurrence in self.occurrences: if not occurrence.is_valid: continue for nbd in nonbookable_periods: if nbd.overlaps(occurrence.start_dt, occurrence.end_dt): if not skip_conflicts: raise ConflictingOccurrences() occurrence.cancel(user, u'Skipped due to nonbookable date', silent=True, propagate=False) break # Check for conflicts with blockings blocked_rooms = self.room.get_blocked_rooms( *(occurrence.start_dt for occurrence in self.occurrences)) for br in blocked_rooms: blocking = br.blocking if blocking.can_be_overridden(user, self.room): continue for occurrence in self.occurrences: if occurrence.is_valid and blocking.is_active_at( occurrence.start_dt.date()): # Cancel OUR occurrence msg = u'Skipped due to collision with a blocking ({})' occurrence.cancel(user, msg.format(blocking.reason), silent=True, propagate=False) # Check for conflicts with other occurrences conflicting_occurrences = self.get_conflicting_occurrences() for occurrence, conflicts in conflicting_occurrences.iteritems(): if not occurrence.is_valid: continue if conflicts['confirmed']: if not skip_conflicts: raise ConflictingOccurrences() # Cancel OUR occurrence msg = u'Skipped due to collision with {} reservation(s)' occurrence.cancel(user, msg.format(len(conflicts['confirmed'])), silent=True, propagate=False) elif conflicts['pending'] and self.is_accepted: # Reject OTHER occurrences for conflict in conflicts['pending']: conflict.reject( user, u'Rejected due to collision with a confirmed reservation' ) def find_excluded_days(self): return self.occurrences.filter(~ReservationOccurrence.is_valid) def find_overlapping(self): occurrences = self.occurrences.filter( ReservationOccurrence.is_valid).all() return Reservation.find_overlapping_with(self.room, occurrences, self.id) @locator_property def locator(self): return {'roomLocation': self.location_name, 'resvID': self.id} def get_conflicting_occurrences(self): valid_occurrences = self.occurrences.filter( ReservationOccurrence.is_valid).all() colliding_occurrences = ReservationOccurrence.find_overlapping_with( self.room, valid_occurrences, self.id).all() conflicts = defaultdict(lambda: dict(confirmed=[], pending=[])) for occurrence in valid_occurrences: for colliding in colliding_occurrences: if occurrence.overlaps(colliding): key = 'confirmed' if colliding.reservation.is_accepted else 'pending' conflicts[occurrence][key].append(colliding) return conflicts def is_booked_for(self, user): return user is not None and self.booked_for_user == user @unify_user_args def is_owned_by(self, user): return self.created_by_user == user def modify(self, data, user): """Modifies an existing reservation. :param data: A dict containing the booking data, usually from a :class:`ModifyBookingForm` instance :param user: The :class:`.User` who modifies the booking. """ populate_fields = ('start_dt', 'end_dt', 'repeat_frequency', 'repeat_interval', 'booked_for_user', 'contact_email', 'contact_phone', 'booking_reason', 'needs_assistance', 'uses_vc', 'needs_vc_assistance') # fields affecting occurrences occurrence_fields = { 'start_dt', 'end_dt', 'repeat_frequency', 'repeat_interval' } # fields where date and time are compared separately date_time_fields = {'start_dt', 'end_dt'} # fields for the repetition repetition_fields = {'repeat_frequency', 'repeat_interval'} # pretty names for logging field_names = { 'start_dt/date': u"start date", 'end_dt/date': u"end date", 'start_dt/time': u"start time", 'end_dt/time': u"end time", 'repetition': u"booking type", 'booked_for_user': u"'Booked for' user", 'contact_email': u"contact email", 'contact_phone': u"contact phone number", 'booking_reason': u"booking reason", 'needs_assistance': u"option 'General Assistance'", 'uses_vc': u"option 'Uses Videoconference'", 'needs_vc_assistance': u"option 'Videoconference Setup Assistance'" } self.room.check_advance_days(data['end_dt'].date(), user) self.room.check_bookable_hours(data['start_dt'].time(), data['end_dt'].time(), user) if data['room_usage'] == 'current_user': data['booked_for_user'] = session.user changes = {} update_occurrences = False old_repetition = self.repetition for field in populate_fields: if field not in data: continue old = getattr(self, field) new = data[field] converter = unicode if old != new: # Booked for user updates the (redundant) name if field == 'booked_for_user': old = self.booked_for_name new = self.booked_for_name = data[field].full_name # Apply the change setattr(self, field, data[field]) # If any occurrence-related field changed we need to recreate the occurrences if field in occurrence_fields: update_occurrences = True # Record change for history entry if field in date_time_fields: # The date/time fields create separate entries for the date and time parts if old.date() != new.date(): changes[field + '/date'] = { 'old': old.date(), 'new': new.date(), 'converter': format_date } if old.time() != new.time(): changes[field + '/time'] = { 'old': old.time(), 'new': new.time(), 'converter': format_time } elif field in repetition_fields: # Repetition needs special handling since it consists of two fields but they are tied together # We simply update it whenever we encounter such a change; after the last change we end up with # the correct change data changes['repetition'] = { 'old': old_repetition, 'new': self.repetition, 'converter': lambda x: RepeatMapping.get_message(*x) } else: changes[field] = { 'old': old, 'new': new, 'converter': converter } if not changes: return False # Create a verbose log entry for the modification log = [u'Booking modified'] for field, change in changes.iteritems(): field_title = field_names.get(field, field) converter = change['converter'] old = to_unicode(converter(change['old'])) new = to_unicode(converter(change['new'])) if not old: log.append(u"The {} was set to '{}'".format(field_title, new)) elif not new: log.append(u"The {} was cleared".format(field_title)) else: log.append(u"The {} was changed from '{}' to '{}'".format( field_title, old, new)) self.edit_logs.append( ReservationEditLog(user_name=user.full_name, info=log)) # Recreate all occurrences if necessary if update_occurrences: cols = [ col.name for col in ReservationOccurrence.__table__.columns if not col.primary_key and col.name not in {'start_dt', 'end_dt'} ] old_occurrences = {occ.date: occ for occ in self.occurrences} self.occurrences.delete(synchronize_session='fetch') self.create_occurrences(True, user) db.session.flush() # Restore rejection data etc. for recreated occurrences for occurrence in self.occurrences: old_occurrence = old_occurrences.get(occurrence.date) # Copy data from old occurrence UNLESS the new one is invalid (e.g. because of collisions) # Otherwise we'd end up with valid occurrences ignoring collisions! if old_occurrence and occurrence.is_valid: for col in cols: setattr(occurrence, col, getattr(old_occurrence, col)) # Don't cause new notifications for the entire booking in case of daily repetition if self.repeat_frequency == RepeatFrequency.DAY and all( occ.notification_sent for occ in old_occurrences.itervalues()): for occurrence in self.occurrences: occurrence.notification_sent = True # Sanity check so we don't end up with an "empty" booking if not any(occ.is_valid for occ in self.occurrences): raise NoReportError(_(u'Reservation has no valid occurrences')) notify_modification(self, changes) return True
def event(cls): return db.relationship('Event', foreign_keys=cls.event_id, lazy=True, backref=db.backref(cls.events_backref_name, lazy='dynamic'))
class Request(db.Model): """Event-related requests, e.g. for a webcast.""" __tablename__ = 'requests' __table_args__ = {'schema': 'events'} #: request 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, nullable=False) #: the request type name type = db.Column(db.String, nullable=False) #: the requests's date, a :class:`RequestState` value state = db.Column(PyIntEnum(RequestState), nullable=False, default=RequestState.pending) #: plugin-specific data of the request data = db.Column(JSONB, nullable=False) #: ID of the user creating the request created_by_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=False) #: the date/time the request was created created_dt = db.Column(UTCDateTime, default=now_utc, index=True, nullable=False) #: ID of the user processing the request processed_by_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=True) #: the date/time the request was accepted/rejected processed_dt = db.Column(UTCDateTime, nullable=True) #: an optional comment for an accepted/rejected request comment = db.Column(db.Text, nullable=True) #: The user who created the request created_by_user = db.relationship('User', lazy=True, foreign_keys=[created_by_id], backref=db.backref('requests_created', lazy='dynamic')) #: The user who processed the request processed_by_user = db.relationship('User', lazy=True, foreign_keys=[processed_by_id], backref=db.backref( 'requests_processed', lazy='dynamic')) #: The Event this agreement is associated with event = db.relationship('Event', lazy=True, backref=db.backref('requests', lazy='dynamic')) @property def definition(self): return get_request_definitions().get(self.type) @definition.setter def definition(self, definition): assert self.type is None self.type = definition.name @property def can_be_modified(self): """Determine if the request can be modified or if a new one must be sent.""" return self.state in {RequestState.pending, RequestState.accepted} @property def locator(self): return {'confId': self.event_id, 'type': self.type} def __repr__(self): state = self.state.name if self.state is not None else None return f'<Request({self.id}, {self.event_id}, {self.type}, {state})>' @classmethod def find_latest_for_event(cls, event, type_=None): """Return the latest requests for a given event. :param event: the event to find the requests for :param type_: the request type to retrieve, or `None` to get all :return: a dict mapping request types to a :class:`Request` or if `type_` was specified, a single :class:`Request` or `None` """ query = Request.query.with_parent(event) if type_ is not None: return (query.filter_by(type=type_).order_by( cls.created_dt.desc()).first()) else: query = limit_groups(query, cls, cls.type, cls.created_dt.desc(), 1) return {req.type: req for req in query}
class User(db.Model): """Indico users""" __tablename__ = 'users' __table_args__ = {'schema': 'users'} #: the unique id of the user id = db.Column(db.Integer, primary_key=True) #: the first name of the user first_name = db.Column(db.String, nullable=False, index=True) #: the last/family name of the user last_name = db.Column(db.String, nullable=False, index=True) # the title of the user - you usually want the `title` property! _title = db.Column('title', PyIntEnum(UserTitle), nullable=False, default=UserTitle.none) #: the phone number of the user phone = db.Column(db.String, nullable=False, default='') #: the address of the user address = db.Column(db.Text, nullable=False, default='') #: the id of the user this user has been merged into merged_into_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), nullable=True) #: if the user is an administrator with unrestricted access to everything is_admin = db.Column(db.Boolean, nullable=False, default=False, index=True) #: if the user has been blocked is_blocked = db.Column(db.Boolean, nullable=False, default=False) #: if the user is pending (e.g. never logged in, only added to some list) is_pending = db.Column(db.Boolean, nullable=False, default=False) #: if the user is deleted (e.g. due to a merge) _is_deleted = db.Column('is_deleted', db.Boolean, nullable=False, default=False) _affiliation = db.relationship('UserAffiliation', lazy=False, uselist=False, cascade='all, delete-orphan', backref=db.backref('user', lazy=True)) _primary_email = db.relationship( 'UserEmail', lazy=False, uselist=False, cascade='all, delete-orphan', primaryjoin='(User.id == UserEmail.user_id) & UserEmail.is_primary') _secondary_emails = db.relationship( 'UserEmail', lazy=True, cascade='all, delete-orphan', collection_class=set, primaryjoin='(User.id == UserEmail.user_id) & ~UserEmail.is_primary') _all_emails = db.relationship('UserEmail', lazy=True, viewonly=True, primaryjoin='User.id == UserEmail.user_id', collection_class=set, backref=db.backref('user', lazy=False)) #: the affiliation of the user affiliation = association_proxy('_affiliation', 'name', creator=lambda v: UserAffiliation(name=v)) #: the primary email address of the user email = association_proxy( '_primary_email', 'email', creator=lambda v: UserEmail(email=v, is_primary=True)) #: any additional emails the user might have secondary_emails = association_proxy('_secondary_emails', 'email', creator=lambda v: UserEmail(email=v)) #: all emails of the user. read-only; use it only for searching by email! also, do not use it between #: modifying `email` or `secondary_emails` and a session expire/commit! all_emails = association_proxy('_all_emails', 'email') # read-only! #: the user this user has been merged into merged_into_user = db.relationship( 'User', lazy=True, backref=db.backref('merged_from_users', lazy=True), remote_side='User.id', ) #: the users's favorite users favorite_users = db.relationship( 'User', secondary=favorite_user_table, primaryjoin=id == favorite_user_table.c.user_id, secondaryjoin=(id == favorite_user_table.c.target_id) & ~_is_deleted, lazy=True, collection_class=set, backref=db.backref('favorite_of', lazy=True, collection_class=set), ) _favorite_categories = db.relationship('FavoriteCategory', lazy=True, cascade='all, delete-orphan', collection_class=set) #: the users's favorite categories favorite_categories = association_proxy( '_favorite_categories', 'target', creator=lambda x: FavoriteCategory(target=x)) #: the legacy objects the user is connected to linked_objects = db.relationship('UserLink', lazy='dynamic', 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: # - local_groups (User.local_groups) @property def as_principal(self): """The serializable principal identifier of this user""" return 'User', self.id @property def as_avatar(self): # TODO: remove this after DB is free of Avatars from indico.modules.users.legacy import AvatarUserWrapper avatar = AvatarUserWrapper(self.id) # avoid garbage collection avatar.user return avatar @property def external_identities(self): """The external identities of the user""" return {x for x in self.identities if x.provider != 'indico'} @property def local_identities(self): """The local identities of the user""" return {x for x in self.identities if x.provider == 'indico'} @property def local_identity(self): """The main (most recently used) local identity""" identities = sorted(self.local_identities, key=attrgetter('safe_last_login_dt'), reverse=True) return identities[0] if identities else None @property def secondary_local_identities(self): """The local identities of the user except the main one""" return self.local_identities - {self.local_identity} @property def locator(self): return {'user_id': self.id} @hybrid_property def title(self): """the title of the user""" return self._title.title @title.expression def title(cls): return cls._title @title.setter def title(self, value): self._title = value @hybrid_property def is_deleted(self): return self._is_deleted @is_deleted.setter def is_deleted(self, value): self._is_deleted = value # not using _all_emails here since it only contains newly added emails after an expire/commit if self._primary_email: self._primary_email.is_user_deleted = value for email in self._secondary_emails: email.is_user_deleted = value @cached_property def settings(self): """Returns the user settings proxy for this user""" from indico.modules.users import user_settings return user_settings.bind(self) @property def full_name(self): """Returns the user's name in 'Firstname Lastname' notation.""" return self.get_full_name(last_name_first=False, last_name_upper=False, abbrev_first_name=False) @property def synced_fields(self): """The fields of the user whose values are currently synced. This set is always a subset of the synced fields define in synced fields of the idp in 'indico.conf'. """ synced_fields = self.settings.get('synced_fields') # If synced_fields is missing or None, then all fields are synced if synced_fields is None: return multipass.synced_fields else: return set(synced_fields) & multipass.synced_fields @synced_fields.setter def synced_fields(self, value): value = set(value) & multipass.synced_fields if value == multipass.synced_fields: self.settings.delete('synced_fields') else: self.settings.set('synced_fields', list(value)) @property def synced_values(self): """The values from the synced identity for the user. Those values are not the actual user's values and might differ if they are not set as synchronized. """ identity = self._get_synced_identity(refresh=False) if identity is None: return {} return { field: (identity.data.get(field) or '') for field in multipass.synced_fields } @return_ascii def __repr__(self): return '<User({}, {}, {}, {})>'.format(self.id, self.first_name, self.last_name, self.email) def can_be_modified(self, user): """If this user can be modified by the given user""" return self == user or user.is_admin def get_full_name(self, last_name_first=True, last_name_upper=True, abbrev_first_name=True, show_title=False): """Returns the user's name in the specified 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 :param show_title: if the title of the user should be included """ last_name = self.last_name.upper( ) if last_name_upper else self.last_name first_name = '{}.'.format(self.first_name[0].upper() ) if abbrev_first_name else self.first_name full_name = '{}, {}'.format( last_name, first_name) if last_name_first else '{} {}'.format( first_name, last_name) return full_name if not show_title or not self.title else '{} {}'.format( self.title, full_name) 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 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 is_in_group(self, group): """Checks if the user is in a group :param group: A :class:`GroupProxy` """ return group.has_member(self) def get_linked_roles(self, type_): """Retrieves the roles the user is linked to for a given type""" return UserLink.get_linked_roles(self, type_) def get_linked_objects(self, type_, role): """Retrieves linked objects for the user""" return UserLink.get_links(self, type_, role) def link_to(self, obj, role): """Adds a link between the user and an object :param obj: a legacy object :param role: the role to use in the link """ return UserLink.create_link(self, obj, role) def unlink_to(self, obj, role): """Removes a link between the user and an object :param obj: a legacy object :param role: the role to use in the link """ return UserLink.remove_link(self, obj, role) 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('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 Attachment(ProtectionMixin, VersionedResourceMixin, db.Model): __tablename__ = 'attachments' __table_args__ = ( # links: url but no file db.CheckConstraint( f'type != {AttachmentType.link.value} OR (link_url IS NOT NULL AND file_id IS NULL)', 'valid_link'), # we can't require the file_id to be NOT NULL for files because of the circular relationship... # but we can ensure that we never have both a file_id AND a link_url...for db.CheckConstraint('link_url IS NULL OR file_id IS NULL', 'link_or_file'), { 'schema': 'attachments' }) stored_file_table = 'attachments.files' stored_file_class = AttachmentFile stored_file_fkey = 'attachment_id' #: The ID of the attachment id = db.Column(db.Integer, primary_key=True) #: The ID of the folder the attachment belongs to folder_id = db.Column(db.Integer, db.ForeignKey('attachments.folders.id'), nullable=False, index=True) #: The ID of the user who created the attachment user_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=False) #: If the attachment has been deleted is_deleted = db.Column(db.Boolean, nullable=False, default=False) #: The name of the attachment title = db.Column(db.String, nullable=False) #: The description of the attachment description = db.Column(db.Text, nullable=False, default='') #: The date/time when the attachment was created/modified modified_dt = db.Column(UTCDateTime, nullable=False, default=now_utc, onupdate=now_utc) #: The type of the attachment (file or link) type = db.Column(PyIntEnum(AttachmentType), nullable=False) #: The target URL for a link attachment link_url = db.Column(db.String, nullable=True) #: The user who created the attachment user = db.relationship('User', lazy=True, backref=db.backref('attachments', lazy='dynamic')) #: The folder containing the attachment folder = db.relationship('AttachmentFolder', lazy=True, backref=db.backref('all_attachments', lazy=True)) acl_entries = db.relationship('AttachmentPrincipal', backref='attachment', cascade='all, delete-orphan', collection_class=set) #: The ACL of the folder (used for ProtectionMode.protected) acl = association_proxy('acl_entries', 'principal', creator=lambda v: AttachmentPrincipal(principal=v)) # relationship backrefs: # - legacy_mapping (LegacyAttachmentMapping.attachment) @property def protection_parent(self): return self.folder @property def locator(self): return dict(self.folder.locator, attachment_id=self.id) def get_download_url(self, absolute=False): """Return the download url for the attachment. During static site generation this returns a local URL for the file or the target URL for the link. :param absolute: If the returned URL should be absolute. """ if g.get('static_site'): return _offline_download_url(self) else: filename = self.file.filename if self.type == AttachmentType.file else 'go' return url_for('attachments.download', self, filename=filename, _external=absolute) @property def download_url(self): """The download url for the attachment.""" return self.get_download_url() @property def absolute_download_url(self): """The absolute download url for the attachment.""" return self.get_download_url(absolute=True) def can_access(self, user, *args, **kwargs): """Check if the user is allowed to access the attachment. This is the case if the user has access to see the attachment or if the user can manage attachments for the linked object. """ return (super().can_access(user, *args, **kwargs) or can_manage_attachments(self.folder.object, user)) def __repr__(self): return '<Attachment({}, {}, {}{}, {}, {})>'.format( self.id, self.title, self.file if self.type == AttachmentType.file else self.link_url, ', is_deleted=True' if self.is_deleted else '', self.protection_repr, self.folder_id)
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_ATTACHED_NOTES = True ATTACHMENT_FOLDER_ID_COLUMN = 'subcontribution_id' description_wrapper = MarkdownText 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_new(self): return self.contribution.event_new @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 reference_type(cls): return db.relationship('ReferenceType', lazy=False, backref=db.backref(cls.reference_backref_name, cascade='all, delete-orphan', lazy=True))
class PaperFile(StoredFileMixin, db.Model): __tablename__ = 'files' __table_args__ = {'schema': 'event_paper_reviewing'} # StoredFileMixin settings add_file_date_column = False id = db.Column(db.Integer, primary_key=True) _contribution_id = db.Column('contribution_id', db.Integer, db.ForeignKey('events.contributions.id'), index=True, nullable=False) revision_id = db.Column( db.Integer, db.ForeignKey('event_paper_reviewing.revisions.id'), index=True, nullable=True) _contribution = db.relationship('Contribution', lazy=True, backref=db.backref('_paper_files', lazy=True)) paper_revision = db.relationship('PaperRevision', lazy=True, backref=db.backref( 'files', lazy=True, cascade='all, delete-orphan')) def __init__(self, *args, **kwargs): paper = kwargs.pop('paper', None) if paper: kwargs.setdefault('_contribution', paper.contribution) super().__init__(*args, **kwargs) def __repr__(self): return format_repr(self, 'id', '_contribution_id', content_type=None, _text=text_to_repr(self.filename)) @locator_property def locator(self): return dict(self.paper.locator, file_id=self.id, filename=self.filename) @property def paper(self): return self._contribution.paper @paper.setter def paper(self, paper): self._contribution = paper.contribution def _build_storage_path(self): self.assign_id() path_segments = [ 'event', strict_str(self._contribution.event.id), 'papers', '{}_{}'.format(self.id, strict_str(self._contribution.id)) ] filename = secure_filename(self.filename, 'paper') path = posixpath.join(*(path_segments + [filename])) return config.ATTACHMENT_STORAGE, path
class PaymentTransaction(db.Model): """Payment transactions""" __tablename__ = 'payment_transactions' __table_args__ = (db.CheckConstraint('amount > 0', 'positive_amount'), { 'schema': 'events' }) #: Entry ID id = db.Column(db.Integer, primary_key=True) #: ID of the associated registration registration_id = db.Column( db.Integer, db.ForeignKey('event_registration.registrations.id'), index=True, nullable=False) #: a :class:`TransactionStatus` status = db.Column(PyIntEnum(TransactionStatus), nullable=False) #: the base amount the user needs to pay (without payment-specific fees) amount = db.Column( db.Numeric(8, 2), # max. 999999.99 nullable=False) #: the currency of the payment (ISO string, e.g. EUR or USD) currency = db.Column(db.String, nullable=False) #: the provider of the payment (e.g. manual, PayPal etc.) provider = db.Column(db.String, nullable=False, default='_manual') #: the date and time the transaction was recorded timestamp = db.Column(UTCDateTime, default=now_utc, nullable=False) #: plugin-specific data of the payment data = db.Column(JSONB, nullable=False) #: The associated registration registration = db.relationship('Registration', lazy=True, foreign_keys=[registration_id], backref=db.backref( 'transactions', cascade='all, delete-orphan', lazy=True)) @property def plugin(self): from indico.modules.events.payment.util import get_payment_plugins return get_payment_plugins().get(self.provider) @property def is_manual(self): return self.provider == '_manual' @return_ascii def __repr__(self): # in case of a new object we might not have the default status set status = TransactionStatus( self.status).name if self.status is not None else None return format_repr(self, 'id', 'registration_id', 'provider', 'amount', 'currency', 'timestamp', status=status) def render_details(self): """Renders the transaction details""" if self.is_manual: return render_template( 'events/payment/transaction_details_manual.html', transaction=self) plugin = self.plugin if plugin is None: return '[plugin not loaded: {}]'.format(self.provider) with plugin.plugin_context(): return plugin.render_transaction_details(self) @classmethod def create_next(cls, registration, amount, currency, action, provider=None, data=None): previous_transaction = registration.transaction new_transaction = PaymentTransaction(amount=amount, currency=currency, provider=provider, data=data) try: next_status = TransactionStatusTransition.next( previous_transaction, action, provider) except InvalidTransactionStatus as e: Logger.get('payment').exception("%s (data received: %r)", e, data) return None except InvalidManualTransactionAction as e: Logger.get('payment').exception( "Invalid manual action code '%s' on initial status (data received: %r)", e, data) return None except InvalidTransactionAction as e: Logger.get('payment').exception( "Invalid action code '%s' on initial status (data received: %r)", e, data) return None except IgnoredTransactionAction as e: Logger.get('payment').warning("%s (data received: %r)", e, data) return None except DoublePaymentTransaction: next_status = TransactionStatus.successful Logger.get('payment').info( "Received successful payment for an already paid registration") registration.transaction = new_transaction new_transaction.status = next_status return new_transaction
class SessionBlock(LocationMixin, db.Model): __tablename__ = 'session_blocks' __auto_table_args = ( db.UniqueConstraint( 'id', 'session_id'), # useless but needed for the compound fkey db.CheckConstraint("date_trunc('minute', duration) = duration", 'duration_no_seconds'), { 'schema': 'events' }) location_backref_name = 'session_blocks' @declared_attr def __table_args__(cls): return auto_table_args(cls) id = db.Column(db.Integer, primary_key=True) session_id = db.Column(db.Integer, db.ForeignKey('events.sessions.id'), index=True, nullable=False) title = db.Column(db.String, nullable=False, default='') code = db.Column(db.String, nullable=False, default='') duration = db.Column(db.Interval, nullable=False) #: Persons associated with this session block person_links = db.relationship('SessionBlockPersonLink', lazy=True, cascade='all, delete-orphan', backref=db.backref('session_block', lazy=True)) # relationship backrefs: # - contributions (Contribution.session_block) # - legacy_mapping (LegacySessionBlockMapping.session_block) # - room_reservation_links (ReservationLink.session_block) # - session (Session.blocks) # - timetable_entry (TimetableEntry.session_block) # - vc_room_associations (VCRoomEventAssociation.linked_block) @declared_attr def contribution_count(cls): from indico.modules.events.contributions.models.contributions import Contribution query = (db.select([ db.func.count(Contribution.id) ]).where((Contribution.session_block_id == cls.id) & ~Contribution.is_deleted).correlate_except( Contribution).scalar_subquery()) return db.column_property(query, deferred=True) def __init__(self, **kwargs): # explicitly initialize those relationships with None to avoid # an extra query to check whether there is an object associated # when assigning a new one (e.g. during cloning) kwargs.setdefault('timetable_entry', None) super().__init__(**kwargs) @property def event(self): return self.session.event @locator_property def locator(self): return dict(self.session.locator, block_id=self.id) @property def location_parent(self): return self.session def can_access(self, user, allow_admin=True): return self.session.can_access(user, allow_admin=allow_admin) @property def has_note(self): return self.session.has_note @property def note(self): return self.session.note @property def full_title(self): return f'{self.session.title}: {self.title}' if self.title else self.session.title def can_manage(self, user, allow_admin=True): return self.session.can_manage_blocks(user, allow_admin=allow_admin) def can_manage_attachments(self, user): return self.session.can_manage_attachments(user) def can_edit_note(self, user): return self.session.can_edit_note(user) @property def start_dt(self): return self.timetable_entry.start_dt if self.timetable_entry else None @property def end_dt(self): return self.timetable_entry.start_dt + self.duration if self.timetable_entry else None @property def slug(self): return slugify('b', self.id, self.session.title, self.title, maxlen=30) def __repr__(self): return format_repr(self, 'id', _text=self.title or None)
class Session(DescriptionMixin, ColorMixin, ProtectionManagersMixin, LocationMixin, AttachedItemsMixin, AttachedNotesMixin, db.Model): __tablename__ = 'sessions' __auto_table_args = (db.Index(None, 'friendly_id', 'event_id', unique=True, postgresql_where=db.text('NOT is_deleted')), {'schema': 'events'}) location_backref_name = 'sessions' disallowed_protection_modes = frozenset() inheriting_have_acl = True default_colors = ColorTuple('#202020', '#e3f2d3') allow_relationship_preloading = True PRELOAD_EVENT_ATTACHED_ITEMS = True PRELOAD_EVENT_NOTES = True ATTACHMENT_FOLDER_ID_COLUMN = 'session_id' possible_render_modes = {RenderMode.markdown} default_render_mode = RenderMode.markdown @declared_attr def __table_args__(cls): return auto_table_args(cls) id = db.Column( db.Integer, primary_key=True ) #: The human-friendly ID for the session friendly_id = db.Column( db.Integer, nullable=False, default=_get_next_friendly_id ) event_id = db.Column( db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False ) type_id = db.Column( db.Integer, db.ForeignKey('events.session_types.id'), index=True, nullable=True ) title = db.Column( db.String, nullable=False ) code = db.Column( db.String, nullable=False, default='' ) default_contribution_duration = db.Column( db.Interval, nullable=False, default=timedelta(minutes=20) ) is_deleted = db.Column( db.Boolean, nullable=False, default=False ) event = db.relationship( 'Event', lazy=True, backref=db.backref( 'sessions', primaryjoin='(Session.event_id == Event.id) & ~Session.is_deleted', cascade='all, delete-orphan', lazy=True ) ) acl_entries = db.relationship( 'SessionPrincipal', lazy=True, cascade='all, delete-orphan', collection_class=set, backref='session' ) blocks = db.relationship( 'SessionBlock', lazy=True, cascade='all, delete-orphan', backref=db.backref( 'session', lazy=False ) ) type = db.relationship( 'SessionType', lazy=True, backref=db.backref( 'sessions', lazy=True ) ) # relationship backrefs: # - attachment_folders (AttachmentFolder.session) # - contributions (Contribution.session) # - default_for_tracks (Track.default_session) # - legacy_mapping (LegacySessionMapping.session) # - note (EventNote.session) def __init__(self, **kwargs): # explicitly initialize this relationship with None to avoid # an extra query to check whether there is an object associated # when assigning a new one (e.g. during cloning) kwargs.setdefault('note', None) super(Session, self).__init__(**kwargs) @classmethod def preload_acl_entries(cls, event): cls.preload_relationships(cls.query.with_parent(event), 'acl_entries') @property def location_parent(self): return self.event @property def protection_parent(self): return self.event @property def session(self): """Convenience property so all event entities have it.""" return self @property @memoize_request def start_dt(self): from indico.modules.events.sessions.models.blocks import SessionBlock start_dt = (self.event.timetable_entries .with_entities(TimetableEntry.start_dt) .join('session_block') .filter(TimetableEntry.type == TimetableEntryType.SESSION_BLOCK, SessionBlock.session == self) .order_by(TimetableEntry.start_dt) .first()) return start_dt[0] if start_dt else None @property @memoize_request def end_dt(self): sorted_blocks = sorted(self.blocks, key=attrgetter('timetable_entry.end_dt'), reverse=True) return sorted_blocks[0].timetable_entry.end_dt if sorted_blocks else None @property @memoize_request def conveners(self): from indico.modules.events.sessions.models.blocks import SessionBlock from indico.modules.events.sessions.models.persons import SessionBlockPersonLink return (SessionBlockPersonLink.query .join(SessionBlock) .filter(SessionBlock.session_id == self.id) .distinct(SessionBlockPersonLink.person_id) .all()) @property def is_poster(self): return self.type.is_poster if self.type else False @locator_property def locator(self): return dict(self.event.locator, session_id=self.id) def get_non_inheriting_objects(self): """Get a set of child objects that do not inherit protection.""" return get_non_inheriting_objects(self) @return_ascii def __repr__(self): return format_repr(self, 'id', is_deleted=False, _text=self.title) def can_manage_contributions(self, user, allow_admin=True): """Check whether a user can manage contributions within the session.""" from indico.modules.events.sessions.util import session_coordinator_priv_enabled if user is None: return False elif self.session.can_manage(user, allow_admin=allow_admin): return True elif (self.session.can_manage(user, 'coordinate') and session_coordinator_priv_enabled(self.event, 'manage-contributions')): return True else: return False def can_manage_blocks(self, user, allow_admin=True): """Check whether a user can manage session blocks. This only applies to the blocks themselves, not to contributions inside them. """ from indico.modules.events.sessions.util import session_coordinator_priv_enabled if user is None: return False # full session manager can always manage blocks. this also includes event managers and higher. elif self.session.can_manage(user, allow_admin=allow_admin): return True # session coordiator if block management is allowed elif (self.session.can_manage(user, 'coordinate') and session_coordinator_priv_enabled(self.event, 'manage-blocks')): return True else: return False
class User(PersonMixin, db.Model): """Indico users""" # Useful when dealing with both users and groups in the same code is_group = False is_single_person = True is_event_role = False is_network = False principal_order = 0 principal_type = PrincipalType.user __tablename__ = 'users' __table_args__ = ( db.Index(None, 'is_system', unique=True, postgresql_where=db.text('is_system')), db.CheckConstraint( 'NOT is_system OR (NOT is_blocked AND NOT is_pending AND NOT is_deleted)', 'valid_system_user'), db.CheckConstraint('id != merged_into_id', 'not_merged_self'), db.CheckConstraint( "is_pending OR (first_name != '' AND last_name != '')", 'not_pending_proper_names'), { 'schema': 'users' }) #: the unique id of the user id = db.Column(db.Integer, primary_key=True) #: the first name of the user first_name = db.Column(db.String, nullable=False, index=True) #: the last/family name of the user last_name = db.Column(db.String, nullable=False, index=True) # the title of the user - you usually want the `title` property! _title = db.Column('title', PyIntEnum(UserTitle), nullable=False, default=UserTitle.none) #: the phone number of the user phone = db.Column(db.String, nullable=False, default='') #: the address of the user address = db.Column(db.Text, nullable=False, default='') #: the id of the user this user has been merged into merged_into_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), nullable=True) #: if the user is the default system user is_system = db.Column(db.Boolean, nullable=False, default=False) #: if the user is an administrator with unrestricted access to everything is_admin = db.Column(db.Boolean, nullable=False, default=False, index=True) #: if the user has been blocked is_blocked = db.Column(db.Boolean, nullable=False, default=False) #: if the user is pending (e.g. never logged in, only added to some list) is_pending = db.Column(db.Boolean, nullable=False, default=False) #: if the user is deleted (e.g. due to a merge) is_deleted = db.Column('is_deleted', db.Boolean, nullable=False, default=False) _affiliation = db.relationship('UserAffiliation', lazy=False, uselist=False, cascade='all, delete-orphan', backref=db.backref('user', lazy=True)) _primary_email = db.relationship( 'UserEmail', lazy=False, uselist=False, cascade='all, delete-orphan', primaryjoin='(User.id == UserEmail.user_id) & UserEmail.is_primary') _secondary_emails = db.relationship( 'UserEmail', lazy=True, cascade='all, delete-orphan', collection_class=set, primaryjoin='(User.id == UserEmail.user_id) & ~UserEmail.is_primary') _all_emails = db.relationship('UserEmail', lazy=True, viewonly=True, primaryjoin='User.id == UserEmail.user_id', collection_class=set, backref=db.backref('user', lazy=False)) #: the affiliation of the user affiliation = association_proxy('_affiliation', 'name', creator=lambda v: UserAffiliation(name=v)) #: the primary email address of the user email = association_proxy( '_primary_email', 'email', creator=lambda v: UserEmail(email=v, is_primary=True)) #: any additional emails the user might have secondary_emails = association_proxy('_secondary_emails', 'email', creator=lambda v: UserEmail(email=v)) #: all emails of the user. read-only; use it only for searching by email! also, do not use it between #: modifying `email` or `secondary_emails` and a session expire/commit! all_emails = association_proxy('_all_emails', 'email') # read-only! #: the user this user has been merged into merged_into_user = db.relationship( 'User', lazy=True, backref=db.backref('merged_from_users', lazy=True), remote_side='User.id', ) #: the users's favorite users favorite_users = db.relationship( 'User', secondary=favorite_user_table, primaryjoin=id == favorite_user_table.c.user_id, secondaryjoin=(id == favorite_user_table.c.target_id) & ~is_deleted, lazy=True, collection_class=set, backref=db.backref('favorite_of', lazy=True, collection_class=set), ) #: the users's favorite categories favorite_categories = db.relationship( 'Category', secondary=favorite_category_table, lazy=True, collection_class=set, backref=db.backref('favorite_of', lazy=True, collection_class=set), ) #: the user's category suggestions suggested_categories = db.relationship( 'SuggestedCategory', lazy='dynamic', order_by='SuggestedCategory.score.desc()', cascade='all, delete-orphan', backref=db.backref('user', lazy=True)) #: the active API key of the user api_key = db.relationship( 'APIKey', lazy=True, uselist=False, cascade='all, delete-orphan', primaryjoin='(User.id == APIKey.user_id) & APIKey.is_active', back_populates='user') #: the previous API keys of the user old_api_keys = db.relationship( 'APIKey', lazy=True, cascade='all, delete-orphan', order_by='APIKey.created_dt.desc()', primaryjoin='(User.id == APIKey.user_id) & ~APIKey.is_active', back_populates='user') #: the identities used by this user identities = db.relationship('Identity', lazy=True, cascade='all, delete-orphan', collection_class=set, backref=db.backref('user', lazy=False)) # relationship backrefs: # - _all_settings (UserSetting.user) # - abstract_comments (AbstractComment.user) # - abstract_email_log_entries (AbstractEmailLogEntry.user) # - abstract_reviewer_for_tracks (Track.abstract_reviewers) # - abstract_reviews (AbstractReview.user) # - abstracts (Abstract.submitter) # - agreements (Agreement.user) # - attachment_files (AttachmentFile.user) # - attachments (Attachment.user) # - blockings (Blocking.created_by_user) # - content_reviewer_for_contributions (Contribution.paper_content_reviewers) # - convener_for_tracks (Track.conveners) # - created_events (Event.creator) # - event_log_entries (EventLogEntry.user) # - event_notes_revisions (EventNoteRevision.user) # - event_persons (EventPerson.user) # - event_reminders (EventReminder.creator) # - event_roles (EventRole.members) # - favorite_of (User.favorite_users) # - 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 indico.modules.users.legacy import AvatarUserWrapper avatar = AvatarUserWrapper(self.id) # avoid garbage collection avatar.user return avatar as_legacy = as_avatar @property def avatar_css(self): from indico.modules.users.util import get_color_for_username return 'background-color: {};'.format( get_color_for_username(self.full_name)) @property def external_identities(self): """The external identities of the user""" return {x for x in self.identities if x.provider != 'indico'} @property def local_identities(self): """The local identities of the user""" return {x for x in self.identities if x.provider == 'indico'} @property def local_identity(self): """The main (most recently used) local identity""" identities = sorted(self.local_identities, key=attrgetter('safe_last_login_dt'), reverse=True) return identities[0] if identities else None @property def secondary_local_identities(self): """The local identities of the user except the main one""" return self.local_identities - {self.local_identity} @locator_property def locator(self): return {'user_id': self.id} @cached_property def settings(self): """Returns the user settings proxy for this user""" from indico.modules.users import user_settings return user_settings.bind(self) @property def synced_fields(self): """The fields of the user whose values are currently synced. This set is always a subset of the synced fields define in synced fields of the idp in 'indico.conf'. """ synced_fields = self.settings.get('synced_fields') # If synced_fields is missing or None, then all fields are synced if synced_fields is None: return multipass.synced_fields else: return set(synced_fields) & multipass.synced_fields @synced_fields.setter def synced_fields(self, value): value = set(value) & multipass.synced_fields if value == multipass.synced_fields: self.settings.delete('synced_fields') else: self.settings.set('synced_fields', list(value)) @property def synced_values(self): """The values from the synced identity for the user. Those values are not the actual user's values and might differ if they are not set as synchronized. """ identity = self._get_synced_identity(refresh=False) if identity is None: return {} return { field: (identity.data.get(field) or '') for field in multipass.synced_fields } def __contains__(self, user): """Convenience method for `user in user_or_group`.""" return self == user @return_ascii def __repr__(self): return format_repr(self, 'id', 'email', is_deleted=False, is_pending=False, _text=self.full_name) def can_be_modified(self, user): """If this user can be modified by the given user""" return self == user or user.is_admin def iter_identifiers(self, check_providers=False, providers=None): """Yields ``(provider, identifier)`` tuples for the user. :param check_providers: If True, providers are searched for additional identifiers once all existing identifiers have been yielded. :param providers: May be a set containing provider names to get only identifiers from the specified providers. """ done = set() for identity in self.identities: if providers is not None and identity.provider not in providers: continue item = (identity.provider, identity.identifier) done.add(item) yield item if not check_providers: return for identity_info in multipass.search_identities( providers=providers, exact=True, email=self.all_emails): item = (identity_info.provider.name, identity_info.identifier) if item not in done: yield item def get_full_name(self, *args, **kwargs): kwargs['_show_empty_names'] = True return super(User, self).get_full_name(*args, **kwargs) def make_email_primary(self, email): """Promotes a secondary email address to the primary email address :param email: an email address that is currently a secondary email """ secondary = next( (x for x in self._secondary_emails if x.email == email), None) if secondary is None: raise ValueError('email is not a secondary email address') self._primary_email.is_primary = False db.session.flush() secondary.is_primary = True db.session.flush() def synchronize_data(self, refresh=False): """Synchronize the fields of the user from the sync identity. This will take only into account :attr:`synced_fields`. :param refresh: bool -- Whether to refresh the synced identity with the sync provider before instead of using the stored data. (Only if the sync provider supports refresh.) """ identity = self._get_synced_identity(refresh=refresh) if identity is None: return for field in self.synced_fields: old_value = getattr(self, field) new_value = identity.data.get(field) or '' if field in ('first_name', 'last_name') and not new_value: continue if old_value == new_value: continue flash( _("Your {field_name} has been synchronised from '{old_value}' to '{new_value}'." ).format(field_name=syncable_fields[field], old_value=old_value, new_value=new_value)) setattr(self, field, new_value) def _get_synced_identity(self, refresh=False): sync_provider = multipass.sync_provider if sync_provider is None: return None identities = sorted( [x for x in self.identities if x.provider == sync_provider.name], key=attrgetter('safe_last_login_dt'), reverse=True) if not identities: return None identity = identities[0] if refresh and identity.multipass_data is not None: try: identity_info = sync_provider.refresh_identity( identity.identifier, identity.multipass_data) except IdentityRetrievalFailed: identity_info = None if identity_info: identity.data = identity_info.data return identity
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 (indico-wide) # is usually reasonably low so we just access the relationships instead of # sending a more specific query which would need to be cached to avoid # repeating it when performing this check on many abstracts. if not user: return False elif check_state and self.public_state not in ( AbstractPublicState.under_review, AbstractPublicState.awaiting): return False elif not self.event.can_manage( user, 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 TimetableEntry(db.Model): __tablename__ = 'timetable_entries' @declared_attr def __table_args__(cls): return ( db.Index('ix_timetable_entries_start_dt_desc', cls.start_dt.desc()), _make_check(TimetableEntryType.SESSION_BLOCK, 'session_block_id'), _make_check(TimetableEntryType.CONTRIBUTION, 'contribution_id'), _make_check(TimetableEntryType.BREAK, 'break_id'), db.CheckConstraint( f'type != {TimetableEntryType.SESSION_BLOCK} OR parent_id IS NULL', 'valid_parent'), { 'schema': 'events' }) id = db.Column(db.Integer, primary_key=True) event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False) parent_id = db.Column( db.Integer, db.ForeignKey('events.timetable_entries.id'), index=True, nullable=True, ) session_block_id = db.Column(db.Integer, db.ForeignKey('events.session_blocks.id'), index=True, unique=True, nullable=True) contribution_id = db.Column(db.Integer, db.ForeignKey('events.contributions.id'), index=True, unique=True, nullable=True) break_id = db.Column(db.Integer, db.ForeignKey('events.breaks.id'), index=True, unique=True, nullable=True) type = db.Column(PyIntEnum(TimetableEntryType), nullable=False) start_dt = db.Column(UTCDateTime, nullable=False) event = db.relationship('Event', lazy=True, backref=db.backref( 'timetable_entries', order_by=lambda: TimetableEntry.start_dt, cascade='all, delete-orphan', lazy='dynamic')) session_block = db.relationship('SessionBlock', lazy=False, backref=db.backref( 'timetable_entry', cascade='all, delete-orphan', uselist=False, lazy=True)) contribution = db.relationship('Contribution', lazy=False, backref=db.backref( 'timetable_entry', cascade='all, delete-orphan', uselist=False, lazy=True)) break_ = db.relationship('Break', cascade='all, delete-orphan', single_parent=True, lazy=False, backref=db.backref('timetable_entry', cascade='all, delete-orphan', uselist=False, lazy=True)) children = db.relationship('TimetableEntry', order_by='TimetableEntry.start_dt', lazy=True, backref=db.backref('parent', remote_side=[id], lazy=True)) # relationship backrefs: # - parent (TimetableEntry.children) @property def object(self): if self.type == TimetableEntryType.SESSION_BLOCK: return self.session_block elif self.type == TimetableEntryType.CONTRIBUTION: return self.contribution elif self.type == TimetableEntryType.BREAK: return self.break_ @object.setter def object(self, value): from indico.modules.events.contributions import Contribution from indico.modules.events.sessions.models.blocks import SessionBlock from indico.modules.events.timetable.models.breaks import Break self.session_block = self.contribution = self.break_ = None if isinstance(value, SessionBlock): self.session_block = value elif isinstance(value, Contribution): self.contribution = value elif isinstance(value, Break): self.break_ = value elif value is not None: raise TypeError(f'Unexpected object: {value}') @hybrid_property def duration(self): return self.object.duration if self.object is not None else None @duration.setter def duration(self, value): self.object.duration = value @duration.expression def duration(cls): from indico.modules.events.contributions import Contribution from indico.modules.events.sessions.models.blocks import SessionBlock from indico.modules.events.timetable.models.breaks import Break return db.case( { TimetableEntryType.SESSION_BLOCK.value: db.select([SessionBlock.duration]).where( SessionBlock.id == cls.session_block_id).correlate_except( SessionBlock).scalar_subquery(), TimetableEntryType.CONTRIBUTION.value: db.select([Contribution.duration]).where( Contribution.id == cls.contribution_id).correlate_except( Contribution).scalar_subquery(), TimetableEntryType.BREAK.value: db.select([Break.duration]).where(Break.id == cls.break_id). correlate_except(Break).scalar_subquery(), }, value=cls.type) @hybrid_property def end_dt(self): if self.start_dt is None or self.duration is None: return None return self.start_dt + self.duration @end_dt.expression def end_dt(cls): return cls.start_dt + cls.duration @property def session_siblings(self): if self.type == TimetableEntryType.SESSION_BLOCK: return [ x for x in self.siblings if x.session_block and x.session_block.session == self.session_block.session ] elif self.parent: return self.siblings else: return [] @property def siblings(self): from indico.modules.events.timetable.util import get_nested_entries, get_top_level_entries tzinfo = self.event.tzinfo day = self.start_dt.astimezone(tzinfo).date() siblings = (get_nested_entries(self.event)[self.parent_id] if self.parent_id else get_top_level_entries(self.event)) return [ x for x in siblings if x.start_dt.astimezone(tzinfo).date() == day and x.id != self.id ] @property def siblings_query(self): tzinfo = self.event.tzinfo day = self.start_dt.astimezone(tzinfo).date() criteria = (TimetableEntry.id != self.id, TimetableEntry.parent == self.parent, db.cast(TimetableEntry.start_dt.astimezone(tzinfo), db.Date) == day) return TimetableEntry.query.with_parent(self.event).filter(*criteria) @locator_property def locator(self): return dict(self.event.locator, entry_id=self.id) def __repr__(self): return format_repr(self, 'id', 'type', 'start_dt', 'end_dt', _repr=self.object) def can_view(self, user): """Check whether the user will see this entry in the timetable.""" if self.type in (TimetableEntryType.CONTRIBUTION, TimetableEntryType.BREAK): return self.object.can_access(user) elif self.type == TimetableEntryType.SESSION_BLOCK: if self.object.can_access(user): return True return any(x.can_access(user) for x in self.object.contributions) def extend_start_dt(self, start_dt): assert start_dt < self.start_dt extension = self.start_dt - start_dt self.start_dt = start_dt self.duration = self.duration + extension def extend_end_dt(self, end_dt): diff = end_dt - self.end_dt if diff < timedelta(0): raise ValueError('New end_dt is before current end_dt.') self.duration += diff def extend_parent(self, by_start=True, by_end=True): """Extend start/end of parent objects if needed. No extension if performed for entries crossing a day boundary in the event timezone. :param by_start: Extend parent by start datetime. :param by_end: Extend parent by end datetime. """ tzinfo = self.event.tzinfo if self.start_dt.astimezone(tzinfo).date() != self.end_dt.astimezone( tzinfo).date(): return if self.parent is None: if by_start and self.start_dt < self.event.start_dt: self.event.start_dt = self.start_dt if by_end and self.end_dt > self.event.end_dt: self.event.end_dt = self.end_dt else: extended = False if by_start and self.start_dt < self.parent.start_dt: self.parent.extend_start_dt(self.start_dt) extended = True if by_end and self.end_dt > self.parent.end_dt: self.parent.extend_end_dt(self.end_dt) extended = True if extended: self.parent.extend_parent(by_start=by_start, by_end=by_end) def is_parallel(self, in_session=False): siblings = self.siblings if not in_session else self.session_siblings for sibling in siblings: if overlaps((self.start_dt, self.end_dt), (sibling.start_dt, sibling.end_dt)): return True return False def move(self, start_dt): """Move the entry to start at a different time. This method automatically moves children of the entry to preserve their start time relative to the parent's start time. """ if self.type == TimetableEntryType.SESSION_BLOCK: diff = start_dt - self.start_dt for child in self.children: child.start_dt += diff self.start_dt = start_dt def move_next_to(self, sibling, position='before'): if sibling not in self.siblings: raise ValueError('Not a sibling') if position not in ('before', 'after'): raise ValueError('Invalid position') if position == 'before': start_dt = sibling.start_dt - self.duration else: start_dt = sibling.end_dt self.move(start_dt)
class RegistrationInvitation(db.Model): """An invitation for someone to register.""" __tablename__ = 'invitations' __table_args__ = (db.CheckConstraint("(state = {state}) OR (registration_id IS NULL)" .format(state=InvitationState.accepted), name='registration_state'), db.UniqueConstraint('registration_form_id', 'email'), {'schema': 'event_registration'}) #: The ID of the invitation id = db.Column( db.Integer, primary_key=True ) #: The UUID of the invitation uuid = db.Column( UUID, index=True, unique=True, nullable=False, default=lambda: str(uuid4()) ) #: The ID of the registration form registration_form_id = db.Column( db.Integer, db.ForeignKey('event_registration.forms.id'), index=True, nullable=False ) #: The ID of the registration (if accepted) registration_id = db.Column( db.Integer, db.ForeignKey('event_registration.registrations.id'), index=True, unique=True, nullable=True ) #: The state of the invitation state = db.Column( PyIntEnum(InvitationState), nullable=False, default=InvitationState.pending ) #: Whether registration moderation should be skipped skip_moderation = db.Column( db.Boolean, nullable=False, default=False ) #: The email of the invited person email = db.Column( db.String, nullable=False ) #: The first name of the invited person first_name = db.Column( db.String, nullable=False ) #: The last name of the invited person last_name = db.Column( db.String, nullable=False ) #: The affiliation of the invited person affiliation = db.Column( db.String, nullable=False ) #: The associated registration registration = db.relationship( 'Registration', lazy=True, backref=db.backref( 'invitation', lazy=True, uselist=False ) ) # relationship backrefs: # - registration_form (RegistrationForm.invitations) @locator_property def locator(self): return dict(self.registration_form.locator, invitation_id=self.id) @locator.uuid def locator(self): """A locator suitable for 'display' pages. Instead of the numeric ID it uses the UUID. """ assert self.uuid is not None return dict(self.registration_form.locator, invitation=self.uuid) def __repr__(self): full_name = f'{self.first_name} {self.last_name}' return format_repr(self, 'id', 'registration_form_id', 'email', 'state', _text=full_name)
class Blocking(db.Model): __tablename__ = 'blockings' __table_args__ = {'schema': 'roombooking'} id = db.Column(db.Integer, primary_key=True) created_by_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=False) created_dt = db.Column(UTCDateTime, nullable=False, default=now_utc) start_date = db.Column(db.Date, nullable=False, index=True) end_date = db.Column(db.Date, nullable=False, index=True) reason = db.Column(db.Text, nullable=False) _allowed = db.relationship('BlockingPrincipal', backref='blocking', cascade='all, delete-orphan', collection_class=set) allowed = association_proxy( '_allowed', 'principal', creator=lambda v: BlockingPrincipal(principal=v)) blocked_rooms = db.relationship('BlockedRoom', backref='blocking', cascade='all, delete-orphan') #: The user who created this blocking. created_by_user = db.relationship('User', lazy=False, backref=db.backref('blockings', lazy='dynamic')) @hybrid_method def is_active_at(self, d): return self.start_date <= d <= self.end_date @is_active_at.expression def is_active_at(self, d): return (self.start_date <= d) & (d <= self.end_date) def can_edit(self, user, allow_admin=True): if not user: return False return user == self.created_by_user or (allow_admin and rb_is_admin(user)) def can_delete(self, user, allow_admin=True): if not user: return False return user == self.created_by_user or (allow_admin and rb_is_admin(user)) def can_override(self, user, room=None, explicit_only=False, allow_admin=True): """Check if a user can override the blocking. The following persons are authorized to override a blocking: - the creator of the blocking - anyone on the blocking's ACL - unless explicit_only is set: rb admins and room managers (if a room is given) """ if not user: return False if self.created_by_user == user: return True if not explicit_only: if allow_admin and rb_is_admin(user): return True if room and room.can_manage(user): return True return any(user in principal for principal in iter_acl(self.allowed)) @property def external_details_url(self): return url_for('rb.blocking_link', blocking_id=self.id, _external=True) @return_ascii def __repr__(self): return format_repr(self, 'id', 'start_date', 'end_date', _text=self.reason)
class AbstractEmailTemplate(db.Model): """Represents an email template for abstracts notifications.""" __tablename__ = 'email_templates' __table_args__ = {'schema': 'event_abstracts'} id = db.Column( db.Integer, primary_key=True ) title = db.Column( db.String, nullable=False ) event_id = db.Column( db.Integer, db.ForeignKey('events.events.id'), index=True, nullable=False ) #: The relative position of the template in the list of templates position = db.Column( db.Integer, nullable=False, default=_get_next_position ) #: The address to use as Reply-To in the email reply_to_address = db.Column( db.String, nullable=False ) #: The subject of the email subject = db.Column( db.String, nullable=False ) #: The body of the template body = db.Column( db.Text, nullable=False, default='' ) #: List of extra email addresses to be added as CC in the email extra_cc_emails = db.Column( ARRAY(db.String), nullable=False, default=[], ) #: Whether to include the submitter's email address as To for emails include_submitter = db.Column( db.Boolean, nullable=False, default=False ) #: Whether to include authors' email addresses as To for emails include_authors = db.Column( db.Boolean, nullable=False, default=False ) #: Whether to include co-authors' email addresses as CC for emails include_coauthors = db.Column( db.Boolean, nullable=False, default=False ) #: Whether to stop checking the rest of the conditions when a match is found stop_on_match = db.Column( db.Boolean, nullable=False, default=True ) #: Conditions need to be met to send the email rules = db.Column( JSON, nullable=False ) event_new = db.relationship( 'Event', lazy=True, backref=db.backref( 'abstract_email_templates', lazy=True ) ) # relationship backrefs: # - logs (AbstractEmailLogEntry.email_template) @locator_property def locator(self): return dict(self.event_new.locator, email_tpl_id=self.id) @return_ascii def __repr__(self): return format_repr(self, 'id', 'event_id', _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 @property def spotlight_file(self): return self.get_spotlight_file() @property def timeline(self): return self.get_timeline() @paper.setter def paper(self, paper): self._contribution = paper.contribution def get_timeline(self, user=None): comments = [x for x in self.comments if x.can_view(user)] if user else self.comments reviews = [x for x in self.reviews if x.can_view(user)] if user else self.reviews judgment = [ PaperJudgmentProxy(self) ] if self.state == PaperRevisionState.to_be_corrected else [] return sorted(chain(comments, reviews, judgment), key=attrgetter('created_dt')) def get_reviews(self, group=None, user=None): reviews = [] if user and group: reviews = [ x for x in self.reviews if x.group.instance == group and x.user == user ] elif user: reviews = [x for x in self.reviews if x.user == user] elif group: reviews = [x for x in self.reviews if x.group.instance == group] return reviews def get_reviewed_for_groups(self, user, include_reviewed=False): from indico.modules.events.papers.models.reviews import PaperTypeProxy reviewed_for = {x.type for x in self.reviews if x.user == user} if include_reviewed else set() if self.paper.cfp.content_reviewing_enabled and user in self.paper.cfp.content_reviewers: reviewed_for.add(PaperReviewType.content) if self.paper.cfp.layout_reviewing_enabled and user in self.paper.cfp.layout_reviewers: reviewed_for.add(PaperReviewType.layout) return set(map(PaperTypeProxy, reviewed_for)) def has_user_reviewed(self, user, review_type=None): from indico.modules.events.papers.models.reviews import PaperReviewType if review_type: if isinstance(review_type, basestring): review_type = PaperReviewType[review_type] return any(review.user == user and review.type == review_type for review in self.reviews) else: layout_review = next( (review for review in self.reviews if review.user == user and review.type == PaperReviewType.layout), None) content_review = next( (review for review in self.reviews if review.user == user and review.type == PaperReviewType.content), None) if user in self._contribution.paper_layout_reviewers and user in self._contribution.paper_content_reviewers: return bool(layout_review and content_review) elif user in self._contribution.paper_layout_reviewers: return bool(layout_review) elif user in self._contribution.paper_content_reviewers: return bool(content_review) def get_spotlight_file(self): pdf_files = [ paper_file for paper_file in self.files if paper_file.content_type == 'application/pdf' ] return pdf_files[0] if len(pdf_files) == 1 else None