def __auto_table_args(cls): checks = [db.CheckConstraint('read_access OR full_access OR array_length(roles, 1) IS NOT NULL', 'has_privs')] if cls.allow_networks: # you can match a network acl entry without being logged in. # we never want that for anything but simple read access checks.append(db.CheckConstraint('type != {} OR (NOT full_access AND array_length(roles, 1) IS NULL)' .format(PrincipalType.network), 'networks_read_only')) return tuple(checks)
class Setting(SettingsBase, db.Model): __table_args__ = (db.Index(None, 'module', 'name'), db.UniqueConstraint('module', 'name'), db.CheckConstraint('module = lower(module)', 'lowercase_module'), db.CheckConstraint('name = lower(name)', 'lowercase_name'), {'schema': 'indico'}) @return_ascii def __repr__(self): return '<Setting({}, {}, {!r})>'.format(self.module, self.name, self.value)
def __auto_table_args(cls): return ( db.Index(None, 'category_chain', postgresql_using='gin'), db.Index('ix_events_title_fts', db.func.to_tsvector('simple', cls.title), postgresql_using='gin'), db.Index('ix_events_start_dt_desc', cls.start_dt.desc()), db.Index('ix_events_end_dt_desc', cls.end_dt.desc()), db.CheckConstraint( "(category_id IS NOT NULL AND category_chain IS NOT NULL) OR is_deleted", 'category_data_set'), db.CheckConstraint("category_id = category_chain[1]", 'category_id_matches_chain'), db.CheckConstraint( "category_chain[array_length(category_chain, 1)] = 0", 'category_chain_has_root'), db.CheckConstraint( "(logo IS NULL) = (logo_metadata::text = 'null')", 'valid_logo'), db.CheckConstraint( "(stylesheet IS NULL) = (stylesheet_metadata::text = 'null')", 'valid_stylesheet'), db.CheckConstraint("end_dt >= start_dt", 'valid_dates'), db.CheckConstraint("title != ''", 'valid_title'), db.CheckConstraint("cloned_from_id != id", 'not_cloned_from_self'), { 'schema': 'events' })
def __auto_table_args(cls): uniques = () if cls.unique_columns: uniques = [db.Index('ix_uq_{}_user'.format(cls.__tablename__), 'user_id', *cls.unique_columns, unique=True, postgresql_where=db.text('type = {}'.format(PrincipalType.user))), db.Index('ix_uq_{}_local_group'.format(cls.__tablename__), 'local_group_id', *cls.unique_columns, unique=True, postgresql_where=db.text('type = {}'.format(PrincipalType.local_group))), db.Index('ix_uq_{}_mp_group'.format(cls.__tablename__), 'mp_group_provider', 'mp_group_name', *cls.unique_columns, unique=True, postgresql_where=db.text('type = {}'.format(PrincipalType.multipass_group)))] if cls.allow_emails: uniques.append(db.Index('ix_uq_{}_email'.format(cls.__tablename__), 'email', *cls.unique_columns, unique=True, postgresql_where=db.text('type = {}'.format(PrincipalType.email)))) indexes = [db.Index(None, 'mp_group_provider', 'mp_group_name')] checks = [_make_check(PrincipalType.user, cls.allow_emails, cls.allow_networks, cls.allow_event_roles, 'user_id'), _make_check(PrincipalType.local_group, cls.allow_emails, cls.allow_networks, cls.allow_event_roles, 'local_group_id'), _make_check(PrincipalType.multipass_group, cls.allow_emails, cls.allow_networks, cls.allow_event_roles, 'mp_group_provider', 'mp_group_name')] if cls.allow_emails: checks.append(_make_check(PrincipalType.email, cls.allow_emails, cls.allow_networks, cls.allow_event_roles, 'email')) checks.append(db.CheckConstraint('email IS NULL OR email = lower(email)', 'lowercase_email')) if cls.allow_networks: checks.append(_make_check(PrincipalType.network, cls.allow_emails, cls.allow_networks, cls.allow_event_roles, 'ip_network_group_id')) if cls.allow_event_roles: checks.append(_make_check(PrincipalType.event_role, cls.allow_emails, cls.allow_networks, cls.allow_event_roles, 'event_role_id')) return tuple(uniques + indexes + checks)
def __auto_table_args(cls): checks = [db.CheckConstraint('read_access OR full_access OR array_length(permissions, 1) IS NOT NULL', 'has_privs')] if cls.allow_networks: # you can match a network acl entry without being logged in. # we never want that for anything but simple read access checks.append(db.CheckConstraint('type != {} OR (NOT full_access AND array_length(permissions, 1) IS NULL)' .format(PrincipalType.network), 'networks_read_only')) if cls.allow_registration_forms: # many events allow everyone to register, letting people give themselves # management access by registering would be bad so we only allow read access checks.append(db.CheckConstraint('type != {} OR (NOT full_access AND array_length(permissions, 1) IS NULL)' .format(PrincipalType.registration_form), 'registration_form_read_only')) return tuple(checks)
def __auto_table_args(cls): return ( db.Index('ix_events_start_dt_desc', cls.start_dt.desc()), db.Index('ix_events_end_dt_desc', cls.end_dt.desc()), db.Index('ix_events_not_deleted_category', cls.is_deleted, cls.category_id), db.Index('ix_events_not_deleted_category_dates', cls.is_deleted, cls.category_id, cls.start_dt, cls.end_dt), db.Index('ix_uq_events_url_shortcut', db.func.lower(cls.url_shortcut), unique=True, postgresql_where=db.text('NOT is_deleted')), db.CheckConstraint("category_id IS NOT NULL OR is_deleted", 'category_data_set'), db.CheckConstraint( "(logo IS NULL) = (logo_metadata::text = 'null')", 'valid_logo'), db.CheckConstraint( "(stylesheet IS NULL) = (stylesheet_metadata::text = 'null')", 'valid_stylesheet'), db.CheckConstraint("end_dt >= start_dt", 'valid_dates'), db.CheckConstraint("url_shortcut != ''", 'url_shortcut_not_empty'), db.CheckConstraint("cloned_from_id != id", 'not_cloned_from_self'), db.CheckConstraint('visibility IS NULL OR visibility >= 0', 'valid_visibility'), { 'schema': 'events' })
def _make_checks(): available_columns = set(_column_for_types.viewvalues()) for link_type in EntryType: required_col = _column_for_types[link_type] forbidden_cols = available_columns - {required_col} criteria = ['{} IS NULL'.format(col) for col in sorted(forbidden_cols)] criteria += ['{} IS NOT NULL'.format(required_col)] condition = 'type != {} OR ({})'.format(link_type, ' AND '.join(criteria)) yield db.CheckConstraint(condition, 'valid_{}_entry'.format(link_type.name))
def _make_check(type_, allow_emails, allow_networks, *cols): all_cols = {'user_id', 'local_group_id', 'mp_group_provider', 'mp_group_name'} if allow_emails: all_cols.add('email') if allow_networks: all_cols.add('ip_network_group_id') required_cols = all_cols & set(cols) forbidden_cols = all_cols - required_cols criteria = ['{} IS NULL'.format(col) for col in sorted(forbidden_cols)] criteria += ['{} IS NOT NULL'.format(col) for col in sorted(required_cols)] condition = 'type != {} OR ({})'.format(type_, ' AND '.join(criteria)) return db.CheckConstraint(condition, 'valid_{}'.format(type_.name))
class EventSetting(SettingsBase, db.Model): __table_args__ = (db.Index(None, 'event_id', 'module', 'name'), db.Index(None, 'event_id', 'module'), db.UniqueConstraint('event_id', 'module', 'name'), db.CheckConstraint('module = lower(module)', 'lowercase_module'), db.CheckConstraint('name = lower(name)', 'lowercase_name'), {'schema': 'events'}) event_id = db.Column( db.String, index=True, nullable=False ) @return_ascii def __repr__(self): return '<EventSetting({}, {}, {}, {!r})>'.format(self.event_id, self.module, self.name, self.value) @classmethod def delete_event(cls, event_id): cls.find(event_id=event_id).delete(synchronize_session='fetch')
def _make_check(type_, allow_emails, allow_networks, allow_event_roles, allow_category_roles, allow_registration_forms, *cols): all_cols = {'user_id', 'local_group_id', 'mp_group_provider', 'mp_group_name'} if allow_emails: all_cols.add('email') if allow_networks: all_cols.add('ip_network_group_id') if allow_event_roles: all_cols.add('event_role_id') if allow_category_roles: all_cols.add('category_role_id') if allow_registration_forms: all_cols.add('registration_form_id') required_cols = all_cols & set(cols) forbidden_cols = all_cols - required_cols criteria = [f'{col} IS NULL' for col in sorted(forbidden_cols)] criteria += [f'{col} IS NOT NULL' for col in sorted(required_cols)] condition = 'type != {} OR ({})'.format(type_, ' AND '.join(criteria)) return db.CheckConstraint(condition, f'valid_{type_.name}')
def __auto_table_args(cls): return (db.Index('ix_events_start_dt_desc', cls.start_dt.desc()), db.Index('ix_events_end_dt_desc', cls.end_dt.desc()), db.Index('ix_events_not_deleted_category', cls.is_deleted, cls.category_id), db.Index('ix_events_not_deleted_category_dates', cls.is_deleted, cls.category_id, cls.start_dt, cls.end_dt), db.CheckConstraint("category_id IS NOT NULL OR is_deleted", 'category_data_set'), db.CheckConstraint("(logo IS NULL) = (logo_metadata::text = 'null')", 'valid_logo'), db.CheckConstraint("(stylesheet IS NULL) = (stylesheet_metadata::text = 'null')", 'valid_stylesheet'), db.CheckConstraint("end_dt >= start_dt", 'valid_dates'), db.CheckConstraint("cloned_from_id != id", 'not_cloned_from_self'), db.CheckConstraint('visibility IS NULL OR visibility >= 0', 'valid_visibility'), {'schema': 'events'})
class Setting(db.Model): __tablename__ = 'settings' __table_args__ = (db.Index('ix_settings_module_key', 'module', 'name'), db.UniqueConstraint('module', 'name'), db.CheckConstraint('module = lower(module)'), db.CheckConstraint('name = lower(name)'), { 'schema': 'indico' }) id = db.Column(db.Integer, primary_key=True) module = db.Column(db.String, index=True) name = db.Column(db.String, index=True) value = db.Column(JSON) @return_ascii def __repr__(self): return u'<Setting({}, {}, {!r})>'.format(self.module, self.name, self.value) @classmethod def get_setting(cls, module, name): return cls.find_first(module=module, name=name) @classmethod def get_all_settings(cls, module): return {s.name: s for s in cls.find(module=module)} @classmethod def get_all(cls, module): return {s.name: s.value for s in cls.find(module=module)} @classmethod def get(cls, module, name, default=None): setting = cls.get_setting(module, name) if setting is None: return default return setting.value @classmethod def set(cls, module, name, value): setting = cls.get_setting(module, name) if setting is None: setting = cls(module=module, name=name) db.session.add(setting) setting.value = value @classmethod def set_multi(cls, module, items): existing = cls.get_all_settings(module) for name in items.viewkeys() - existing.viewkeys(): setting = cls(module=module, name=name, value=items[name]) db.session.add(setting) for name in items.viewkeys() & existing.viewkeys(): existing[name].value = items[name] @classmethod def delete(cls, module, *names): if not names: return Setting.find( Setting.name.in_(names), Setting.module == module).delete(synchronize_session='fetch') @classmethod def delete_all(cls, module): Setting.find(module=module).delete()
class EventPerson(PersonMixin, db.Model): """A person inside an event, e.g. a speaker/author etc.""" __tablename__ = 'persons' __table_args__ = (db.UniqueConstraint('event_id', 'user_id'), db.CheckConstraint('email = lower(email)', 'lowercase_email'), db.Index(None, 'event_id', 'email', unique=True, postgresql_where=db.text("email != ''")), { 'schema': 'events' }) id = db.Column(db.Integer, primary_key=True) event_id = db.Column(db.Integer, db.ForeignKey('events.events.id'), nullable=False, index=True) user_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), nullable=True, index=True) first_name = db.Column(db.String, nullable=False, default='') last_name = db.Column(db.String, nullable=False) email = db.Column(db.String, nullable=False, index=True, default='') # the title of the user - you usually want the `title` property! _title = db.Column('title', PyIntEnum(UserTitle), nullable=False, default=UserTitle.none) affiliation = db.Column(db.String, nullable=False, default='') address = db.Column(db.Text, nullable=False, default='') phone = db.Column(db.String, nullable=False, default='') invited_dt = db.Column(UTCDateTime, nullable=True) is_untrusted = db.Column(db.Boolean, nullable=False, default=False) event = db.relationship('Event', lazy=True, backref=db.backref('persons', cascade='all, delete-orphan', cascade_backrefs=False, lazy='dynamic')) user = db.relationship('User', lazy=True, backref=db.backref('event_persons', cascade_backrefs=False, lazy='dynamic')) # relationship backrefs: # - abstract_links (AbstractPersonLink.person) # - contribution_links (ContributionPersonLink.person) # - event_links (EventPersonLink.person) # - session_block_links (SessionBlockPersonLink.person) # - subcontribution_links (SubContributionPersonLink.person) @locator_property def locator(self): return dict(self.event.locator, person_id=self.id) @return_ascii def __repr__(self): return format_repr(self, 'id', is_untrusted=False, _text=self.full_name) @property def principal(self): if self.user is not None: return self.user elif self.email: return EmailPrincipal(self.email) return None @classmethod def create_from_user(cls, user, event=None, is_untrusted=False): return EventPerson(user=user, event=event, first_name=user.first_name, last_name=user.last_name, email=user.email, affiliation=user.affiliation, address=user.address, phone=user.phone, is_untrusted=is_untrusted) @classmethod def for_user(cls, user, event=None, is_untrusted=False): """Return EventPerson for a matching User in Event creating if needed""" person = event.persons.filter_by(user=user).first() if event else None return person or cls.create_from_user( user, event, is_untrusted=is_untrusted) @classmethod def merge_users(cls, target, source): """Merge the EventPersons of two users. :param target: The target user of the merge :param source: The user that is being merged into `target` """ existing_persons = {ep.event_id: ep for ep in target.event_persons} for event_person in source.event_persons: existing = existing_persons.get(event_person.event_id) if existing is None: event_person.user = target else: existing.merge_person_info(event_person) db.session.delete(event_person) db.session.flush() @classmethod def link_user_by_email(cls, user): """ Links all email-based persons matching the user's email addresses with the user. :param user: A User object. """ from indico.modules.events.models.events import Event query = (cls.query.join(EventPerson.event).filter( ~Event.is_deleted, cls.email.in_(user.all_emails), cls.user_id.is_(None))) for event_person in query: existing = (cls.query.filter_by( user_id=user.id, event_id=event_person.event_id).one_or_none()) if existing is None: event_person.user = user else: existing.merge_person_info(event_person) db.session.delete(event_person) db.session.flush() @no_autoflush def merge_person_info(self, other): from indico.modules.events.contributions.models.persons import AuthorType for column_name in { '_title', 'affiliation', 'address', 'phone', 'first_name', 'last_name' }: value = getattr(self, column_name) or getattr(other, column_name) setattr(self, column_name, value) for event_link in other.event_links: existing_event_link = next( (link for link in self.event_links if link.event_id == event_link.event_id), None) if existing_event_link is None: event_link.person = self else: other.event_links.remove(event_link) for abstract_link in other.abstract_links: existing_abstract_link = next( (link for link in self.abstract_links if link.abstract_id == abstract_link.abstract_id), None) if existing_abstract_link is None: abstract_link.person = self else: existing_abstract_link.is_speaker |= abstract_link.is_speaker existing_abstract_link.author_type = AuthorType.get_highest( existing_abstract_link.author_type, abstract_link.author_type) other.abstract_links.remove(abstract_link) for contribution_link in other.contribution_links: existing_contribution_link = next( (link for link in self.contribution_links if link.contribution_id == contribution_link.contribution_id), None) if existing_contribution_link is None: contribution_link.person = self else: existing_contribution_link.is_speaker |= contribution_link.is_speaker existing_contribution_link.author_type = AuthorType.get_highest( existing_contribution_link.author_type, contribution_link.author_type) other.contribution_links.remove(contribution_link) for subcontribution_link in other.subcontribution_links: existing_subcontribution_link = next( (link for link in self.subcontribution_links if link.subcontribution_id == subcontribution_link.subcontribution_id), None) if existing_subcontribution_link is None: subcontribution_link.person = self else: other.subcontribution_links.remove(subcontribution_link) for session_block_link in other.session_block_links: existing_session_block_link = next( (link for link in self.session_block_links if link.session_block_id == session_block_link.session_block_id), None) if existing_session_block_link is None: session_block_link.person = self else: other.session_block_links.remove(session_block_link) db.session.flush() def has_role(self, role, obj): """Whether the person has a role in the ACL list of a given object""" principals = [ x for x in obj.acl_entries if x.has_management_permission(role, explicit=True) ] return any( x for x in principals if ((self.user_id is not None and self.user_id == x.user_id) or ( self.email is not None and self.email == x.email)))
class AbstractReview(ProposalReviewMixin, RenderModeMixin, db.Model): """An abstract review, emitted by a reviewer.""" possible_render_modes = {RenderMode.markdown} default_render_mode = RenderMode.markdown revision_attr = 'abstract' group_attr = 'track' marshmallow_aliases = {'_comment': 'comment'} __tablename__ = 'abstract_reviews' __table_args__ = ( db.UniqueConstraint('abstract_id', 'user_id', 'track_id'), db.CheckConstraint( "proposed_action = {} OR (proposed_contribution_type_id IS NULL)". format(AbstractAction.accept), name='prop_contrib_id_only_accepted'), db.CheckConstraint( "(proposed_action IN ({}, {})) = (proposed_related_abstract_id IS NOT NULL)" .format(AbstractAction.mark_as_duplicate, AbstractAction.merge), name='prop_abstract_id_only_duplicate_merge'), { 'schema': 'event_abstracts' }) id = db.Column(db.Integer, primary_key=True) abstract_id = db.Column(db.Integer, db.ForeignKey('event_abstracts.abstracts.id'), index=True, nullable=False) user_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=False) track_id = db.Column(db.Integer, db.ForeignKey('events.tracks.id'), index=True, nullable=True) created_dt = db.Column( UTCDateTime, nullable=False, default=now_utc, ) modified_dt = db.Column(UTCDateTime, nullable=True) _comment = db.Column('comment', db.Text, nullable=False, default='') proposed_action = db.Column(PyIntEnum(AbstractAction), nullable=False) proposed_related_abstract_id = db.Column( db.Integer, db.ForeignKey('event_abstracts.abstracts.id'), index=True, nullable=True) proposed_contribution_type_id = db.Column( db.Integer, db.ForeignKey('events.contribution_types.id'), nullable=True, index=True) abstract = db.relationship('Abstract', lazy=True, foreign_keys=abstract_id, backref=db.backref('reviews', cascade='all, delete-orphan', lazy=True)) user = db.relationship('User', lazy=True, backref=db.backref('abstract_reviews', lazy='dynamic')) track = db.relationship('Track', lazy=True, foreign_keys=track_id, backref=db.backref('abstract_reviews', lazy='dynamic')) proposed_related_abstract = db.relationship( 'Abstract', lazy=True, foreign_keys=proposed_related_abstract_id, backref=db.backref('proposed_related_abstract_reviews', lazy='dynamic')) proposed_tracks = db.relationship( 'Track', secondary='event_abstracts.proposed_for_tracks', lazy=True, collection_class=set, backref=db.backref('proposed_abstract_reviews', lazy='dynamic', passive_deletes=True)) proposed_contribution_type = db.relationship('ContributionType', lazy=True, backref=db.backref( 'abstract_reviews', lazy='dynamic')) # relationship backrefs: # - ratings (AbstractReviewRating.review) comment = RenderModeMixin.create_hybrid_property('_comment') @locator_property def locator(self): return dict(self.abstract.locator, review_id=self.id) @return_ascii def __repr__(self): return format_repr(self, 'id', 'abstract_id', 'user_id', proposed_action=None) @property def visibility(self): return AbstractCommentVisibility.reviewers @property def score(self): ratings = [ r for r in self.ratings if not r.question.no_score and not r.question.is_deleted and r.value is not None ] if not ratings: return None return sum(x.value for x in ratings) / len(ratings) def can_edit(self, user, check_state=False): if user is None: return False if check_state and self.abstract.public_state.name != 'under_review': return False return self.user == user def can_view(self, user): if user is None: return False elif user == self.user: return True if self.abstract.can_judge(user): return True else: return self.track.can_convene(user)
class Room(ProtectionManagersMixin, db.Model, Serializer): __tablename__ = 'rooms' __table_args__ = ( db.UniqueConstraint( 'id', 'location_id'), # useless but needed for the LocationMixin fkey db.CheckConstraint("verbose_name != ''", 'verbose_name_not_empty'), { 'schema': 'roombooking' }) default_protection_mode = ProtectionMode.public disallowed_protection_modes = frozenset({ProtectionMode.inheriting}) __api_public__ = ('id', 'building', 'name', 'floor', 'longitude', 'latitude', ('number', 'roomNr'), ('location_name', 'location'), ('full_name', 'fullName')) __api_minimal_public__ = ('id', ('full_name', 'fullName')) id = db.Column(db.Integer, primary_key=True) location_id = db.Column(db.Integer, db.ForeignKey('roombooking.locations.id'), nullable=False) photo_id = db.Column(db.Integer, db.ForeignKey('roombooking.photos.id')) #: Verbose name for the room (long) verbose_name = db.Column(db.String, nullable=True, default=None) site = db.Column(db.String, default='') division = db.Column(db.String) building = db.Column(db.String, nullable=False) floor = db.Column(db.String, default='', nullable=False) number = db.Column(db.String, default='', nullable=False) notification_emails = db.Column(ARRAY(db.String), nullable=False, default=[]) notification_before_days = db.Column(db.Integer) notification_before_days_weekly = db.Column(db.Integer) notification_before_days_monthly = db.Column(db.Integer) end_notification_daily = db.Column(db.Integer, nullable=True) end_notification_weekly = db.Column(db.Integer, nullable=True) end_notification_monthly = db.Column(db.Integer, nullable=True) reservations_need_confirmation = db.Column(db.Boolean, nullable=False, default=False) notifications_enabled = db.Column(db.Boolean, nullable=False, default=True) end_notifications_enabled = db.Column(db.Boolean, nullable=False, default=True) telephone = db.Column(db.String, nullable=False, default='') key_location = db.Column(db.String, nullable=False, default='') capacity = db.Column(db.Integer, default=20) surface_area = db.Column(db.Integer) longitude = db.Column(db.Float) latitude = db.Column(db.Float) comments = db.Column(db.String, nullable=False, default='') owner_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=False) is_deleted = db.Column( db.Boolean, nullable=False, default=False, ) is_reservable = db.Column(db.Boolean, nullable=False, default=True) max_advance_days = db.Column(db.Integer) booking_limit_days = db.Column(db.Integer) location = db.relationship('Location', back_populates='rooms', lazy=True) acl_entries = db.relationship('RoomPrincipal', lazy=True, backref='room', cascade='all, delete-orphan', collection_class=set) attributes = db.relationship('RoomAttributeAssociation', backref='room', cascade='all, delete-orphan', lazy='dynamic') blocked_rooms = db.relationship('BlockedRoom', backref='room', cascade='all, delete-orphan', lazy='dynamic') bookable_hours = db.relationship('BookableHours', backref='room', order_by=BookableHours.start_time, cascade='all, delete-orphan', lazy='dynamic') available_equipment = db.relationship('EquipmentType', secondary=RoomEquipmentAssociation, backref='rooms', lazy=True) nonbookable_periods = db.relationship( 'NonBookablePeriod', backref='room', order_by=NonBookablePeriod.end_dt.desc(), cascade='all, delete-orphan', lazy='dynamic') photo = db.relationship('Photo', backref='room', cascade='all, delete-orphan', single_parent=True, lazy=True) reservations = db.relationship('Reservation', backref='room', cascade='all, delete-orphan', lazy='dynamic') favorite_of = db.relationship( 'User', secondary=favorite_room_table, lazy=True, collection_class=set, backref=db.backref('favorite_rooms', lazy=True, collection_class=set), ) #: The owner of the room. This is purely informational and does not grant #: any permissions on the room. owner = db.relationship( 'User', # subquery load since a normal joinedload breaks `get_with_data` lazy='subquery', backref=db.backref('owned_rooms', lazy='dynamic')) # relationship backrefs: # - breaks (Break.own_room) # - contributions (Contribution.own_room) # - events (Event.own_room) # - session_blocks (SessionBlock.own_room) # - sessions (Session.own_room) @hybrid_property def is_auto_confirm(self): return not self.reservations_need_confirmation @is_auto_confirm.expression def is_auto_confirm(self): return ~self.reservations_need_confirmation @property def details_url(self): if self.id is None: return None return url_for('rb.room_link', room_id=self.id) @property def map_url(self): if not self.location.map_url_template: return None return self.location.map_url_template.format( id=self.id, building=self.building, floor=self.floor, number=self.number, lat=self.latitude, lng=self.longitude, ) @property def has_photo(self): return self.photo_id is not None @hybrid_property def name(self): return self.generate_name() @name.expression def name(cls): q = (db.session.query(db.m.Location.room_name_format).filter( db.m.Location.id == cls.location_id).correlate(Room).as_scalar()) return db.func.format(q, cls.building, cls.floor, cls.number) @hybrid_property def full_name(self): if self.verbose_name: return u'{} - {}'.format(self.generate_name(), self.verbose_name) else: return u'{}'.format(self.generate_name()) @full_name.expression def full_name(cls): return db.case([[ cls.verbose_name.isnot(None), cls.name + ' - ' + cls.verbose_name ]], else_=cls.name) @property def location_name(self): return self.location.name @property def sprite_position(self): sprite_mapping = _cache.get('rooms-sprite-mapping') return sprite_mapping.get( self.id, 0) if sprite_mapping else 0 # placeholder at position 0 @return_ascii def __repr__(self): return format_repr(self, 'id', 'full_name', is_deleted=False) def has_equipment(self, *names): available = {x.name for x in self.available_equipment} return bool(available & set(names)) def get_attribute_by_name(self, attribute_name): return (self.attributes.join(RoomAttribute).filter( RoomAttribute.name == attribute_name).first()) def has_attribute(self, attribute_name): return self.get_attribute_by_name(attribute_name) is not None def get_attribute_value(self, name, default=None): attr = self.get_attribute_by_name(name) return attr.value if attr else default def set_attribute_value(self, name, value): attr = self.get_attribute_by_name(name) if attr: if value: attr.value = value else: self.attributes.filter(RoomAttributeAssociation.attribute_id == attr.attribute_id) \ .delete(synchronize_session='fetch') elif value: attr = RoomAttribute.query.filter_by(name=name).first() if not attr: raise ValueError("Attribute {} does not exist".format(name)) attr_assoc = RoomAttributeAssociation() attr_assoc.value = value attr_assoc.attribute = attr self.attributes.append(attr_assoc) db.session.flush() def generate_name(self): if self.location is None: warnings.warn('Room has no location; using default name format') return '{}/{}-{}'.format(self.building, self.floor, self.number) return self.location.room_name_format.format(building=self.building, floor=self.floor, number=self.number) @classmethod def find_all(cls, *args, **kwargs): """Retrieves rooms, sorted by location and full name""" rooms = super(Room, cls).find_all(*args, **kwargs) rooms.sort( key=lambda r: natural_sort_key(r.location_name + r.full_name)) return rooms @classmethod def find_with_attribute(cls, attribute): """Search rooms which have a specific attribute""" return (Room.query.with_entities( Room, RoomAttributeAssociation.value).join( Room.attributes, RoomAttributeAssociation.attribute).filter( RoomAttribute.name == attribute).all()) @staticmethod def get_with_data(*args, **kwargs): from indico.modules.rb.models.locations import Location only_active = kwargs.pop('only_active', True) filters = kwargs.pop('filters', None) order = kwargs.pop('order', [ Location.name, Room.building, Room.floor, Room.number, Room.verbose_name ]) if kwargs: raise ValueError('Unexpected kwargs: {}'.format(kwargs)) query = Room.query entities = [Room] if 'equipment' in args: entities.append(static_array.array_agg(EquipmentType.name)) query = query.outerjoin(RoomEquipmentAssociation).outerjoin( EquipmentType) query = (query.with_entities(*entities).outerjoin( Location, Location.id == Room.location_id).group_by(Location.name, Room.id)) if only_active: query = query.filter(~Room.is_deleted) if filters: # pragma: no cover query = query.filter(*filters) if order: # pragma: no cover query = query.order_by(*order) keys = ('room', ) + tuple(args) return (dict(zip(keys, row if args else [row])) for row in query) @staticmethod def filter_available(start_dt, end_dt, repetition, include_blockings=True, include_pre_bookings=True, include_pending_blockings=False): """Returns a SQLAlchemy filter criterion ensuring that the room is available during the given time.""" # Check availability against reservation occurrences dummy_occurrences = ReservationOccurrence.create_series( start_dt, end_dt, repetition) overlap_criteria = ReservationOccurrence.filter_overlap( dummy_occurrences) reservation_criteria = [ Reservation.room_id == Room.id, ReservationOccurrence.is_valid, overlap_criteria ] if not include_pre_bookings: reservation_criteria.append(Reservation.is_accepted) occurrences_filter = (Reservation.query.join( ReservationOccurrence.reservation).filter( and_(*reservation_criteria))) # Check availability against blockings filters = ~occurrences_filter.exists() if include_blockings: if include_pending_blockings: valid_states = (BlockedRoom.State.accepted, BlockedRoom.State.pending) else: valid_states = (BlockedRoom.State.accepted, ) # TODO: only take blockings into account which the user cannot override blocking_criteria = [ Room.id == BlockedRoom.room_id, BlockedRoom.state.in_(valid_states), db_dates_overlap(Blocking, 'start_date', end_dt.date(), 'end_date', start_dt.date(), inclusive=True) ] blockings_filter = (BlockedRoom.query.join( Blocking.blocked_rooms).filter(and_(*blocking_criteria))) return filters & ~blockings_filter.exists() return filters @staticmethod def filter_bookable_hours(start_time, end_time): if end_time == time(0): end_time = time(23, 59, 59) period_end_time = db.case({time(0): time(23, 59, 59)}, else_=BookableHours.end_time, value=BookableHours.end_time) bookable_hours_filter = Room.bookable_hours.any( (BookableHours.start_time <= start_time) & (period_end_time >= end_time)) return ~Room.bookable_hours.any() | bookable_hours_filter @staticmethod def filter_nonbookable_periods(start_dt, end_dt): return ~Room.nonbookable_periods.any( and_(NonBookablePeriod.start_dt <= end_dt, NonBookablePeriod.end_dt >= start_dt)) def get_blocked_rooms(self, *dates, **kwargs): states = kwargs.get('states', (BlockedRoom.State.accepted, )) return (self.blocked_rooms.join(BlockedRoom.blocking).options( contains_eager(BlockedRoom.blocking)).filter( or_(Blocking.is_active_at(d) for d in dates), BlockedRoom.state.in_(states)).all()) @property def protection_parent(self): return None @staticmethod def is_user_admin(user): return rb_is_admin(user) @classmethod def get_permissions_for_user(cls, user, allow_admin=True): """Get the permissions for all rooms for a user. In case of multipass-based groups it will try to get a list of all groups the user is in, and if that's not possible check the permissions one by one for each room (which may result in many group membership lookups). It is recommended to not call this in any place where performance matters and to memoize the result. """ # XXX: When changing the logic in here, make sure to update can_* as well! all_rooms_query = (Room.query.filter(~Room.is_deleted).options( load_only('id', 'protection_mode', 'reservations_need_confirmation', 'is_reservable', 'owner_id'), joinedload('owner').load_only('id'), joinedload('acl_entries'))) is_admin = allow_admin and cls.is_user_admin(user) if (is_admin and allow_admin) or not user.can_get_all_multipass_groups: # check one by one if we can't get a list of all groups the user is in return { r.id: { 'book': r.can_book(user, allow_admin=allow_admin), 'prebook': r.can_prebook(user, allow_admin=allow_admin), 'override': r.can_override(user, allow_admin=allow_admin), 'moderate': r.can_moderate(user, allow_admin=allow_admin), 'manage': r.can_manage(user, allow_admin=allow_admin), } for r in all_rooms_query } criteria = [ db.and_(RoomPrincipal.type == PrincipalType.user, RoomPrincipal.user_id == user.id) ] for group in user.local_groups: criteria.append( db.and_(RoomPrincipal.type == PrincipalType.local_group, RoomPrincipal.local_group_id == group.id)) for group in user.iter_all_multipass_groups(): criteria.append( db.and_( RoomPrincipal.type == PrincipalType.multipass_group, RoomPrincipal.multipass_group_provider == group.provider.name, db.func.lower(RoomPrincipal.multipass_group_name) == group.name.lower())) data = {} permissions = {'book', 'prebook', 'override', 'moderate', 'manage'} prebooking_required_rooms = set() non_reservable_rooms = set() for room in all_rooms_query: is_owner = user == room.owner data[room.id] = {x: False for x in permissions} if room.reservations_need_confirmation: prebooking_required_rooms.add(room.id) if not room.is_reservable: non_reservable_rooms.add(room.id) if (room.is_reservable and (room.is_public or is_owner)) or (is_admin and allow_admin): if not room.reservations_need_confirmation or is_owner or ( is_admin and allow_admin): data[room.id]['book'] = True if room.reservations_need_confirmation: data[room.id]['prebook'] = True if is_owner or (is_admin and allow_admin): data[room.id]['override'] = True data[room.id]['moderate'] = True data[room.id]['manage'] = True query = (RoomPrincipal.query.join(Room).filter( ~Room.is_deleted, db.or_(*criteria)).options( load_only('room_id', 'full_access', 'permissions'))) for principal in query: is_reservable = principal.room_id not in non_reservable_rooms for permission in permissions: if not is_reservable and not (is_admin and allow_admin ) and permission in ('book', 'prebook'): continue explicit = permission == 'prebook' and principal.room_id not in prebooking_required_rooms check_permission = None if permission == 'manage' else permission if principal.has_management_permission(check_permission, explicit=explicit): data[principal.room_id][permission] = True return data def can_access(self, user, allow_admin=True): # rooms are never access-restricted raise NotImplementedError def can_manage(self, user, permission=None, allow_admin=True, check_parent=True, explicit_permission=False): if user and user == self.owner and (permission is None or not explicit_permission): return True return super(Room, self).can_manage(user, permission=permission, allow_admin=allow_admin, check_parent=check_parent, explicit_permission=explicit_permission) def can_book(self, user, allow_admin=True): # XXX: When changing the logic in here, make sure to update get_permissions_for_user as well! if not user: return False if not self.is_reservable and not (allow_admin and self.is_user_admin(user)): return False if self.is_public and not self.reservations_need_confirmation: return True return self.can_manage(user, permission='book', allow_admin=allow_admin) def can_prebook(self, user, allow_admin=True): # XXX: When changing the logic in here, make sure to update get_permissions_for_user as well! if not user: return False if not self.is_reservable and not (allow_admin and self.is_user_admin(user)): return False if self.is_public and self.reservations_need_confirmation: return True # When the room does not use prebookings, we do not want the prebook option to show # up for admins or room managers unless they are actually in the ACL with the prebook # permission. explicit = not self.reservations_need_confirmation return self.can_manage(user, permission='prebook', allow_admin=allow_admin, explicit_permission=explicit) def can_override(self, user, allow_admin=True): # XXX: When changing the logic in here, make sure to update get_permissions_for_user as well! return self.can_manage(user, permission='override', allow_admin=allow_admin) def can_moderate(self, user, allow_admin=True): # XXX: When changing the logic in here, make sure to update get_permissions_for_user as well! return self.can_manage(user, permission='moderate', allow_admin=allow_admin) def can_edit(self, user): if not user: return False return rb_is_admin(user) def can_delete(self, user): if not user: return False return rb_is_admin(user) def check_advance_days(self, end_date, user=None, quiet=False): if not self.max_advance_days: return True if user and (rb_is_admin(user) or self.can_manage(user)): return True advance_days = (end_date - date.today()).days ok = advance_days < self.max_advance_days if quiet or ok: return ok else: msg = _(u'You cannot book this room more than {} days in advance') raise NoReportError(msg.format(self.max_advance_days)) def check_bookable_hours(self, start_time, end_time, user=None, quiet=False): if user and (rb_is_admin(user) or self.can_manage(user)): return True bookable_hours = self.bookable_hours.all() if not bookable_hours: return True for bt in bookable_hours: if bt.fits_period(start_time, end_time): return True if quiet: return False raise NoReportError(u'Room cannot be booked at this time')
class Room(versioned_cache(_cache, 'id'), ProtectionManagersMixin, db.Model, Serializer): __tablename__ = 'rooms' __table_args__ = (db.UniqueConstraint('id', 'location_id'), # useless but needed for the LocationMixin fkey db.CheckConstraint("verbose_name != ''", 'verbose_name_not_empty'), {'schema': 'roombooking'}) default_protection_mode = ProtectionMode.public disallowed_protection_modes = frozenset({ProtectionMode.inheriting}) __public__ = [ 'id', 'name', 'location_name', 'floor', 'number', 'building', 'booking_url', 'capacity', 'comments', 'owner_id', 'details_url', 'large_photo_url', 'has_photo', 'sprite_position', 'is_active', 'is_reservable', 'is_auto_confirm', 'marker_description', 'kind', 'booking_limit_days' ] __public_exhaustive__ = __public__ + [ 'has_webcast_recording', 'has_vc', 'has_projector', 'is_public', 'has_booking_groups' ] __calendar_public__ = [ 'id', 'building', 'name', 'floor', 'number', 'kind', 'booking_url', 'details_url', 'location_name', 'max_advance_days' ] __api_public__ = ( 'id', 'building', 'name', 'floor', 'longitude', 'latitude', ('number', 'roomNr'), ('location_name', 'location'), ('full_name', 'fullName'), ('booking_url', 'bookingUrl') ) __api_minimal_public__ = ( 'id', ('full_name', 'fullName') ) id = db.Column( db.Integer, primary_key=True ) location_id = db.Column( db.Integer, db.ForeignKey('roombooking.locations.id'), nullable=False ) photo_id = db.Column( db.Integer, db.ForeignKey('roombooking.photos.id') ) #: Verbose name for the room (long) verbose_name = db.Column( db.String, nullable=True, default=None ) site = db.Column( db.String, default='' ) division = db.Column( db.String ) building = db.Column( db.String, nullable=False ) floor = db.Column( db.String, default='', nullable=False ) number = db.Column( db.String, default='', nullable=False ) notification_before_days = db.Column( db.Integer ) notification_before_days_weekly = db.Column( db.Integer ) notification_before_days_monthly = db.Column( db.Integer ) notification_for_assistance = db.Column( db.Boolean, nullable=False, default=False ) reservations_need_confirmation = db.Column( db.Boolean, nullable=False, default=False ) notifications_enabled = db.Column( db.Boolean, nullable=False, default=True ) telephone = db.Column( db.String ) key_location = db.Column( db.String ) capacity = db.Column( db.Integer, default=20 ) surface_area = db.Column( db.Integer ) longitude = db.Column( db.Float ) latitude = db.Column( db.Float ) comments = db.Column( db.String ) owner_id = db.Column( db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=False ) is_active = db.Column( db.Boolean, nullable=False, default=True, index=True ) is_reservable = db.Column( db.Boolean, nullable=False, default=True ) max_advance_days = db.Column( db.Integer ) booking_limit_days = db.Column( db.Integer ) acl_entries = db.relationship( 'RoomPrincipal', lazy=True, backref='room', cascade='all, delete-orphan', collection_class=set ) attributes = db.relationship( 'RoomAttributeAssociation', backref='room', cascade='all, delete-orphan', lazy='dynamic' ) blocked_rooms = db.relationship( 'BlockedRoom', backref='room', cascade='all, delete-orphan', lazy='dynamic' ) bookable_hours = db.relationship( 'BookableHours', backref='room', order_by=BookableHours.start_time, cascade='all, delete-orphan', lazy='dynamic' ) available_equipment = db.relationship( 'EquipmentType', secondary=RoomEquipmentAssociation, backref='rooms', lazy=True ) nonbookable_periods = db.relationship( 'NonBookablePeriod', backref='room', order_by=NonBookablePeriod.end_dt.desc(), cascade='all, delete-orphan', lazy='dynamic' ) photo = db.relationship( 'Photo', backref='room', cascade='all, delete-orphan', single_parent=True, lazy=True ) reservations = db.relationship( 'Reservation', backref='room', cascade='all, delete-orphan', lazy='dynamic' ) favorite_of = db.relationship( 'User', secondary=favorite_room_table, lazy=True, collection_class=set, backref=db.backref('favorite_rooms', lazy=True, collection_class=set), ) #: The owner of the room. If the room has the `manager-group` #: attribute set, any users in that group are also considered #: owners when it comes to management privileges. #: Use :meth:`is_owned_by` for ownership checks that should #: also check against the management group. owner = db.relationship( 'User', # subquery load since a normal joinedload breaks `get_with_data` lazy='subquery', backref=db.backref( 'owned_rooms', lazy='dynamic' ) ) # relationship backrefs: # - breaks (Break.own_room) # - contributions (Contribution.own_room) # - events (Event.own_room) # - location (Location.rooms) # - session_blocks (SessionBlock.own_room) # - sessions (Session.own_room) @hybrid_property def is_auto_confirm(self): return not self.reservations_need_confirmation @is_auto_confirm.expression def is_auto_confirm(self): return ~self.reservations_need_confirmation @property def booking_url(self): if self.id is None: return None return url_for('rooms.room_book', self) @property def details_url(self): if self.id is None: return None return url_for('rooms.roomBooking-roomDetails', self) @property def large_photo_url(self): if self.id is None: return None return url_for('rooms.photo', roomID=self.id) @property def map_url(self): if self.location.map_url_template: return self.location.map_url_template.format( building=self.building, floor=self.floor, number=self.number ) else: return None @property def has_photo(self): return self.photo_id is not None @hybrid_property def name(self): return self.generate_name() @name.expression def name(cls): q = (db.session.query(db.m.Location.room_name_format) .filter(db.m.Location.id == cls.location_id) .correlate(Room) .as_scalar()) return db.func.format(q, cls.building, cls.floor, cls.number) @hybrid_property def full_name(self): if self.verbose_name: return u'{} - {}'.format(self.generate_name(), self.verbose_name) else: return u'{}'.format(self.generate_name()) @full_name.expression def full_name(cls): return db.case([ [cls.verbose_name.isnot(None), cls.name + ' - ' + cls.verbose_name] ], else_=cls.name) @property @cached(_cache) def has_booking_groups(self): return self.has_attribute('allowed-booking-group') @property @cached(_cache) def has_projector(self): return self.has_equipment(u'Computer Projector', u'Video projector 4:3', u'Video projector 16:9') @property @cached(_cache) def has_webcast_recording(self): return self.has_equipment('Webcast/Recording') @property @cached(_cache) def has_vc(self): return self.has_equipment('Video conference') @property def kind(self): if not self.is_reservable or self.has_booking_groups: return 'privateRoom' elif self.reservations_need_confirmation: return 'moderatedRoom' else: return 'basicRoom' @property def location_name(self): return self.location.name @property def marker_description(self): infos = [] infos.append(u'{capacity} {label}'.format(capacity=self.capacity, label=_(u'person') if self.capacity == 1 else _(u'people'))) infos.append(_(u'public') if self.is_public else _(u'private')) infos.append(_(u'auto-confirmation') if self.is_auto_confirm else _(u'needs confirmation')) if self.has_vc: infos.append(_(u'videoconference')) return u', '.join(map(unicode, infos)) @property def manager_emails(self): manager_group = self.get_attribute_value('manager-group') if not manager_group: return set() group = GroupProxy.get_named_default_group(manager_group) return {u.email for u in group.get_members()} @property def notification_emails(self): return set(filter(None, map(unicode.strip, self.get_attribute_value(u'notification-email', u'').split(u',')))) @property def sprite_position(self): sprite_mapping = _cache.get('rooms-sprite-mapping') return sprite_mapping.get(self.id, 0) if sprite_mapping else 0 # placeholder at position 0 @return_ascii def __repr__(self): return format_repr(self, 'id', 'full_name') @cached(_cache) def has_equipment(self, *names): available = {x.name for x in self.available_equipment} return bool(available & set(names)) def get_attribute_by_name(self, attribute_name): return (self.attributes .join(RoomAttribute) .filter(RoomAttribute.name == attribute_name) .first()) def has_attribute(self, attribute_name): return self.get_attribute_by_name(attribute_name) is not None @cached(_cache) def get_attribute_value(self, name, default=None): attr = self.get_attribute_by_name(name) return attr.value if attr else default def set_attribute_value(self, name, value): attr = self.get_attribute_by_name(name) if attr: if value: attr.value = value else: self.attributes.filter(RoomAttributeAssociation.attribute_id == attr.attribute_id) \ .delete(synchronize_session='fetch') elif value: attr = RoomAttribute.query.filter_by(name=name).first() if not attr: raise ValueError("Attribute {} does not exist".format(name)) attr_assoc = RoomAttributeAssociation() attr_assoc.value = value attr_assoc.attribute = attr self.attributes.append(attr_assoc) db.session.flush() @locator_property def locator(self): return {'roomLocation': self.location_name, 'roomID': self.id} def generate_name(self): if self.location is None: warnings.warn('Room has no location; using default name format') return '{}/{}-{}'.format(self.building, self.floor, self.number) return self.location.room_name_format.format( building=self.building, floor=self.floor, number=self.number ) @classmethod def find_all(cls, *args, **kwargs): """Retrieves rooms, sorted by location and full name""" rooms = super(Room, cls).find_all(*args, **kwargs) rooms.sort(key=lambda r: natural_sort_key(r.location_name + r.full_name)) return rooms @classmethod def find_with_attribute(cls, attribute): """Search rooms which have a specific attribute""" return (Room.query .with_entities(Room, RoomAttributeAssociation.value) .join(Room.attributes, RoomAttributeAssociation.attribute) .filter(RoomAttribute.name == attribute) .all()) @staticmethod def get_with_data(*args, **kwargs): from indico.modules.rb.models.locations import Location only_active = kwargs.pop('only_active', True) filters = kwargs.pop('filters', None) order = kwargs.pop('order', [Location.name, Room.building, Room.floor, Room.number, Room.verbose_name]) if kwargs: raise ValueError('Unexpected kwargs: {}'.format(kwargs)) query = Room.query entities = [Room] if 'equipment' in args: entities.append(static_array.array_agg(EquipmentType.name)) query = query.outerjoin(RoomEquipmentAssociation).outerjoin(EquipmentType) query = (query.with_entities(*entities) .outerjoin(Location, Location.id == Room.location_id) .group_by(Location.name, Room.id)) if only_active: query = query.filter(Room.is_active) if filters: # pragma: no cover query = query.filter(*filters) if order: # pragma: no cover query = query.order_by(*order) keys = ('room',) + tuple(args) return (dict(zip(keys, row if args else [row])) for row in query) @classproperty @staticmethod def max_capacity(): return db.session.query(db.func.max(Room.capacity)).scalar() or 0 @staticmethod def filter_available(start_dt, end_dt, repetition, include_blockings=True, include_pre_bookings=True, include_pending_blockings=False): """Returns a SQLAlchemy filter criterion ensuring that the room is available during the given time.""" # Check availability against reservation occurrences dummy_occurrences = ReservationOccurrence.create_series(start_dt, end_dt, repetition) overlap_criteria = ReservationOccurrence.filter_overlap(dummy_occurrences) reservation_criteria = [Reservation.room_id == Room.id, ReservationOccurrence.is_valid, overlap_criteria] if not include_pre_bookings: reservation_criteria.append(Reservation.is_accepted) occurrences_filter = (Reservation.query .join(ReservationOccurrence.reservation) .filter(and_(*reservation_criteria))) # Check availability against blockings filters = ~occurrences_filter.exists() if include_blockings: if include_pending_blockings: valid_states = (BlockedRoom.State.accepted, BlockedRoom.State.pending) else: valid_states = (BlockedRoom.State.accepted,) # TODO: only take blockings into account which the user cannot override blocking_criteria = [Room.id == BlockedRoom.room_id, BlockedRoom.state.in_(valid_states), db_dates_overlap(Blocking, 'start_date', end_dt.date(), 'end_date', start_dt.date(), inclusive=True)] blockings_filter = (BlockedRoom.query .join(Blocking.blocked_rooms) .filter(and_(*blocking_criteria))) return filters & ~blockings_filter.exists() return filters @staticmethod def filter_bookable_hours(start_time, end_time): if end_time == time(0): end_time = time(23, 59, 59) period_end_time = db.case({time(0): time(23, 59, 59)}, else_=BookableHours.end_time, value=BookableHours.end_time) bookable_hours_filter = Room.bookable_hours.any( (BookableHours.start_time <= start_time) & (period_end_time >= end_time) ) return ~Room.bookable_hours.any() | bookable_hours_filter @staticmethod def filter_nonbookable_periods(start_dt, end_dt): return ~Room.nonbookable_periods.any(and_(NonBookablePeriod.start_dt <= end_dt, NonBookablePeriod.end_dt >= start_dt)) @staticmethod def find_with_filters(filters, user=None): from indico.modules.rb.models.locations import Location equipment_count = len(filters.get('available_equipment', ())) equipment_subquery = None if equipment_count: equipment_subquery = ( db.session.query(RoomEquipmentAssociation) .with_entities(func.count(RoomEquipmentAssociation.c.room_id)) .filter( RoomEquipmentAssociation.c.room_id == Room.id, RoomEquipmentAssociation.c.equipment_id.in_(eq.id for eq in filters['available_equipment']) ) .correlate(Room) .as_scalar() ) capacity = filters.get('capacity') q = ( Room.query .join(Location.rooms) .filter( Location.id == filters['location'].id if filters.get('location') else True, ((Room.capacity >= (capacity * 0.8)) | (Room.capacity == None)) if capacity else True, Room.is_reservable if filters.get('is_only_public') else True, Room.is_auto_confirm if filters.get('is_auto_confirm') else True, Room.is_active if filters.get('is_only_active', False) else True, (equipment_subquery == equipment_count) if equipment_subquery is not None else True) ) if filters.get('available', -1) != -1: repetition = RepeatMapping.convert_legacy_repeatability(ast.literal_eval(filters['repeatability'])) is_available = Room.filter_available(filters['start_dt'], filters['end_dt'], repetition, include_blockings=True, include_pre_bookings=filters.get('include_pre_bookings', True), include_pending_blockings=filters.get('include_pending_blockings', True)) # Filter the search results if filters['available'] == 0: # booked/unavailable q = q.filter(~is_available) elif filters['available'] == 1: # available q = q.filter(is_available) else: raise ValueError('Unexpected availability value') free_search_columns = ( 'full_name', 'site', 'division', 'building', 'floor', 'number', 'telephone', 'key_location', 'comments' ) if filters.get('details'): # Attributes are stored JSON-encoded, so we need to JSON-encode the provided string and remove the quotes # afterwards since PostgreSQL currently does not expose a function to decode a JSON string: # http://www.postgresql.org/message-id/[email protected] details = filters['details'].lower() details_str = u'%{}%'.format(escape_like(details)) details_json = u'%{}%'.format(escape_like(json.dumps(details)[1:-1])) free_search_criteria = [getattr(Room, c).ilike(details_str) for c in free_search_columns] free_search_criteria.append(Room.attributes.any(cast(RoomAttributeAssociation.value, db.String) .ilike(details_json))) q = q.filter(or_(*free_search_criteria)) q = q.order_by(Room.capacity) rooms = q.all() # Apply a bunch of filters which are *much* easier to do here than in SQL! if filters.get('is_only_public'): # This may trigger additional SQL queries but is_public is cached and doing this check here is *much* easier rooms = [r for r in rooms if r.is_public] if filters.get('is_only_my_rooms'): assert user is not None rooms = [r for r in rooms if r.is_owned_by(user)] if capacity: # Unless it would result in an empty resultset we don't want to show rooms with >20% more capacity # than requested. This cannot be done easily in SQL so we do that logic here after the SQL query already # weeded out rooms that are too small matching_capacity_rooms = [r for r in rooms if r.capacity is None or r.capacity <= capacity * 1.2] if matching_capacity_rooms: rooms = matching_capacity_rooms return rooms def has_live_reservations(self): return self.reservations.filter_by( is_archived=False, is_cancelled=False, is_rejected=False ).count() > 0 def get_blocked_rooms(self, *dates, **kwargs): states = kwargs.get('states', (BlockedRoom.State.accepted,)) return (self.blocked_rooms .join(BlockedRoom.blocking) .options(contains_eager(BlockedRoom.blocking)) .filter(or_(Blocking.is_active_at(d) for d in dates), BlockedRoom.state.in_(states)) .all()) @property def protection_parent(self): return None @staticmethod def is_user_admin(user): return rb_is_admin(user) @classmethod def get_permissions_for_user(cls, user, allow_admin=True): """Get the permissions for all rooms for a user. In case of multipass-based groups it will try to get a list of all groups the user is in, and if that's not possible check the permissions one by one for each room (which may result in many group membership lookups). It is recommended to not call this in any place where performance matters and to memoize the result. """ # XXX: When changing the logic in here, make sure to update can_* as well! all_rooms_query = (Room.query .filter(Room.is_active) .options(load_only('id', 'protection_mode', 'reservations_need_confirmation', 'is_reservable'), raiseload('owner'), joinedload('acl_entries'))) is_admin = allow_admin and cls.is_user_admin(user) if (is_admin and allow_admin) or not user.can_get_all_multipass_groups: # check one by one if we can't get a list of all groups the user is in return {r.id: { 'book': r.can_book(user, allow_admin=allow_admin), 'prebook': r.can_prebook(user, allow_admin=allow_admin), 'override': r.can_override(user, allow_admin=allow_admin), 'moderate': r.can_moderate(user, allow_admin=allow_admin), 'manage': r.can_manage(user, allow_admin=allow_admin), } for r in all_rooms_query} criteria = [db.and_(RoomPrincipal.type == PrincipalType.user, RoomPrincipal.user_id == user.id)] for group in user.local_groups: criteria.append(db.and_(RoomPrincipal.type == PrincipalType.local_group, RoomPrincipal.local_group_id == group.id)) for group in user.iter_all_multipass_groups(): criteria.append(db.and_(RoomPrincipal.type == PrincipalType.multipass_group, RoomPrincipal.multipass_group_provider == group.provider.name, db.func.lower(RoomPrincipal.multipass_group_name) == group.name.lower())) data = {} permissions = {'book', 'prebook', 'override', 'moderate', 'manage'} prebooking_required_rooms = set() non_reservable_rooms = set() for room in all_rooms_query: data[room.id] = {x: False for x in permissions} if room.reservations_need_confirmation: prebooking_required_rooms.add(room.id) if not room.is_reservable: non_reservable_rooms.add(room.id) if (room.is_reservable and room.is_public) or (is_admin and allow_admin): if not room.reservations_need_confirmation or (is_admin and allow_admin): data[room.id]['book'] = True if room.reservations_need_confirmation: data[room.id]['prebook'] = True if is_admin and allow_admin: data[room.id]['override'] = True data[room.id]['moderate'] = True data[room.id]['manage'] = True query = (RoomPrincipal.query .join(Room) .filter(Room.is_active, db.or_(*criteria)) .options(load_only('room_id', 'full_access', 'permissions'))) for principal in query: is_reservable = principal.room_id not in non_reservable_rooms for permission in permissions: if not is_reservable and not (is_admin and allow_admin) and permission in ('book', 'prebook'): continue explicit = permission == 'prebook' and principal.room_id not in prebooking_required_rooms check_permission = None if permission == 'manage' else permission if principal.has_management_permission(check_permission, explicit=explicit): data[principal.room_id][permission] = True return data def can_access(self, user, allow_admin=True): # rooms are never access-restricted raise NotImplementedError def can_book(self, user, allow_admin=True): # XXX: When changing the logic in here, make sure to update get_permissions_for_user as well! if not user: return False if not self.is_reservable and not (allow_admin and self.is_user_admin(user)): return False if self.is_public and not self.reservations_need_confirmation: return True return self.can_manage(user, permission='book', allow_admin=allow_admin) def can_prebook(self, user, allow_admin=True): # XXX: When changing the logic in here, make sure to update get_permissions_for_user as well! if not user: return False if not self.is_reservable and not (allow_admin and self.is_user_admin(user)): return False if self.is_public and self.reservations_need_confirmation: return True # When the room does not use prebookings, we do not want the prebook option to show # up for admins or room managers unless they are actually in the ACL with the prebook # permission. explicit = not self.reservations_need_confirmation return self.can_manage(user, permission='prebook', allow_admin=allow_admin, explicit_permission=explicit) def can_override(self, user, allow_admin=True): # XXX: When changing the logic in here, make sure to update get_permissions_for_user as well! return self.can_manage(user, permission='override', allow_admin=allow_admin) def can_moderate(self, user, allow_admin=True): # XXX: When changing the logic in here, make sure to update get_permissions_for_user as well! return self.can_manage(user, permission='moderate', allow_admin=allow_admin) def can_edit(self, user): if not user: return False return rb_is_admin(user) def can_delete(self, user): if not user: return False return rb_is_admin(user) @unify_user_args @cached(_cache) def is_owned_by(self, user): """Checks if the user is managing the room (owner or manager)""" if self.owner == user: return True manager_group = self.get_attribute_value('manager-group') if not manager_group: return False return user in GroupProxy.get_named_default_group(manager_group) @classmethod def get_owned_by(cls, user): return [room for room in cls.find(is_active=True) if room.is_owned_by(user)] @classmethod def user_owns_rooms(cls, user): return any(room for room in cls.find(is_active=True) if room.is_owned_by(user)) def check_advance_days(self, end_date, user=None, quiet=False): if not self.max_advance_days: return True if user and (rb_is_admin(user) or self.is_owned_by(user)): return True advance_days = (end_date - date.today()).days ok = advance_days < self.max_advance_days if quiet or ok: return ok else: msg = _(u'You cannot book this room more than {} days in advance') raise NoReportError(msg.format(self.max_advance_days)) def check_bookable_hours(self, start_time, end_time, user=None, quiet=False): if user and (rb_is_admin(user) or self.is_owned_by(user)): return True bookable_hours = self.bookable_hours.all() if not bookable_hours: return True for bt in bookable_hours: if bt.fits_period(start_time, end_time): return True if quiet: return False raise NoReportError(u'Room cannot be booked at this time')
def __auto_table_args(cls): return db.CheckConstraint( 'read_access OR full_access OR array_length(roles, 1) IS NOT NULL', 'has_privs'),
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Indico; if not, see <http://www.gnu.org/licenses/>. from __future__ import unicode_literals from indico.core.db.sqlalchemy import db _track_abstract_reviewers_table = db.Table( 'track_abstract_reviewers', db.metadata, db.Column('id', db.Integer, primary_key=True), db.Column('user_id', db.Integer, db.ForeignKey('users.users.id'), index=True, nullable=False), db.Column('event_id', db.Integer, db.ForeignKey('events.events.id'), index=True), db.Column('track_id', db.Integer, db.ForeignKey('events.tracks.id'), index=True), db.CheckConstraint('(track_id IS NULL) != (event_id IS NULL)', name='track_xor_event_id_null'), schema='events')
class Event(ProtectionManagersMixin, db.Model): """An Indico event This model contains the most basic information related to an event. Note that the ACL is currently only used for managers but not for view access! """ __tablename__ = 'events' __table_args__ = ( db.CheckConstraint("(logo IS NULL) = (logo_metadata::text = 'null')", 'valid_logo'), db.CheckConstraint( "(stylesheet IS NULL) = (stylesheet_metadata::text = 'null')", 'valid_stylesheet'), { 'schema': 'events' }) disallowed_protection_modes = frozenset() inheriting_have_acl = True __logging_disabled = False #: The ID of the event id = db.Column(db.Integer, primary_key=True) #: If the event has been deleted is_deleted = db.Column(db.Boolean, nullable=False, default=False) #: The ID of the user who created the event creator_id = db.Column(db.Integer, db.ForeignKey('users.users.id'), nullable=False, index=True) #: The metadata of the logo (hash, size, filename, content_type) logo_metadata = db.Column(JSON, nullable=False, default=None) #: The logo's raw image data logo = db.deferred(db.Column(db.LargeBinary, nullable=True)) #: The metadata of the stylesheet (hash, size, filename) stylesheet_metadata = db.Column(JSON, nullable=False, default=None) #: The stylesheet's raw image data stylesheet = db.deferred(db.Column(db.Text, nullable=True)) #: The ID of the event's default page (conferences only) default_page_id = db.Column(db.Integer, db.ForeignKey('events.pages.id'), index=True, nullable=True) #: The last user-friendly registration ID _last_friendly_registration_id = db.deferred( db.Column('last_friendly_registration_id', db.Integer, nullable=False, default=0)) #: The user who created the event creator = db.relationship('User', lazy=True, backref=db.backref('created_events', lazy='dynamic')) #: The event's default page (conferences only) default_page = db.relationship( 'EventPage', lazy=True, foreign_keys=[default_page_id], # don't use this backref. we just need it so SA properly NULLs # this column when deleting the default page backref=db.backref('_default_page_of_event', lazy=True)) #: The ACL entries for the event acl_entries = db.relationship('EventPrincipal', backref='event_new', cascade='all, delete-orphan', collection_class=set) # relationship backrefs: # - agreements (Agreement.event_new) # - attachment_folders (AttachmentFolder.event_new) # - custom_pages (EventPage.event_new) # - layout_images (ImageFile.event_new) # - legacy_attachment_folder_mappings (LegacyAttachmentFolderMapping.event_new) # - legacy_attachment_mappings (LegacyAttachmentMapping.event_new) # - legacy_mapping (LegacyEventMapping.event_new) # - log_entries (EventLogEntry.event_new) # - menu_entries (MenuEntry.event_new) # - notes (EventNote.event_new) # - registration_forms (RegistrationForm.event_new) # - registrations (Registration.event_new) # - reminders (EventReminder.event_new) # - requests (Request.event_new) # - reservations (Reservation.event_new) # - settings (EventSetting.event_new) # - settings_principals (EventSettingPrincipal.event_new) # - static_sites (StaticSite.event_new) # - surveys (Survey.event_new) # - vc_room_associations (VCRoomEventAssociation.event_new) @property @memoize_request def as_legacy(self): """Returns a legacy `Conference` object (ZODB)""" from MaKaC.conference import ConferenceHolder return ConferenceHolder().getById(self.id, True) @property def protection_parent(self): return self.as_legacy.getOwner() @property def has_logo(self): return self.logo_metadata is not None @property def logo_url(self): return url_for('event_images.logo_display', self, slug=self.logo_metadata['hash']) @property def has_stylesheet(self): return self.stylesheet_metadata is not None @property def locator(self): return {'confId': self.id} @property def participation_regform(self): return self.registration_forms.filter_by(is_participation=True, is_deleted=False).first() @property def title(self): return to_unicode(self.as_legacy.getTitle()) @property def type(self): event_type = self.as_legacy.getType() if event_type == 'simple_event': event_type = 'lecture' return event_type @property @contextmanager def logging_disabled(self): """Temporarily disables event logging This is useful when performing actions e.g. during event creation or at other times where adding entries to the event log doesn't make sense. """ self.__logging_disabled = True try: yield finally: self.__logging_disabled = False def can_access(self, user, allow_admin=True): if not allow_admin: raise NotImplementedError( 'can_access(..., allow_admin=False) is unsupported until ACLs are migrated' ) from MaKaC.accessControl import AccessWrapper return self.as_legacy.canAccess( AccessWrapper(user.as_avatar if user else None)) def can_manage(self, user, role=None, allow_key=False, *args, **kwargs): # XXX: Remove this method once modification keys are gone! return (super(Event, self).can_manage(user, role, *args, **kwargs) or (allow_key and self.as_legacy.canKeyModify())) @memoize_request def has_feature(self, feature): """Checks if a feature is enabled for the event""" from indico.modules.events.features.util import is_feature_enabled return is_feature_enabled(self, feature) def log(self, realm, kind, module, summary, user=None, type_='simple', data=None): """Creates a new log entry for the event :param realm: A value from :class:`.EventLogRealm` indicating the realm of the action. :param kind: A value from :class:`.EventLogKind` indicating the kind of the action that was performed. :param module: A human-friendly string describing the module related to the action. :param summary: A one-line summary describing the logged action. :param user: The user who performed the action. :param type_: The type of the log entry. This is used for custom rendering of the log message/data :param data: JSON-serializable data specific to the log type. In most cases the ``simple`` log type is fine. For this type, any items from data will be shown in the detailed view of the log entry. You may either use a dict (which will be sorted) alphabetically or a list of ``key, value`` pairs which will be displayed in the given order. """ if self.__logging_disabled: return entry = EventLogEntry(user=user, realm=realm, kind=kind, module=module, type=type_, summary=summary, data=data or {}) self.log_entries.append(entry) @return_ascii def __repr__(self): # TODO: add self.protection_repr once we use it and the title once we store it here return format_repr(self, 'id', is_deleted=False) # TODO: Remove the next block of code once event acls (read access) are migrated def _fail(self, *args, **kwargs): raise NotImplementedError( 'These properties are not usable until event ACLs are in the new DB' ) is_public = classproperty(classmethod(_fail)) is_inheriting = classproperty(classmethod(_fail)) is_protected = classproperty(classmethod(_fail)) protection_repr = property(_fail) del _fail