Пример #1
0
class PaperReviewRating(ReviewRatingMixin, db.Model):
    __tablename__ = 'review_ratings'
    __table_args__ = (db.UniqueConstraint('review_id', 'question_id'),
                      {'schema': 'event_paper_reviewing'})

    question_class = PaperReviewQuestion
    review_class = PaperReview
Пример #2
0
class AbstractReviewRating(db.Model):
    __tablename__ = 'abstract_review_ratings'
    __table_args__ = (db.UniqueConstraint('review_id', 'question_id'), {
        'schema': 'event_abstracts'
    })

    id = db.Column(db.Integer, primary_key=True)
    question_id = db.Column(
        db.Integer,
        db.ForeignKey('event_abstracts.abstract_review_questions.id'),
        index=True,
        nullable=False)
    review_id = db.Column(db.Integer,
                          db.ForeignKey('event_abstracts.abstract_reviews.id'),
                          index=True,
                          nullable=False)
    value = db.Column(db.Integer, nullable=False)
    question = db.relationship('AbstractReviewQuestion',
                               lazy=True,
                               backref=db.backref('ratings',
                                                  cascade='all, delete-orphan',
                                                  lazy=True))
    review = db.relationship('AbstractReview',
                             lazy=True,
                             backref=db.backref('ratings',
                                                cascade='all, delete-orphan',
                                                lazy=True))

    @return_ascii
    def __repr__(self):
        return format_repr(self, 'id', 'review_id', 'question_id')
Пример #3
0
class AbstractReviewRating(ReviewRatingMixin, db.Model):
    __tablename__ = 'abstract_review_ratings'
    __table_args__ = (db.UniqueConstraint('review_id', 'question_id'),
                      {'schema': 'event_abstracts'})

    question_class = AbstractReviewQuestion
    review_class = AbstractReview
Пример #4
0
class Chatroom(db.Model):
    __tablename__ = 'chatrooms'
    __table_args__ = (db.UniqueConstraint('jid_node', 'custom_server'), {
        'schema': 'plugin_chat'
    })

    #: Chatroom ID
    id = db.Column(db.Integer, primary_key=True)
    #: Node of the chatroom's JID (the part before `@domain`)
    jid_node = db.Column(db.String, nullable=False)
    #: Name of the chatroom
    name = db.Column(db.String, nullable=False)
    #: Description of the chatroom
    description = db.Column(db.Text, nullable=False, default='')
    #: Password to join the room
    password = db.Column(db.String, nullable=False, default='')
    #: Custom Jabber MUC server hostname
    custom_server = db.Column(db.String, nullable=False, default='')
    #: ID of the creator
    created_by_id = db.Column(db.Integer,
                              db.ForeignKey('users.users.id'),
                              index=True,
                              nullable=False)
    #: Creation timestamp of the chatroom
    created_dt = db.Column(UTCDateTime, nullable=False, default=now_utc)
    #: Modification timestamp of the chatroom
    modified_dt = db.Column(UTCDateTime)

    #: The user who created the chatroom
    created_by_user = db.relationship('User',
                                      lazy=True,
                                      backref=db.backref('chatrooms',
                                                         lazy='dynamic'))

    @property
    def locator(self):
        return {'chatroom_id': self.id}

    @property
    def server(self):
        """The server name of the chatroom.

        Usually the default one unless a custom one is set.
        """
        from indico_chat.plugin import ChatPlugin

        return self.custom_server or ChatPlugin.settings.get('muc_server')

    @property
    def jid(self):
        return '{}@{}'.format(self.jid_node, self.server)

    @return_ascii
    def __repr__(self):
        server = self.server
        if self.custom_server:
            server = '!' + server
        return '<Chatroom({}, {}, {}, {})>'.format(self.id, self.name,
                                                   self.jid_node, server)
Пример #5
0
class Judgment(db.Model):
    """Represents an abstract judgment, emitted by a judge"""

    __tablename__ = 'judgments'
    __table_args__ = (db.UniqueConstraint('abstract_id', 'track_id',
                                          'judge_user_id'), {
                                              'schema': 'event_abstracts'
                                          })

    id = db.Column(db.Integer, primary_key=True)
    creation_dt = db.Column(UTCDateTime,
                            nullable=False,
                            default=now_utc,
                            index=True)
    abstract_id = db.Column(db.Integer,
                            db.ForeignKey('event_abstracts.abstracts.id'),
                            index=True,
                            nullable=False)
    track_id = db.Column(db.Integer, nullable=False)
    judge_user_id = db.Column(db.Integer,
                              db.ForeignKey('users.users.id'),
                              nullable=False,
                              index=True)
    accepted_type_id = db.Column(db.Integer,
                                 db.ForeignKey('events.contribution_types.id'),
                                 nullable=True,
                                 index=True)
    abstract = db.relationship('Abstract',
                               lazy=False,
                               backref=db.backref('judgments', lazy='dynamic'))
    judge = db.relationship('User',
                            lazy=False,
                            backref=db.backref('abstract_judgments',
                                               lazy='dynamic'))
    accepted_type = db.relationship('ContributionType',
                                    lazy=False,
                                    backref=db.backref('abstract_judgments',
                                                       lazy='dynamic'))

    @return_ascii
    def __repr__(self):
        return format_repr(self,
                           'id',
                           abstract=self.abstract,
                           judge=self.judge)

    @property
    def as_legacy(self):
        return next(
            (judgment for judgment in
             self.abstract.as_legacy.getJudgementHistoryByTrack(self.track)
             if judgment.getResponsible().as_new == self.judge), None)

    @property
    def track(self):
        return self.abstract.event_new.as_legacy.getTrackById(
            str(self.track_id))
Пример #6
0
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)
Пример #7
0
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')
Пример #8
0
 def __auto_table_args():
     return db.UniqueConstraint('module', 'name'),
Пример #9
0
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)))
Пример #10
0
 def __auto_table_args(cls):
     return (db.UniqueConstraint('person_id',
                                 *cls.person_link_unique_columns), )
Пример #11
0
class PaperReview(ProposalReviewMixin, RenderModeMixin, db.Model):
    """A paper review, emitted by a layout or content reviewer."""

    possible_render_modes = {RenderMode.markdown}
    default_render_mode = RenderMode.markdown

    revision_attr = 'revision'
    group_attr = 'type'
    group_proxy_cls = PaperTypeProxy

    __tablename__ = 'reviews'
    __table_args__ = (db.UniqueConstraint('revision_id', 'user_id', 'type'), {
        'schema': 'event_paper_reviewing'
    })
    TIMELINE_TYPE = 'review'

    id = db.Column(db.Integer, primary_key=True)
    revision_id = db.Column(
        db.Integer,
        db.ForeignKey('event_paper_reviewing.revisions.id'),
        index=True,
        nullable=False)
    user_id = db.Column(db.Integer,
                        db.ForeignKey('users.users.id'),
                        index=True,
                        nullable=False)
    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='')
    type = db.Column(PyIntEnum(PaperReviewType), nullable=False)
    proposed_action = db.Column(PyIntEnum(PaperAction), nullable=False)

    revision = db.relationship('PaperRevision',
                               lazy=True,
                               backref=db.backref('reviews',
                                                  lazy=True,
                                                  order_by=created_dt.desc()))
    user = db.relationship('User',
                           lazy=True,
                           backref=db.backref('paper_reviews', lazy='dynamic'))

    # relationship backrefs:
    # - ratings (PaperReviewRating.review)

    comment = RenderModeMixin.create_hybrid_property('_comment')

    @locator_property
    def locator(self):
        return dict(self.revision.locator, review_id=self.id)

    @return_ascii
    def __repr__(self):
        return format_repr(self,
                           'id',
                           'type',
                           'revision_id',
                           'user_id',
                           proposed_action=None)

    def can_edit(self, user, check_state=False):
        from indico.modules.events.papers.models.revisions import PaperRevisionState
        if user is None:
            return False
        if check_state and self.revision.state != PaperRevisionState.submitted:
            return False
        return self.user == user

    def can_view(self, user):
        if user is None:
            return False
        elif user == self.user:
            return True
        elif self.revision.paper.can_judge(user):
            return True
        return False

    @property
    def visibility(self):
        return PaperCommentVisibility.reviewers

    @property
    def score(self):
        ratings = [
            r for r in self.ratings if not r.question.is_deleted
            and r.question.field_type == 'rating' and r.value is not None
        ]
        if not ratings:
            return None
        return sum(x.value for x in ratings) / len(ratings)
Пример #12
0
class Room(versioned_cache(_cache, 'id'), db.Model, Serializer):
    __tablename__ = 'rooms'
    __table_args__ = (db.UniqueConstraint('id', 'location_id'),  # useless but needed for the LocationMixin fkey
                      {'schema': 'roombooking'})

    __public__ = [
        'id', 'name', 'location_name', 'floor', 'number', 'building',
        'booking_url', 'capacity', 'comments', 'owner_id', 'details_url',
        'large_photo_url', 'small_photo_url', 'has_photo', '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')
    )
    name = db.Column(
        db.String,
        nullable=False
    )
    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
    )
    latitude = db.Column(
        db.String
    )
    longitude = db.Column(
        db.String
    )
    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
    )

    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='dynamic'
    )

    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'
    )

    #: 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', self, size='large')

    @property
    def small_photo_url(self):
        if self.id is None:
            return None
        return url_for('rooms.photo', self, size='small')

    @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

    @property
    def full_name(self):
        if self.has_special_name:
            return u'{} - {}'.format(self.generate_name(), self.name)
        else:
            return u'{}'.format(self.generate_name())

    @property
    def has_special_name(self):
        return self.name and self.name != self.generate_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
    @cached(_cache)
    def is_public(self):
        return self.is_reservable and not self.has_booking_groups

    @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','))))

    @return_ascii
    def __repr__(self):
        return u'<Room({0}, {1}, {2})>'.format(
            self.id,
            self.location_id,
            self.name
        )

    @cached(_cache)
    def has_equipment(self, *names):
        return self.available_equipment.filter(EquipmentType.name.in_(names)).count() > 0

    def find_available_vc_equipment(self):
        vc_equipment = (self.available_equipment
                        .correlate(Room)
                        .with_entities(EquipmentType.id)
                        .filter_by(name='Video conference')
                        .as_scalar())
        return self.available_equipment.filter(EquipmentType.parent_id == vc_equipment)

    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 = self.location.get_attribute_by_name(name)
            if not attr:
                raise ValueError("Attribute {} not supported in location {}".format(name, self.location_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):
        return u'{}-{}-{}'.format(
            self.building,
            self.floor,
            self.number
        )

    def update_name(self):
        if not self.has_special_name and self.building and self.floor and self.number:
            self.name = self.generate_name()

    @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.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)
        if 'vc_equipment' in args or 'non_vc_equipment' in args:
            vc_id_subquery = db.session.query(EquipmentType.id) \
                                       .correlate(Room) \
                                       .filter_by(name='Video conference') \
                                       .join(RoomEquipmentAssociation) \
                                       .filter(RoomEquipmentAssociation.c.room_id == Room.id) \
                                       .as_scalar()

            if 'vc_equipment' in args:
                # noinspection PyTypeChecker
                entities.append(static_array.array(
                    db.session.query(EquipmentType.name)
                    .join(RoomEquipmentAssociation)
                    .filter(
                        RoomEquipmentAssociation.c.room_id == Room.id,
                        EquipmentType.parent_id == vc_id_subquery
                    )
                    .order_by(EquipmentType.name)
                    .as_scalar()
                ))
            if 'non_vc_equipment' in args:
                # noinspection PyTypeChecker
                entities.append(static_array.array(
                    db.session.query(EquipmentType.name)
                    .join(RoomEquipmentAssociation)
                    .filter(
                        RoomEquipmentAssociation.c.room_id == Room.id,
                        (EquipmentType.parent_id == None) | (EquipmentType.parent_id != vc_id_subquery)
                    )
                    .order_by(EquipmentType.name)
                    .as_scalar()
                ))

        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_pre_bookings=True, include_pending_blockings=True):
        """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.occurrences.any(and_(*reservation_criteria))
        # Check availability against blockings
        if include_pending_blockings:
            valid_states = (BlockedRoom.State.accepted, BlockedRoom.State.pending)
        else:
            valid_states = (BlockedRoom.State.accepted,)
        blocking_criteria = [BlockedRoom.blocking_id == Blocking.id,
                             BlockedRoom.state.in_(valid_states),
                             Blocking.start_date <= start_dt.date(),
                             Blocking.end_date >= end_dt.date()]
        blockings_filter = Room.blocked_rooms.any(and_(*blocking_criteria))
        return ~occurrences_filter & ~blockings_filter

    @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_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 = (
            '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())

    @unify_user_args
    def _can_be_booked(self, user, prebook=False, ignore_admin=False):
        if not user or not rb_check_user_access(user):
            return False

        if (not ignore_admin and rb_is_admin(user)) or (self.is_owned_by(user) and self.is_active):
            return True

        if self.is_active and self.is_reservable and (prebook or not self.reservations_need_confirmation):
            group_name = self.get_attribute_value('allowed-booking-group')
            if not group_name or user in GroupProxy.get_named_default_group(group_name):
                return True

        return False

    def can_be_booked(self, user, ignore_admin=False):
        """
        Reservable rooms which does not require pre-booking can be booked by anyone.
        Other rooms - only by their responsibles.
        """
        return self._can_be_booked(user, ignore_admin=ignore_admin)

    def can_be_prebooked(self, user, ignore_admin=False):
        """
        Reservable rooms can be pre-booked by anyone.
        Other rooms - only by their responsibles.
        """
        return self._can_be_booked(user, prebook=True, ignore_admin=ignore_admin)

    def can_be_overridden(self, user):
        if not user:
            return False
        return rb_is_admin(user) or self.is_owned_by(user)

    def can_be_modified(self, user):
        """Only admin can modify rooms."""
        if not user:
            return False
        return rb_is_admin(user)

    def can_be_deleted(self, user):
        return self.can_be_modified(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')
Пример #13
0
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)
Пример #14
0
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')
Пример #15
0
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')
Пример #16
0
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()