コード例 #1
0
class Reservation(Serializer, db.Model):
    __tablename__ = 'reservations'

    __api_public__ = [
        'id', ('start_dt', 'startDT'), ('end_dt', 'endDT'), 'repeat_frequency', 'repeat_interval',
        ('booked_for_name', 'bookedForName'), ('external_details_url', 'bookingUrl'), ('booking_reason', 'reason'),
        ('is_accepted', 'isConfirmed'), ('is_accepted', 'isValid'), 'is_cancelled',
        'is_rejected', ('location_name', 'location'), ('contact_email', 'booked_for_user_email')
    ]

    @declared_attr
    def __table_args__(cls):
        return (db.Index('ix_reservations_start_dt_date', cast(cls.start_dt, Date)),
                db.Index('ix_reservations_end_dt_date', cast(cls.end_dt, Date)),
                db.Index('ix_reservations_start_dt_time', cast(cls.start_dt, Time)),
                db.Index('ix_reservations_end_dt_time', cast(cls.end_dt, Time)),
                db.CheckConstraint("rejection_reason != ''", 'rejection_reason_not_empty'),
                {'schema': 'roombooking'})

    id = db.Column(
        db.Integer,
        primary_key=True
    )
    created_dt = db.Column(
        UTCDateTime,
        nullable=False,
        default=now_utc
    )
    start_dt = db.Column(
        db.DateTime,
        nullable=False,
        index=True
    )
    end_dt = db.Column(
        db.DateTime,
        nullable=False,
        index=True
    )
    repeat_frequency = db.Column(
        PyIntEnum(RepeatFrequency),
        nullable=False,
        default=RepeatFrequency.NEVER
    )  # week, month, year, etc.
    repeat_interval = db.Column(
        db.SmallInteger,
        nullable=False,
        default=0
    )  # 1, 2, 3, etc.
    booked_for_id = db.Column(
        db.Integer,
        db.ForeignKey('users.users.id'),
        index=True,
        nullable=True,
        # Must be nullable for legacy data :(
    )
    booked_for_name = db.Column(
        db.String,
        nullable=False
    )
    created_by_id = db.Column(
        db.Integer,
        db.ForeignKey('users.users.id'),
        index=True,
        nullable=True,
        # Must be nullable for legacy data :(
    )
    room_id = db.Column(
        db.Integer,
        db.ForeignKey('roombooking.rooms.id'),
        nullable=False,
        index=True
    )
    state = db.Column(
        PyIntEnum(ReservationState),
        nullable=False,
        default=ReservationState.accepted
    )
    booking_reason = db.Column(
        db.Text,
        nullable=False
    )
    rejection_reason = db.Column(
        db.String,
        nullable=True
    )
    link_id = db.Column(
        db.Integer,
        db.ForeignKey('roombooking.reservation_links.id'),
        nullable=True,
        index=True
    )
    end_notification_sent = db.Column(
        db.Boolean,
        nullable=False,
        default=False
    )

    edit_logs = db.relationship(
        'ReservationEditLog',
        backref='reservation',
        cascade='all, delete-orphan',
        lazy='dynamic'
    )
    occurrences = db.relationship(
        'ReservationOccurrence',
        backref='reservation',
        cascade='all, delete-orphan',
        lazy='dynamic'
    )
    #: The user this booking was made for.
    #: Assigning a user here also updates `booked_for_name`.
    booked_for_user = db.relationship(
        'User',
        lazy=False,
        foreign_keys=[booked_for_id],
        backref=db.backref(
            'reservations_booked_for',
            lazy='dynamic'
        )
    )
    #: The user who created this booking.
    created_by_user = db.relationship(
        'User',
        lazy=False,
        foreign_keys=[created_by_id],
        backref=db.backref(
            'reservations',
            lazy='dynamic'
        )
    )

    link = db.relationship(
        'ReservationLink',
        lazy=True,
        backref=db.backref(
            'reservation',
            uselist=False
        )
    )

    # relationship backrefs:
    # - room (Room.reservations)

    @hybrid_property
    def is_pending(self):
        return self.state == ReservationState.pending

    @hybrid_property
    def is_accepted(self):
        return self.state == ReservationState.accepted

    @hybrid_property
    def is_cancelled(self):
        return self.state == ReservationState.cancelled

    @hybrid_property
    def is_rejected(self):
        return self.state == ReservationState.rejected

    @hybrid_property
    def is_archived(self):
        return self.end_dt < datetime.now()

    @hybrid_property
    def is_repeating(self):
        return self.repeat_frequency != RepeatFrequency.NEVER

    @property
    def contact_email(self):
        return self.booked_for_user.email if self.booked_for_user else None

    @property
    def external_details_url(self):
        return url_for('rb.booking_link', booking_id=self.id, _external=True)

    @property
    def location_name(self):
        return self.room.location_name

    @property
    def repetition(self):
        return self.repeat_frequency, self.repeat_interval

    @property
    def linked_object(self):
        return self.link.object if self.link else None

    @linked_object.setter
    def linked_object(self, obj):
        assert self.link is None
        self.link = ReservationLink(object=obj)

    @property
    def event(self):
        return self.link.event if self.link else None

    @return_ascii
    def __repr__(self):
        return format_repr(self, 'id', 'room_id', 'start_dt', 'end_dt', 'state', _text=self.booking_reason)

    @classmethod
    def create_from_data(cls, room, data, user, prebook=None, ignore_admin=False):
        """Creates a new reservation.

        :param room: The Room that's being booked.
        :param data: A dict containing the booking data, usually from a :class:`NewBookingConfirmForm` instance
        :param user: The :class:`.User` who creates the booking.
        :param prebook: Instead of determining the booking type from the user's
                        permissions, always use the given mode.
        """

        populate_fields = ('start_dt', 'end_dt', 'repeat_frequency', 'repeat_interval', 'room_id', 'booking_reason')
        if data['repeat_frequency'] == RepeatFrequency.NEVER and data['start_dt'].date() != data['end_dt'].date():
            raise ValueError('end_dt != start_dt for non-repeating booking')

        if prebook is None:
            prebook = not room.can_book(user, allow_admin=(not ignore_admin))
            if prebook and not room.can_prebook(user, allow_admin=(not ignore_admin)):
                raise NoReportError(u'You cannot book this room')

        room.check_advance_days(data['end_dt'].date(), user)
        room.check_bookable_hours(data['start_dt'].time(), data['end_dt'].time(), user)

        reservation = cls()
        for field in populate_fields:
            if field in data:
                setattr(reservation, field, data[field])
        reservation.room = room
        reservation.booked_for_user = data.get('booked_for_user') or user
        reservation.booked_for_name = reservation.booked_for_user.full_name
        reservation.state = ReservationState.pending if prebook else ReservationState.accepted
        reservation.created_by_user = user
        reservation.create_occurrences(True)
        if not any(occ.is_valid for occ in reservation.occurrences):
            raise NoReportError(_(u'Reservation has no valid occurrences'))
        db.session.flush()
        signals.rb.booking_created.send(reservation)
        notify_creation(reservation)
        return reservation

    @staticmethod
    def get_with_data(*args, **kwargs):
        filters = kwargs.pop('filters', None)
        limit = kwargs.pop('limit', None)
        offset = kwargs.pop('offset', 0)
        order = kwargs.pop('order', Reservation.start_dt)
        limit_per_room = kwargs.pop('limit_per_room', False)
        occurs_on = kwargs.pop('occurs_on')
        if kwargs:
            raise ValueError('Unexpected kwargs: {}'.format(kwargs))

        query = Reservation.query.options(joinedload(Reservation.room))
        if filters:
            query = query.filter(*filters)
        if occurs_on:
            query = query.filter(
                Reservation.id.in_(db.session.query(ReservationOccurrence.reservation_id)
                                   .filter(ReservationOccurrence.date.in_(occurs_on),
                                           ReservationOccurrence.is_valid))
            )
        if limit_per_room and (limit or offset):
            query = limit_groups(query, Reservation, Reservation.room_id, order, limit, offset)

        query = query.order_by(order, Reservation.created_dt)

        if not limit_per_room:
            if limit:
                query = query.limit(limit)
            if offset:
                query = query.offset(offset)

        result = OrderedDict((r.id, {'reservation': r}) for r in query)

        if 'occurrences' in args:
            occurrence_data = OrderedMultiDict(db.session.query(ReservationOccurrence.reservation_id,
                                                                ReservationOccurrence)
                                               .filter(ReservationOccurrence.reservation_id.in_(result.iterkeys()))
                                               .order_by(ReservationOccurrence.start_dt))
            for id_, data in result.iteritems():
                data['occurrences'] = occurrence_data.getlist(id_)

        return result.values()

    @staticmethod
    def find_overlapping_with(room, occurrences, skip_reservation_id=None):
        return Reservation.find(Reservation.room == room,
                                Reservation.id != skip_reservation_id,
                                ReservationOccurrence.is_valid,
                                ReservationOccurrence.filter_overlap(occurrences),
                                _join=ReservationOccurrence)

    def accept(self, user):
        self.state = ReservationState.accepted
        self.add_edit_log(ReservationEditLog(user_name=user.full_name, info=['Reservation accepted']))
        notify_confirmation(self)
        signals.rb.booking_state_changed.send(self)
        valid_occurrences = self.occurrences.filter(ReservationOccurrence.is_valid).all()
        pre_occurrences = ReservationOccurrence.find_overlapping_with(self.room, valid_occurrences, self.id).all()
        for occurrence in pre_occurrences:
            if not occurrence.is_valid:
                continue
            occurrence.reject(user, u'Rejected due to collision with a confirmed reservation')

    def reset_approval(self, user):
        self.state = ReservationState.pending
        notify_reset_approval(self)
        self.add_edit_log(ReservationEditLog(user_name=user.full_name, info=['Requiring new approval due to change']))

    def cancel(self, user, reason=None, silent=False):
        self.state = ReservationState.cancelled
        self.rejection_reason = reason or None
        criteria = (ReservationOccurrence.is_valid, ReservationOccurrence.is_within_cancel_grace_period)
        self.occurrences.filter(*criteria).update({
            ReservationOccurrence.state: ReservationOccurrenceState.cancelled,
            ReservationOccurrence.rejection_reason: reason
        }, synchronize_session='fetch')
        signals.rb.booking_state_changed.send(self)
        if not silent:
            notify_cancellation(self)
            log_msg = u'Reservation cancelled: {}'.format(reason) if reason else 'Reservation cancelled'
            self.add_edit_log(ReservationEditLog(user_name=user.full_name, info=[log_msg]))

    def reject(self, user, reason, silent=False):
        self.state = ReservationState.rejected
        self.rejection_reason = reason or None
        self.occurrences.filter_by(is_valid=True).update({
            ReservationOccurrence.state: ReservationOccurrenceState.rejected,
            ReservationOccurrence.rejection_reason: reason
        }, synchronize_session='fetch')
        signals.rb.booking_state_changed.send(self)
        if not silent:
            notify_rejection(self)
            log_msg = u'Reservation rejected: {}'.format(reason)
            self.add_edit_log(ReservationEditLog(user_name=user.full_name, info=[log_msg]))

    def add_edit_log(self, edit_log):
        self.edit_logs.append(edit_log)
        db.session.flush()

    def can_accept(self, user, allow_admin=True):
        if user is None:
            return False
        return self.is_pending and self.room.can_moderate(user, allow_admin=allow_admin)

    def can_reject(self, user, allow_admin=True):
        if user is None:
            return False
        if self.is_rejected or self.is_cancelled:
            return False
        return self.room.can_moderate(user, allow_admin=allow_admin)

    def can_cancel(self, user, allow_admin=True):
        if user is None:
            return False
        if self.is_rejected or self.is_cancelled or self.is_archived:
            return False

        is_booked_or_owned_by_user = self.is_owned_by(user) or self.is_booked_for(user)
        return is_booked_or_owned_by_user or (allow_admin and rb_is_admin(user))

    def can_edit(self, user, allow_admin=True):
        if user is None:
            return False
        if self.is_rejected or self.is_cancelled:
            return False
        if self.is_archived and not (allow_admin and rb_is_admin(user)):
            return False
        return self.is_owned_by(user) or self.is_booked_for(user) or self.room.can_manage(user, allow_admin=allow_admin)

    def can_delete(self, user, allow_admin=True):
        if user is None:
            return False
        return allow_admin and rb_is_admin(user) and (self.is_cancelled or self.is_rejected)

    def create_occurrences(self, skip_conflicts, user=None):
        ReservationOccurrence.create_series_for_reservation(self)
        db.session.flush()

        if user is None:
            user = self.created_by_user

        # Check for conflicts with nonbookable periods
        if not rb_is_admin(user) and not self.room.can_manage(user, permission='override'):
            nonbookable_periods = self.room.nonbookable_periods.filter(NonBookablePeriod.end_dt > self.start_dt)
            for occurrence in self.occurrences:
                if not occurrence.is_valid:
                    continue
                for nbd in nonbookable_periods:
                    if nbd.overlaps(occurrence.start_dt, occurrence.end_dt):
                        if not skip_conflicts:
                            raise ConflictingOccurrences()
                        occurrence.cancel(user, u'Skipped due to nonbookable date', silent=True, propagate=False)
                        break

        # Check for conflicts with blockings
        blocked_rooms = self.room.get_blocked_rooms(*(occurrence.start_dt for occurrence in self.occurrences))
        for br in blocked_rooms:
            blocking = br.blocking
            if blocking.can_override(user, room=self.room):
                continue
            for occurrence in self.occurrences:
                if occurrence.is_valid and blocking.is_active_at(occurrence.start_dt.date()):
                    # Cancel OUR occurrence
                    msg = u'Skipped due to collision with a blocking ({})'
                    occurrence.cancel(user, msg.format(blocking.reason), silent=True, propagate=False)

        # Check for conflicts with other occurrences
        conflicting_occurrences = self.get_conflicting_occurrences()
        for occurrence, conflicts in conflicting_occurrences.iteritems():
            if not occurrence.is_valid:
                continue
            if conflicts['confirmed']:
                if not skip_conflicts:
                    raise ConflictingOccurrences()
                # Cancel OUR occurrence
                msg = u'Skipped due to collision with {} reservation(s)'
                occurrence.cancel(user, msg.format(len(conflicts['confirmed'])), silent=True, propagate=False)
            elif conflicts['pending'] and self.is_accepted:
                # Reject OTHER occurrences
                for conflict in conflicts['pending']:
                    conflict.reject(user, u'Rejected due to collision with a confirmed reservation')

    def find_excluded_days(self):
        return self.occurrences.filter(~ReservationOccurrence.is_valid)

    def find_overlapping(self):
        occurrences = self.occurrences.filter(ReservationOccurrence.is_valid).all()
        return Reservation.find_overlapping_with(self.room, occurrences, self.id)

    def get_conflicting_occurrences(self):
        valid_occurrences = self.occurrences.filter(ReservationOccurrence.is_valid).all()
        colliding_occurrences = ReservationOccurrence.find_overlapping_with(self.room, valid_occurrences, self.id).all()
        conflicts = defaultdict(lambda: dict(confirmed=[], pending=[]))
        for occurrence in valid_occurrences:
            for colliding in colliding_occurrences:
                if occurrence.overlaps(colliding):
                    key = 'confirmed' if colliding.reservation.is_accepted else 'pending'
                    conflicts[occurrence][key].append(colliding)
        return conflicts

    def is_booked_for(self, user):
        return user is not None and self.booked_for_user == user

    def is_owned_by(self, user):
        return self.created_by_user == user

    def modify(self, data, user):
        """Modifies an existing reservation.

        :param data: A dict containing the booking data, usually from a :class:`ModifyBookingForm` instance
        :param user: The :class:`.User` who modifies the booking.
        """

        populate_fields = ('start_dt', 'end_dt', 'repeat_frequency', 'repeat_interval', 'booked_for_user',
                           'booking_reason')
        # fields affecting occurrences
        occurrence_fields = {'start_dt', 'end_dt', 'repeat_frequency', 'repeat_interval'}
        # fields where date and time are compared separately
        date_time_fields = {'start_dt', 'end_dt'}
        # fields for the repetition
        repetition_fields = {'repeat_frequency', 'repeat_interval'}
        # pretty names for logging
        field_names = {
            'start_dt/date': u"start date",
            'end_dt/date': u"end date",
            'start_dt/time': u"start time",
            'end_dt/time': u"end time",
            'repetition': u"booking type",
            'booked_for_user': u"'Booked for' user",
            'booking_reason': u"booking reason",
        }

        self.room.check_advance_days(data['end_dt'].date(), user)
        self.room.check_bookable_hours(data['start_dt'].time(), data['end_dt'].time(), user)

        changes = {}
        update_occurrences = False
        old_repetition = self.repetition

        for field in populate_fields:
            if field not in data:
                continue
            old = getattr(self, field)
            new = data[field]
            converter = unicode
            if old != new:
                # Booked for user updates the (redundant) name
                if field == 'booked_for_user':
                    old = self.booked_for_name
                    new = self.booked_for_name = data[field].full_name
                # Apply the change
                setattr(self, field, data[field])
                # If any occurrence-related field changed we need to recreate the occurrences
                if field in occurrence_fields:
                    update_occurrences = True
                # Record change for history entry
                if field in date_time_fields:
                    # The date/time fields create separate entries for the date and time parts
                    if old.date() != new.date():
                        changes[field + '/date'] = {'old': old.date(), 'new': new.date(), 'converter': format_date}
                    if old.time() != new.time():
                        changes[field + '/time'] = {'old': old.time(), 'new': new.time(), 'converter': format_time}
                elif field in repetition_fields:
                    # Repetition needs special handling since it consists of two fields but they are tied together
                    # We simply update it whenever we encounter such a change; after the last change we end up with
                    # the correct change data
                    changes['repetition'] = {'old': old_repetition,
                                             'new': self.repetition,
                                             'converter': lambda x: RepeatMapping.get_message(*x)}
                else:
                    changes[field] = {'old': old, 'new': new, 'converter': converter}

        if not changes:
            return False

        # Create a verbose log entry for the modification
        log = [u'Booking modified']
        for field, change in changes.iteritems():
            field_title = field_names.get(field, field)
            converter = change['converter']
            old = to_unicode(converter(change['old']))
            new = to_unicode(converter(change['new']))
            if not old:
                log.append(u"The {} was set to '{}'".format(field_title, new))
            elif not new:
                log.append(u"The {} was cleared".format(field_title))
            else:
                log.append(u"The {} was changed from '{}' to '{}'".format(field_title, old, new))

        self.edit_logs.append(ReservationEditLog(user_name=user.full_name, info=log))

        # Recreate all occurrences if necessary
        if update_occurrences:
            cols = [col.name for col in ReservationOccurrence.__table__.columns
                    if not col.primary_key and col.name not in {'start_dt', 'end_dt'}]

            old_occurrences = {occ.date: occ for occ in self.occurrences}
            self.occurrences.delete(synchronize_session='fetch')
            self.create_occurrences(True, user)
            db.session.flush()
            # Restore rejection data etc. for recreated occurrences
            for occurrence in self.occurrences:
                old_occurrence = old_occurrences.get(occurrence.date)
                # Copy data from old occurrence UNLESS the new one is invalid (e.g. because of collisions)
                # Otherwise we'd end up with valid occurrences ignoring collisions!
                if old_occurrence and occurrence.is_valid:
                    for col in cols:
                        setattr(occurrence, col, getattr(old_occurrence, col))
            # Don't cause new notifications for the entire booking in case of daily repetition
            if self.repeat_frequency == RepeatFrequency.DAY and all(occ.notification_sent
                                                                    for occ in old_occurrences.itervalues()):
                for occurrence in self.occurrences:
                    occurrence.notification_sent = True

        # Sanity check so we don't end up with an "empty" booking
        if not any(occ.is_valid for occ in self.occurrences):
            raise NoReportError(_(u'Reservation has no valid occurrences'))

        notify_modification(self, changes)
        return True
コード例 #2
0
class Reservation(Serializer, db.Model):
    __tablename__ = 'reservations'
    __public__ = []
    __calendar_public__ = [
        'id', ('booked_for_name', 'bookedForName'),
        ('booking_reason', 'reason'), ('details_url', 'bookingUrl')
    ]
    __api_public__ = [
        'id', ('start_dt', 'startDT'), ('end_dt', 'endDT'),
        'repeat_frequency', 'repeat_interval',
        ('booked_for_name', 'bookedForName'), ('details_url', 'bookingUrl'),
        ('booking_reason', 'reason'), ('uses_vc', 'usesAVC'),
        ('needs_vc_assistance', 'needsAVCSupport'), 'needs_assistance',
        ('is_accepted', 'isConfirmed'),
        ('is_valid', 'isValid'), 'is_cancelled', 'is_rejected',
        ('location_name', 'location'), 'booked_for_user_email'
    ]

    @declared_attr
    def __table_args__(cls):
        return (db.Index('ix_reservations_start_dt_date',
                         cast(cls.start_dt, Date)),
                db.Index('ix_reservations_end_dt_date', cast(cls.end_dt,
                                                             Date)),
                db.Index('ix_reservations_start_dt_time',
                         cast(cls.start_dt, Time)),
                db.Index('ix_reservations_end_dt_time',
                         cast(cls.end_dt, Time)), {
                             'schema': 'roombooking'
                         })

    id = db.Column(db.Integer, primary_key=True)
    created_dt = db.Column(UTCDateTime, nullable=False, default=now_utc)
    start_dt = db.Column(db.DateTime, nullable=False, index=True)
    end_dt = db.Column(db.DateTime, nullable=False, index=True)
    repeat_frequency = db.Column(
        PyIntEnum(RepeatFrequency),
        nullable=False,
        default=RepeatFrequency.NEVER)  # week, month, year, etc.
    repeat_interval = db.Column(db.SmallInteger, nullable=False,
                                default=0)  # 1, 2, 3, etc.
    booked_for_id = db.Column(db.String
                              # Must be nullable for legacy data :(
                              )
    booked_for_name = db.Column(db.String, nullable=False)
    created_by_id = db.Column(db.String
                              # Must be nullable for legacy data :(
                              )
    room_id = db.Column(db.Integer,
                        db.ForeignKey('roombooking.rooms.id'),
                        nullable=False,
                        index=True)
    contact_email = db.Column(db.String, nullable=False, default='')
    contact_phone = db.Column(db.String, nullable=False, default='')
    is_accepted = db.Column(db.Boolean, nullable=False)
    is_cancelled = db.Column(db.Boolean, nullable=False, default=False)
    is_rejected = db.Column(db.Boolean, nullable=False, default=False)
    booking_reason = db.Column(db.Text, nullable=False)
    rejection_reason = db.Column(db.String)
    uses_vc = db.Column(db.Boolean, nullable=False, default=False)
    needs_vc_assistance = db.Column(db.Boolean, nullable=False, default=False)
    needs_assistance = db.Column(db.Boolean, nullable=False, default=False)
    event_id = db.Column(db.Integer, index=True)

    edit_logs = db.relationship('ReservationEditLog',
                                backref='reservation',
                                cascade='all, delete-orphan',
                                lazy='dynamic')
    occurrences = db.relationship('ReservationOccurrence',
                                  backref='reservation',
                                  cascade='all, delete-orphan',
                                  lazy='dynamic')
    used_equipment = db.relationship('EquipmentType',
                                     secondary=ReservationEquipmentAssociation,
                                     backref='reservations',
                                     lazy='dynamic')

    @hybrid_property
    def is_archived(self):
        return self.end_dt < datetime.now()

    @hybrid_property
    def is_pending(self):
        return not (self.is_accepted or self.is_rejected or self.is_cancelled)

    @is_pending.expression
    def is_pending(self):
        return ~(Reservation.is_accepted | Reservation.is_rejected
                 | Reservation.is_cancelled)

    @hybrid_property
    def is_repeating(self):
        return self.repeat_frequency != RepeatFrequency.NEVER

    @hybrid_property
    def is_valid(self):
        return self.is_accepted and not (self.is_rejected or self.is_cancelled)

    @is_valid.expression
    def is_valid(self):
        return self.is_accepted & ~(self.is_rejected | self.is_cancelled)

    @property
    def booked_for_user(self):
        return AvatarHolder().getById(
            self.booked_for_id) if self.booked_for_id else None

    @booked_for_user.setter
    def booked_for_user(self, user):
        self.booked_for_id = user.getId()
        self.booked_for_name = user.getFullName()

    @property
    def booked_for_user_email(self):
        user = self.booked_for_user
        return self.booked_for_user.getEmail() if user else None

    @property
    def contact_emails(self):
        return set(
            filter(None, map(unicode.strip, self.contact_email.split(u','))))

    @property
    def created_by_user(self):
        return AvatarHolder().getById(
            self.created_by_id) if self.created_by_id else None

    @created_by_user.setter
    def created_by_user(self, user):
        self.created_by_id = user.getId()

    @property
    def details_url(self):
        return url_for('rooms.roomBooking-bookingDetails',
                       self,
                       _external=True)

    @property
    def event(self):
        from MaKaC.conference import ConferenceHolder
        return ConferenceHolder().getById(str(self.event_id))

    @event.setter
    def event(self, event):
        self.event_id = int(event.getId()) if event else None

    @property
    def location_name(self):
        return self.room.location_name

    @property
    def repetition(self):
        return self.repeat_frequency, self.repeat_interval

    @property
    def status_string(self):
        parts = []
        if self.is_valid:
            parts.append(_(u"Valid"))
        else:
            if self.is_cancelled:
                parts.append(_(u"Cancelled"))
            if self.is_rejected:
                parts.append(_(u"Rejected"))
            if not self.is_accepted:
                parts.append(_(u"Not confirmed"))
        if self.is_archived:
            parts.append(_(u"Archived"))
        else:
            parts.append(_(u"Live"))
        return u', '.join(map(unicode, parts))

    @return_ascii
    def __repr__(self):
        return u'<Reservation({0}, {1}, {2}, {3}, {4})>'.format(
            self.id, self.room_id, self.booked_for_name, self.start_dt,
            self.end_dt)

    @classmethod
    def create_from_data(cls, room, data, user, prebook=None):
        """Creates a new reservation.

        :param room: The Room that's being booked.
        :param data: A dict containing the booking data, usually from a :class:`NewBookingConfirmForm` instance
        :param user: The :class:`Avatar` who creates the booking.
        :param prebook: Instead of determining the booking type from the user's
                        permissions, always use the given mode.
        """

        populate_fields = ('start_dt', 'end_dt', 'repeat_frequency',
                           'repeat_interval', 'room_id', 'booked_for_id',
                           'contact_email', 'contact_phone', 'booking_reason',
                           'used_equipment', 'needs_assistance', 'uses_vc',
                           'needs_vc_assistance')

        if data['repeat_frequency'] == RepeatFrequency.NEVER and data[
                'start_dt'].date() != data['end_dt'].date():
            raise ValueError('end_dt != start_dt for non-repeating booking')

        if prebook is None:
            prebook = not room.can_be_booked(user)
            if prebook and not room.can_be_prebooked(user):
                raise NoReportError('You cannot book this room')

        room.check_advance_days(data['end_dt'].date(), user)
        room.check_bookable_hours(data['start_dt'].time(),
                                  data['end_dt'].time(), user)

        reservation = cls()
        for field in populate_fields:
            if field in data:
                setattr(reservation, field, data[field])
        reservation.room = room
        reservation.booked_for_name = reservation.booked_for_user.getFullName()
        reservation.is_accepted = not prebook
        reservation.created_by_user = user
        reservation.create_occurrences(True)
        if not any(occ.is_valid for occ in reservation.occurrences):
            raise NoReportError(_('Reservation has no valid occurrences'))
        notify_creation(reservation)
        return reservation

    @staticmethod
    def get_with_data(*args, **kwargs):
        filters = kwargs.pop('filters', None)
        limit = kwargs.pop('limit', None)
        offset = kwargs.pop('offset', 0)
        order = kwargs.pop('order', Reservation.start_dt)
        limit_per_room = kwargs.pop('limit_per_room', False)
        occurs_on = kwargs.pop('occurs_on')
        if kwargs:
            raise ValueError('Unexpected kwargs: {}'.format(kwargs))

        query = Reservation.query.options(joinedload(Reservation.room))
        if filters:
            query = query.filter(*filters)
        if occurs_on:
            query = query.filter(
                Reservation.id.in_(
                    db.session.query(
                        ReservationOccurrence.reservation_id).filter(
                            ReservationOccurrence.date.in_(occurs_on),
                            ReservationOccurrence.is_valid)))
        if limit_per_room and (limit or offset):
            query = limit_groups(query, Reservation, Reservation.room_id,
                                 order, limit, offset)

        query = query.order_by(order, Reservation.created_dt)

        if not limit_per_room:
            if limit:
                query = query.limit(limit)
            if offset:
                query = query.offset(offset)

        result = OrderedDict((r.id, {'reservation': r}) for r in query)

        if 'vc_equipment' in args:
            vc_id_subquery = db.session.query(EquipmentType.id) \
                .correlate(Reservation) \
                .filter_by(name='Video conference') \
                .join(RoomEquipmentAssociation) \
                .filter(RoomEquipmentAssociation.c.room_id == Reservation.room_id) \
                .as_scalar()

            # noinspection PyTypeChecker
            vc_equipment_data = dict(
                db.session.query(
                    Reservation.id,
                    static_array.array_agg(EquipmentType.name)).join(
                        ReservationEquipmentAssociation, EquipmentType).filter(
                            Reservation.id.in_(result.iterkeys())).filter(
                                EquipmentType.parent_id ==
                                vc_id_subquery).group_by(Reservation.id))

            for id_, data in result.iteritems():
                data['vc_equipment'] = vc_equipment_data.get(id_, ())

        if 'occurrences' in args:
            occurrence_data = OrderedMultiDict(
                db.session.query(ReservationOccurrence.reservation_id,
                                 ReservationOccurrence).filter(
                                     ReservationOccurrence.reservation_id.in_(
                                         result.iterkeys())).order_by(
                                             ReservationOccurrence.start_dt))
            for id_, data in result.iteritems():
                data['occurrences'] = occurrence_data.getlist(id_)

        return result.values()

    @staticmethod
    def find_overlapping_with(room, occurrences, skip_reservation_id=None):
        return Reservation.find(
            Reservation.room == room,
            Reservation.id != skip_reservation_id,
            ReservationOccurrence.is_valid,
            ReservationOccurrence.filter_overlap(occurrences),
            _join=ReservationOccurrence)

    def accept(self, user):
        self.is_accepted = True
        self.add_edit_log(
            ReservationEditLog(user_name=user.getFullName(),
                               info=['Reservation accepted']))
        notify_confirmation(self)

        valid_occurrences = self.occurrences.filter(
            ReservationOccurrence.is_valid).all()
        pre_occurrences = ReservationOccurrence.find_overlapping_with(
            self.room, valid_occurrences, self.id).all()
        for occurrence in pre_occurrences:
            if not occurrence.is_valid:
                continue
            occurrence.reject(
                user,
                u'Rejected due to collision with a confirmed reservation')

    def cancel(self, user, reason=None, silent=False):
        self.is_cancelled = True
        self.rejection_reason = reason
        self.occurrences.filter_by(is_valid=True).update(
            {
                'is_cancelled': True,
                'rejection_reason': reason
            },
            synchronize_session='fetch')
        if not silent:
            notify_cancellation(self)
            log_msg = u'Reservation cancelled: {}'.format(
                reason) if reason else 'Reservation cancelled'
            self.add_edit_log(
                ReservationEditLog(user_name=user.getFullName(),
                                   info=[log_msg]))

    def reject(self, user, reason, silent=False):
        self.is_rejected = True
        self.rejection_reason = reason
        self.occurrences.filter_by(is_valid=True).update(
            {
                'is_rejected': True,
                'rejection_reason': reason
            },
            synchronize_session='fetch')
        if not silent:
            notify_rejection(self)
            log_msg = u'Reservation rejected: {}'.format(reason)
            self.add_edit_log(
                ReservationEditLog(user_name=user.getFullName(),
                                   info=[log_msg]))

    def add_edit_log(self, edit_log):
        self.edit_logs.append(edit_log)
        db.session.flush()

    def can_be_accepted(self, user):
        if user is None:
            return False
        return user.isRBAdmin() or self.room.is_owned_by(user)

    def can_be_cancelled(self, user):
        if user is None:
            return False
        return self.is_owned_by(
            user) or user.isRBAdmin() or self.is_booked_for(user)

    def can_be_deleted(self, user):
        if user is None:
            return False
        return user.isRBAdmin()

    def can_be_modified(self, user):
        if user is None:
            return False
        if self.is_rejected or self.is_cancelled:
            return False
        if user.isRBAdmin():
            return True
        return self.created_by_user == user or self.is_booked_for(
            user) or self.room.is_owned_by(user)

    def can_be_rejected(self, user):
        if user is None:
            return False
        return user.isRBAdmin() or self.room.is_owned_by(user)

    def create_occurrences(self, skip_conflicts, user=None):
        ReservationOccurrence.create_series_for_reservation(self)
        db.session.flush()

        if user is None:
            user = self.created_by_user

        # Check for conflicts with nonbookable periods
        if not user.isRBAdmin() and not self.room.is_owned_by(user):
            nonbookable_periods = self.room.nonbookable_periods.filter(
                NonBookablePeriod.end_dt > self.start_dt)
            for occurrence in self.occurrences:
                if not occurrence.is_valid:
                    continue
                for nbd in nonbookable_periods:
                    if nbd.overlaps(occurrence.start_dt, occurrence.end_dt):
                        if not skip_conflicts:
                            raise ConflictingOccurrences()
                        occurrence.cancel(user,
                                          u'Skipped due to nonbookable date',
                                          silent=True,
                                          propagate=False)
                        break

        # Check for conflicts with blockings
        blocked_rooms = self.room.get_blocked_rooms(
            *(occurrence.start_dt for occurrence in self.occurrences))
        for br in blocked_rooms:
            blocking = br.blocking
            if blocking.can_be_overridden(user, self.room):
                continue
            for occurrence in self.occurrences:
                if occurrence.is_valid and blocking.is_active_at(
                        occurrence.start_dt.date()):
                    # Cancel OUR occurrence
                    msg = u'Skipped due to collision with a blocking ({})'
                    occurrence.cancel(user,
                                      msg.format(blocking.reason),
                                      silent=True,
                                      propagate=False)

        # Check for conflicts with other occurrences
        conflicting_occurrences = self.get_conflicting_occurrences()
        for occurrence, conflicts in conflicting_occurrences.iteritems():
            if not occurrence.is_valid:
                continue
            if conflicts['confirmed']:
                if not skip_conflicts:
                    raise ConflictingOccurrences()
                # Cancel OUR occurrence
                msg = u'Skipped due to collision with {} reservation(s)'
                occurrence.cancel(user,
                                  msg.format(len(conflicts['confirmed'])),
                                  silent=True,
                                  propagate=False)
            elif conflicts['pending'] and self.is_accepted:
                # Reject OTHER occurrences
                for conflict in conflicts['pending']:
                    conflict.reject(
                        user,
                        u'Rejected due to collision with a confirmed reservation'
                    )

        # Mark occurrences created within the notification window as notified
        for occurrence in self.occurrences:
            if occurrence.is_valid and occurrence.is_in_notification_window():
                occurrence.notification_sent = True

        # Mark occurrences created within the digest window as notified
        if self.repeat_frequency == RepeatFrequency.WEEK:
            if self.room.is_in_digest_window():
                digest_start = round_up_month(date.today())
            else:
                digest_start = date.today()
            digest_end = get_month_end(digest_start)
            self.occurrences.filter(
                ReservationOccurrence.start_dt <= digest_end).update(
                    {'notification_sent': True})

    def find_excluded_days(self):
        return self.occurrences.filter(~ReservationOccurrence.is_valid)

    def find_overlapping(self):
        occurrences = self.occurrences.filter(
            ReservationOccurrence.is_valid).all()
        return Reservation.find_overlapping_with(self.room, occurrences,
                                                 self.id)

    def getLocator(self):
        locator = Locator()
        locator['roomLocation'] = self.location_name
        locator['resvID'] = self.id
        return locator

    def get_conflicting_occurrences(self):
        valid_occurrences = self.occurrences.filter(
            ReservationOccurrence.is_valid).all()
        colliding_occurrences = ReservationOccurrence.find_overlapping_with(
            self.room, valid_occurrences, self.id).all()
        conflicts = defaultdict(lambda: dict(confirmed=[], pending=[]))
        for occurrence in valid_occurrences:
            for colliding in colliding_occurrences:
                if occurrence.overlaps(colliding):
                    key = 'confirmed' if colliding.reservation.is_accepted else 'pending'
                    conflicts[occurrence][key].append(colliding)
        return conflicts

    def get_vc_equipment(self):
        vc_equipment = self.room.available_equipment \
                           .correlate(ReservationOccurrence) \
                           .with_entities(EquipmentType.id) \
                           .filter_by(name='Video conference') \
                           .as_scalar()
        return self.used_equipment.filter(
            EquipmentType.parent_id == vc_equipment)

    def is_booked_for(self, user):
        if user is None:
            return False
        return self.booked_for_user == user or bool(
            set(self.contact_emails) & set(user.getEmails()))

    def is_owned_by(self, avatar):
        return self.created_by_id == avatar.id

    def modify(self, data, user):
        """Modifies an existing reservation.

        :param data: A dict containing the booking data, usually from a :class:`ModifyBookingForm` instance
        :param user: The :class:`Avatar` who modifies the booking.
        """

        populate_fields = ('start_dt', 'end_dt', 'repeat_frequency',
                           'repeat_interval', 'booked_for_id', 'contact_email',
                           'contact_phone', 'booking_reason', 'used_equipment',
                           'needs_assistance', 'uses_vc',
                           'needs_vc_assistance')
        # fields affecting occurrences
        occurrence_fields = {
            'start_dt', 'end_dt', 'repeat_frequency', 'repeat_interval'
        }
        # fields where date and time are compared separately
        date_time_fields = {'start_dt', 'end_dt'}
        # fields for the repetition
        repetition_fields = {'repeat_frequency', 'repeat_interval'}
        # pretty names for logging
        field_names = {
            'start_dt/date': "start date",
            'end_dt/date': "end date",
            'start_dt/time': "start time",
            'end_dt/time': "end time",
            'repetition': "booking type",
            'booked_for_id': "'Booked for' user",
            'contact_email': "contact email",
            'contact_phone': "contact phone number",
            'booking_reason': "booking reason",
            'used_equipment': "list of equipment",
            'needs_assistance': "option 'General Assistance'",
            'uses_vc': "option 'Uses Videoconference'",
            'needs_vc_assistance': "option 'Videoconference Setup Assistance'"
        }

        self.room.check_advance_days(data['end_dt'].date(), user)
        self.room.check_bookable_hours(data['start_dt'].time(),
                                       data['end_dt'].time(), user)

        changes = {}
        update_occurrences = False
        old_repetition = self.repetition

        for field in populate_fields:
            if field not in data:
                continue
            old = getattr(self, field)
            new = data[field]
            converter = unicode
            if field == 'used_equipment':
                # Dynamic relationship
                old = sorted(old.all())
                converter = lambda x: u', '.join(x.name for x in x)
            if old != new:
                # Apply the change
                setattr(self, field, data[field])
                # Booked for id updates also update the (redundant) name
                if field == 'booked_for_id':
                    old = self.booked_for_name
                    new = self.booked_for_name = self.booked_for_user.getFullName(
                    ).decode('utf-8')
                # If any occurrence-related field changed we need to recreate the occurrences
                if field in occurrence_fields:
                    update_occurrences = True
                # Record change for history entry
                if field in date_time_fields:
                    # The date/time fields create separate entries for the date and time parts
                    if old.date() != new.date():
                        changes[field + '/date'] = {
                            'old': old.date(),
                            'new': new.date(),
                            'converter': format_date
                        }
                    if old.time() != new.time():
                        changes[field + '/time'] = {
                            'old': old.time(),
                            'new': new.time(),
                            'converter': format_time
                        }
                elif field in repetition_fields:
                    # Repetition needs special handling since it consists of two fields but they are tied together
                    # We simply update it whenever we encounter such a change; after the last change we end up with
                    # the correct change data
                    changes['repetition'] = {
                        'old': old_repetition,
                        'new': self.repetition,
                        'converter': lambda x: RepeatMapping.get_message(*x)
                    }
                else:
                    changes[field] = {
                        'old': old,
                        'new': new,
                        'converter': converter
                    }

        if not changes:
            return False

        # Create a verbose log entry for the modification
        log = [u'Booking modified']
        for field, change in changes.iteritems():
            field_title = field_names.get(field, field)
            converter = change['converter']
            old = converter(change['old'])
            new = converter(change['new'])
            if not old:
                log.append(u"The {} was set to '{}'".format(field_title, new))
            elif not new:
                log.append(u"The {} was cleared".format(field_title))
            else:
                log.append(u"The {} was changed from '{}' to '{}'".format(
                    field_title, old, new))

        self.edit_logs.append(
            ReservationEditLog(user_name=user.getFullName(), info=log))

        # Recreate all occurrences if necessary
        if update_occurrences:
            cols = [
                col.name for col in ReservationOccurrence.__table__.columns if
                not col.primary_key and col.name not in {'start_dt', 'end_dt'}
            ]

            old_occurrences = {occ.date: occ for occ in self.occurrences}
            self.occurrences.delete(synchronize_session='fetch')
            self.create_occurrences(True, user)
            db.session.flush()
            # Restore rejection data etc. for recreated occurrences
            for occurrence in self.occurrences:
                old_occurrence = old_occurrences.get(occurrence.date)
                # Copy data from old occurrence UNLESS the new one is invalid (e.g. because of collisions)
                # Otherwise we'd end up with valid occurrences ignoring collisions!
                if old_occurrence and occurrence.is_valid:
                    for col in cols:
                        setattr(occurrence, col, getattr(old_occurrence, col))
            # Don't cause new notifications for the entire booking in case of daily repetition
            if self.repeat_frequency == RepeatFrequency.DAY and all(
                    occ.notification_sent
                    for occ in old_occurrences.itervalues()):
                for occurrence in self.occurrences:
                    occurrence.notification_sent = True

        # Sanity check so we don't end up with an "empty" booking
        if not any(occ.is_valid for occ in self.occurrences):
            raise NoReportError(_('Reservation has no valid occurrences'))

        notify_modification(self, changes)
        return True