Ejemplo n.º 1
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
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
Ejemplo n.º 3
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 fossir.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 fossir.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_role(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)))
Ejemplo n.º 4
0
 def __auto_table_args(cls):
     return (db.UniqueConstraint('person_id',
                                 *cls.person_link_unique_columns), )
Ejemplo n.º 5
0
class PaperReview(ProposalReviewMixin, RenderModeMixin, db.Model):
    """Represents 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 fossir.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 ratings:
            return None
        return sum(x.value for x in ratings) / len(ratings)
Ejemplo n.º 6
0
 def __auto_table_args():
     return db.UniqueConstraint('module', 'name'),
Ejemplo n.º 7
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 fossir.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 fossir.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')
Ejemplo n.º 8
0
class AbstractReview(ProposalReviewMixin, RenderModeMixin, db.Model):
    """Represents 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
        ]
        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)