def test_overlaps(overlapping_combination_from_2am_to_4am):
    start_hour, end_hour, expected = overlapping_combination_from_2am_to_4am()
    occ1 = ReservationOccurrence(start_dt=date.today() + relativedelta(hour=2),
                                 end_dt=date.today() + relativedelta(hour=4))
    occ2 = ReservationOccurrence(start_dt=date.today() + relativedelta(hour=start_hour),
                                 end_dt=date.today() + relativedelta(hour=end_hour))
    assert occ1.overlaps(occ2) == expected
def test_create_series(creation_params):
    for occ1, occ2 in izip(
        list(ReservationOccurrence.iter_create_occurrences(**creation_params)),
        ReservationOccurrence.create_series(**creation_params),
    ):
        assert occ1.start_dt == occ2.start_dt
        assert occ1.end_dt == occ2.end_dt
Example #3
0
 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.query
                           .join(ReservationOccurrence.reservation)
                           .filter(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 = [Room.id == BlockedRoom.room_id,
                          BlockedRoom.state.in_(valid_states),
                          db_dates_overlap(Blocking, 'start_date', end_dt.date(), 'end_date', start_dt.date(),
                                           inclusive=True)]
     blockings_filter = (BlockedRoom.query
                         .join(Blocking.blocked_rooms)
                         .filter(and_(*blocking_criteria)))
     return ~occurrences_filter.exists() & ~blockings_filter.exists()
Example #4
0
    def _get_all_occurrences(self, room_ids, form, flexible_days=0, reservation_id=None):
        start_dt = form.start_dt.data
        end_dt = form.end_dt.data
        repeat_frequency = form.repeat_frequency.data
        repeat_interval = form.repeat_interval.data
        day_start_dt = datetime.combine(start_dt.date(), time())
        day_end_dt = datetime.combine(end_dt.date(), time(23, 59))
        today_start_dt = datetime.combine(date.today(), time())
        flexible_start_dt = day_start_dt - timedelta(days=flexible_days)
        if not session.user.isAdmin():
            flexible_start_dt = max(today_start_dt, flexible_start_dt)
        flexible_end_dt = day_end_dt + timedelta(days=flexible_days)

        occurrences = ReservationOccurrence.find_all(
            Reservation.room_id.in_(room_ids),
            Reservation.id != reservation_id,
            ReservationOccurrence.start_dt >= flexible_start_dt,
            ReservationOccurrence.end_dt <= flexible_end_dt,
            ReservationOccurrence.is_valid,
            _join=Reservation,
            _eager=ReservationOccurrence.reservation
        )

        candidates = {}
        for days in xrange(-flexible_days, flexible_days + 1):
            offset = timedelta(days=days)
            series_start = start_dt + offset
            series_end = end_dt + offset
            if series_start < flexible_start_dt:
                continue
            candidates[series_start, series_end] = ReservationOccurrence.create_series(series_start, series_end,
                                                                                       (repeat_frequency,
                                                                                        repeat_interval))
        return occurrences, candidates
Example #5
0
    def _get_all_occurrences(self, room_ids, form, flexible_days=0, reservation_id=None):
        start_dt = form.start_dt.data
        end_dt = form.end_dt.data
        repeat_frequency = form.repeat_frequency.data
        repeat_interval = form.repeat_interval.data
        day_start_dt = datetime.combine(start_dt.date(), time())
        day_end_dt = datetime.combine(end_dt.date(), time(23, 59))
        flexible_start_dt = day_start_dt - timedelta(days=flexible_days)
        flexible_end_dt = day_end_dt + timedelta(days=flexible_days)

        occurrences = (
            ReservationOccurrence.find(
                Reservation.room_id.in_(room_ids),
                Reservation.id != reservation_id,
                ReservationOccurrence.start_dt >= flexible_start_dt,
                ReservationOccurrence.end_dt <= flexible_end_dt,
                ReservationOccurrence.is_valid,
                _join=ReservationOccurrence.reservation,
                _eager=ReservationOccurrence.reservation,
            )
            .options(ReservationOccurrence.NO_RESERVATION_USER_STRATEGY)
            .all()
        )

        candidates = {}
        for days in xrange(-flexible_days, flexible_days + 1):
            offset = timedelta(days=days)
            series_start = start_dt + offset
            series_end = end_dt + offset
            if series_start < flexible_start_dt:
                continue
            candidates[series_start, series_end] = ReservationOccurrence.create_series(
                series_start, series_end, (repeat_frequency, repeat_interval)
            )
        return occurrences, candidates
Example #6
0
def get_rooms_conflicts(rooms, start_dt, end_dt, repeat_frequency, repeat_interval, blocked_rooms,
                        nonbookable_periods, unbookable_hours):
    rooms_conflicts = defaultdict(list)
    rooms_pre_conflicts = defaultdict(list)

    candidates = ReservationOccurrence.create_series(start_dt, end_dt, (repeat_frequency, repeat_interval))
    room_ids = [room.id for room in rooms]
    query = (ReservationOccurrence.query
             .filter(Reservation.room_id.in_(room_ids),
                     ReservationOccurrence.is_valid,
                     ReservationOccurrence.filter_overlap(candidates))
             .join(ReservationOccurrence.reservation)
             .options(ReservationOccurrence.NO_RESERVATION_USER_STRATEGY,
                      contains_eager(ReservationOccurrence.reservation)))

    overlapping_occurrences = group_list(query, key=lambda obj: obj.reservation.room.id)
    for room_id, occurrences in overlapping_occurrences.iteritems():
        rooms_conflicts[room_id], rooms_pre_conflicts[room_id] = get_room_bookings_conflicts(candidates, occurrences)

    for room_id, occurrences in blocked_rooms.iteritems():
        rooms_conflicts[room_id] += get_room_blockings_conflicts(room_id, candidates, occurrences)

    for room_id, occurrences in nonbookable_periods.iteritems():
        rooms_conflicts[room_id] += get_room_nonbookable_periods_conflicts(candidates, occurrences)

    for room_id, occurrences in unbookable_hours.iteritems():
        rooms_conflicts[room_id] += get_room_unbookable_hours_conflicts(candidates, occurrences)
    return rooms_conflicts, rooms_pre_conflicts
Example #7
0
def _build_notification_window_filter():
    if datetime.now().hour >= rb_settings.get('notification_hour'):
        # Both today and delayed notifications
        return ReservationOccurrence.is_in_notification_window()
    else:
        # Delayed notifications only
        return ReservationOccurrence.is_in_notification_window(exclude_first_day=True)
def test_filter_overlap(create_occurrence, overlapping_combination_from_2am_to_4am):
    start_hour, end_hour, expected = overlapping_combination_from_2am_to_4am()
    occ1 = create_occurrence(start_dt=date.today() + relativedelta(hour=2),
                             end_dt=date.today() + relativedelta(hour=4))
    occ2 = ReservationOccurrence(start_dt=date.today() + relativedelta(hour=start_hour),
                                 end_dt=date.today() + relativedelta(hour=end_hour))
    overlap_filter = ReservationOccurrence.filter_overlap([occ2])
    assert (occ1 in ReservationOccurrence.find_all(overlap_filter)) == expected
def test_find_overlapping_with_is_not_valid(db, overlapping_occurrences):
    db_occ, occ = overlapping_occurrences
    assert db_occ in ReservationOccurrence.find_overlapping_with(room=db_occ.reservation.room,
                                                                 occurrences=[occ]).all()
    db_occ.is_cancelled = True
    db.session.flush()
    assert db_occ not in ReservationOccurrence.find_overlapping_with(room=db_occ.reservation.room,
                                                                     occurrences=[occ]).all()
def test_find_overlapping_with_skip_reservation(overlapping_occurrences):
    db_occ, occ = overlapping_occurrences
    assert db_occ in ReservationOccurrence.find_overlapping_with(room=db_occ.reservation.room, occurrences=[occ]).all()
    assert (
        db_occ
        not in ReservationOccurrence.find_overlapping_with(
            room=db_occ.reservation.room, occurrences=[occ], skip_reservation_id=db_occ.reservation.id
        ).all()
    )
def assert_is_in_notification_window(occurrence, expected, expected_with_exclude):
    assert occurrence.is_in_notification_window() == expected
    assert occurrence.is_in_notification_window(exclude_first_day=True) == expected_with_exclude
    assert ReservationOccurrence.find_first(
        ReservationOccurrence.is_in_notification_window()) == (occurrence if expected else None)
    assert ReservationOccurrence.find_first(
        ReservationOccurrence.is_in_notification_window(exclude_first_day=True)) == (occurrence if
                                                                                     expected_with_exclude
                                                                                     else None)
def test_create_series_for_reservation(dummy_reservation):
    ReservationOccurrence.create_series_for_reservation(dummy_reservation)
    occurrences = ReservationOccurrence.iter_create_occurrences(start=dummy_reservation.start_dt,
                                                                end=dummy_reservation.end_dt,
                                                                repetition=dummy_reservation.repetition)
    for occ1, occ2 in izip(dummy_reservation.occurrences, occurrences):
        assert occ1.start_dt == occ2.start_dt
        assert occ1.end_dt == occ2.end_dt
        assert occ1.is_cancelled == dummy_reservation.is_cancelled
        assert occ1.is_rejected == dummy_reservation.is_rejected
        assert occ1.rejection_reason == dummy_reservation.rejection_reason
def test_get_overlap(overlapping_combination_from_2am_to_4am):
    start_hour, end_hour, expected_overlap = overlapping_combination_from_2am_to_4am(boolean=False)
    occ1 = ReservationOccurrence(start_dt=date.today() + relativedelta(hour=2),
                                 end_dt=date.today() + relativedelta(hour=4))
    occ2 = ReservationOccurrence(start_dt=date.today() + relativedelta(hour=start_hour),
                                 end_dt=date.today() + relativedelta(hour=end_hour))
    if expected_overlap != (None, None):
        overlap_start = date.today() + relativedelta(hour=expected_overlap[0])
        overlap_end = date.today() + relativedelta(hour=expected_overlap[1])
        assert occ1.get_overlap(occ2) == (overlap_start, overlap_end)
    else:
        assert occ1.get_overlap(occ2) == expected_overlap
def test_iter_start_time_monthly(interval, days_elapsed, expected_length):
    assert days_elapsed >= 0
    params = {'start': date.today() + relativedelta(hour=8),
              'end': date.today() + relativedelta(days=days_elapsed, hour=17),
              'repetition': (RepeatFrequency.MONTH, interval)}
    if expected_length is None:
        with pytest.raises(IndicoError):
            ReservationOccurrence.iter_start_time(**params)
    else:
        days = list(ReservationOccurrence.iter_start_time(**params))
        weekday = params['start'].weekday()
        assert len(days) == expected_length
        assert all(day.weekday() == weekday for day in days)
def test_iter_start_time_weekly(interval, days_elapsed, expected_length):
    assert days_elapsed >= 0
    params = {'start': date.today() + relativedelta(hour=8),
              'end': date.today() + relativedelta(days=days_elapsed, hour=17),
              'repetition': (RepeatFrequency.WEEK, interval)}
    if expected_length is None:
        with pytest.raises(IndicoError):
            ReservationOccurrence.iter_start_time(**params)
    else:
        days = list(ReservationOccurrence.iter_start_time(**params))
        assert len(days) == expected_length
        for i, day in enumerate(days):
            assert day.date() == date.today() + relativedelta(weeks=i * interval)
def test_iter_start_time_daily(interval, days_elapsed, expected_length):
    assert days_elapsed >= 0
    params = {
        "start": date.today() + relativedelta(hour=8),
        "end": date.today() + relativedelta(days=days_elapsed, hour=17),
        "repetition": (RepeatFrequency.DAY, interval),
    }
    if expected_length is None:
        with pytest.raises(IndicoError):
            ReservationOccurrence.iter_start_time(**params)
    else:
        days = list(ReservationOccurrence.iter_start_time(**params))
        assert len(days) == expected_length
        for i, day in enumerate(days):
            assert day.date() == date.today() + relativedelta(days=i)
Example #17
0
def get_recurring_booking_suggestions(rooms, start_dt, end_dt, repeat_frequency, repeat_interval, limit=None):
    data = []
    booking_days = end_dt - start_dt
    booking_length = booking_days.days + 1
    candidates = ReservationOccurrence.create_series(start_dt, end_dt, (repeat_frequency, repeat_interval))
    blocked_rooms = group_blocked_rooms(get_rooms_blockings(rooms, start_dt.date(), end_dt.date()))
    unbookable_hours = get_rooms_unbookable_hours(rooms)
    nonbookable_periods = get_rooms_nonbookable_periods(rooms, start_dt, end_dt)
    conflicts = get_rooms_conflicts(rooms, start_dt, end_dt, repeat_frequency, repeat_interval, blocked_rooms,
                                    nonbookable_periods, unbookable_hours)[0]
    for room in rooms:
        if limit and len(data) == limit:
            break

        suggestions = {}
        booking_limit = room.booking_limit_days or rb_settings.get('booking_limit')
        limit_exceeded = booking_limit is not None and booking_limit < booking_length
        if limit_exceeded:
            excess_days = booking_length - booking_limit
            suggestions['shorten'] = excess_days

        if not limit_exceeded:
            number_of_conflicting_days = len(group_by_occurrence_date(conflicts.get(room.id, [])))
            if number_of_conflicting_days and number_of_conflicting_days < len(candidates):
                suggestions['skip'] = number_of_conflicting_days
        if suggestions:
            data.append({'room': room, 'suggestions': suggestions})
    return data
Example #18
0
def get_rooms_availability(rooms, start_dt, end_dt, repeat_frequency, repeat_interval):
    availability = OrderedDict()
    candidates = ReservationOccurrence.create_series(start_dt, end_dt, (repeat_frequency, repeat_interval))
    date_range = sorted(set(cand.start_dt.date() for cand in candidates))
    occurrences = get_existing_rooms_occurrences(rooms, start_dt.replace(hour=0, minute=0),
                                                 end_dt.replace(hour=23, minute=59), repeat_frequency, repeat_interval)
    blocked_rooms = get_rooms_blockings(rooms, start_dt.date(), end_dt.date())
    unbookable_hours = get_rooms_unbookable_hours(rooms)
    nonbookable_periods = get_rooms_nonbookable_periods(rooms, start_dt, end_dt)
    conflicts, pre_conflicts = get_rooms_conflicts(rooms, start_dt.replace(tzinfo=None), end_dt.replace(tzinfo=None),
                                                   repeat_frequency, repeat_interval, blocked_rooms,
                                                   nonbookable_periods, unbookable_hours)
    dates = list(candidate.start_dt.date() for candidate in candidates)
    for room in rooms:
        room_occurrences = occurrences.get(room.id, [])
        room_conflicts = conflicts.get(room.id, [])
        pre_room_conflicts = pre_conflicts.get(room.id, [])
        pre_bookings = [occ for occ in room_occurrences if not occ.reservation.is_accepted]
        existing_bookings = [occ for occ in room_occurrences if occ.reservation.is_accepted]
        room_blocked_rooms = blocked_rooms.get(room.id, [])
        room_nonbookable_periods = nonbookable_periods.get(room.id, [])
        room_unbookable_hours = unbookable_hours.get(room.id, [])
        availability[room.id] = {'room_id': room.id,
                                 'candidates': group_by_occurrence_date(candidates),
                                 'pre_bookings': group_by_occurrence_date(pre_bookings),
                                 'bookings': group_by_occurrence_date(existing_bookings),
                                 'conflicts': group_by_occurrence_date(room_conflicts),
                                 'pre_conflicts': group_by_occurrence_date(pre_room_conflicts),
                                 'blockings': group_blockings(room_blocked_rooms, dates),
                                 'nonbookable_periods': group_nonbookable_periods(room_nonbookable_periods, dates),
                                 'unbookable_hours': room_unbookable_hours}
    return date_range, availability
Example #19
0
    def _process(self):
        if self._overload:
            rooms = []
            occurrences = []
        else:
            rooms = Room.find_all(is_active=True)
            occurrences = (
                ReservationOccurrence.find(
                    Reservation.room_id.in_(room.id for room in rooms),
                    ReservationOccurrence.start_dt >= self.start_dt,
                    ReservationOccurrence.end_dt <= self.end_dt,
                    ReservationOccurrence.is_valid,
                    _join=ReservationOccurrence.reservation,
                    _eager=ReservationOccurrence.reservation,
                )
                .options(ReservationOccurrence.NO_RESERVATION_USER_STRATEGY)
                .all()
            )

        return WPRoomBookingCalendar(
            self,
            rooms=rooms,
            occurrences=occurrences,
            start_dt=self.start_dt,
            end_dt=self.end_dt,
            overload=self._overload,
            max_days=self.MAX_DAYS,
        ).display()
Example #20
0
def roombooking_occurrences():
    if not Config.getInstance().getIsRoomBookingActive():
        logger.info('Notifications not sent because room booking is disabled')
        return
    if not rb_settings.get('notifications_enabled'):
        logger.info('Notifications not sent because they are globally disabled')
        return

    occurrences = ReservationOccurrence.find(
        Room.notifications_enabled,
        Reservation.is_accepted,
        Reservation.repeat_frequency != RepeatFrequency.WEEK,
        ReservationOccurrence.is_valid,
        ReservationOccurrence.start_dt >= datetime.now(),
        ~ReservationOccurrence.notification_sent,
        _build_notification_window_filter(),
        _join=[Reservation, Room]
    )

    try:
        for occ in occurrences:
            notify_upcoming_occurrence(occ)
            occ.notification_sent = True
            if occ.reservation.repeat_frequency == RepeatFrequency.DAY:
                occ.reservation.occurrences.update({'notification_sent': True})
    finally:
        db.session.commit()
Example #21
0
    def _process(self):
        form = self._form
        if self._is_submitted() and form.validate():
            if form.data.get("is_only_my_rooms"):
                form.room_ids.data = [room.id for room in Room.find_all() if room.is_owned_by(session.user)]

            occurrences = ReservationOccurrence.find_with_filters(form.data, session.user).all()
            rooms = self._filter_displayed_rooms(
                [r for r in self._rooms if r.id in set(form.room_ids.data)], occurrences
            )
            return WPRoomBookingSearchBookingsResults(
                self,
                rooms=rooms,
                occurrences=occurrences,
                show_blockings=self.show_blockings,
                start_dt=form.start_dt.data,
                end_dt=form.end_dt.data,
                form=form,
                form_data=self._form_data,
                menu_item=self.menu_item,
            ).display()

        my_rooms = [r.id for r in Room.get_owned_by(session.user)]
        return WPRoomBookingSearchBookings(
            self, errors=form.error_list, rooms=self._rooms, my_rooms=my_rooms, form=form
        ).display()
Example #22
0
def roombooking_occurrences_digest():
    if not Config.getInstance().getIsRoomBookingActive():
        logger.info('Digest not sent because room booking is disabled')
        return
    if not rb_settings.get('notifications_enabled'):
        logger.info('Digest not sent because notifications are globally disabled')
        return

    digest_start = round_up_month(date.today(), from_day=2)
    digest_end = get_month_end(digest_start)

    occurrences = ReservationOccurrence.find(
        Room.notifications_enabled,
        Reservation.is_accepted,
        Reservation.repeat_frequency == RepeatFrequency.WEEK,
        ReservationOccurrence.is_valid,
        ReservationOccurrence.start_dt >= digest_start,
        ReservationOccurrence.start_dt <= digest_end,
        ~ReservationOccurrence.notification_sent,
        _build_digest_window_filter(),
        _join=[Reservation, Room]
    )

    digests = defaultdict(list)
    for occurrence in occurrences:
        digests[occurrence.reservation].append(occurrence)

    try:
        for reservation, occurrences in digests.iteritems():
            notify_reservation_digest(reservation, occurrences)
            for occurrence in occurrences:
                occurrence.notification_sent = True
    finally:
        db.session.commit()
Example #23
0
 def _get_date_range(self, filters):
     try:
         start_dt, end_dt = filters['start_dt'], filters['end_dt']
         repetition = filters['repeat_frequency'], filters['repeat_interval']
     except KeyError:
         return None
     return [dt.date().isoformat() for dt in ReservationOccurrence.iter_start_time(start_dt, end_dt, repetition)]
Example #24
0
    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():
            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')
Example #25
0
 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,
     )
Example #26
0
    def _get_all_conflicts(self, room, form, reservation_id=None):
        conflicts = defaultdict(list)
        pre_conflicts = defaultdict(list)

        candidates = ReservationOccurrence.create_series(form.start_dt.data, form.end_dt.data,
                                                         (form.repeat_frequency.data, form.repeat_interval.data))
        occurrences = ReservationOccurrence.find_overlapping_with(room, candidates, reservation_id).all()

        for cand in candidates:
            for occ in occurrences:
                if cand.overlaps(occ):
                    if occ.reservation.is_accepted:
                        conflicts[cand].append(occ)
                    else:
                        pre_conflicts[cand].append(occ)

        return conflicts, pre_conflicts
Example #27
0
def get_rooms_conflicts(rooms, start_dt, end_dt, repeat_frequency, repeat_interval, blocked_rooms,
                        nonbookable_periods, unbookable_hours, skip_conflicts_with=None, allow_admin=False):
    rooms_conflicts = defaultdict(set)
    rooms_pre_conflicts = defaultdict(set)
    rooms_conflicting_candidates = defaultdict(set)
    skip_conflicts_with = skip_conflicts_with or []

    candidates = ReservationOccurrence.create_series(start_dt, end_dt, (repeat_frequency, repeat_interval))
    room_ids = [room.id for room in rooms]
    query = (ReservationOccurrence.query
             .filter(Reservation.room_id.in_(room_ids),
                     ReservationOccurrence.is_valid,
                     ReservationOccurrence.filter_overlap(candidates))
             .join(ReservationOccurrence.reservation)
             .options(ReservationOccurrence.NO_RESERVATION_USER_STRATEGY,
                      contains_eager(ReservationOccurrence.reservation)))

    if skip_conflicts_with:
        query = query.filter(~Reservation.id.in_(skip_conflicts_with))

    overlapping_occurrences = group_list(query, key=lambda obj: obj.reservation.room.id)
    for room_id, occurrences in overlapping_occurrences.iteritems():
        conflicts = get_room_bookings_conflicts(candidates, occurrences, room_id, skip_conflicts_with)
        rooms_conflicts[room_id], rooms_pre_conflicts[room_id], rooms_conflicting_candidates[room_id] = conflicts
    for room_id, occurrences in blocked_rooms.iteritems():
        conflicts, conflicting_candidates = get_room_blockings_conflicts(room_id, candidates, occurrences)
        rooms_conflicts[room_id] |= conflicts
        rooms_conflicting_candidates[room_id] |= conflicting_candidates

    if not (allow_admin and rb_is_admin(session.user)):
        for room_id, occurrences in nonbookable_periods.iteritems():
            room = Room.get_one(room_id)
            if not room.can_override(session.user, allow_admin=allow_admin):
                conflicts, conflicting_candidates = get_room_nonbookable_periods_conflicts(candidates, occurrences)
                rooms_conflicts[room_id] |= conflicts
                rooms_conflicting_candidates[room_id] |= conflicting_candidates

        for room_id, occurrences in unbookable_hours.iteritems():
            room = Room.get_one(room_id)
            if not room.can_override(session.user, allow_admin=allow_admin):
                conflicts, conflicting_candidates = get_room_unbookable_hours_conflicts(candidates, occurrences)
                rooms_conflicts[room_id] |= conflicts
                rooms_conflicting_candidates[room_id] |= conflicting_candidates
    rooms_conflicting_candidates = defaultdict(list, ((k, list(v)) for k, v in rooms_conflicting_candidates.items()))
    return rooms_conflicts, rooms_pre_conflicts, rooms_conflicting_candidates
def test_iter_start_time_single(interval):
    days = list(
        ReservationOccurrence.iter_start_time(
            start=date.today() + relativedelta(hour=8),
            end=date.today() + relativedelta(hour=17),
            repetition=(RepeatFrequency.NEVER, interval),
        )
    )
    assert len(days) == 1
Example #29
0
 def iter_days(self):
     if self.repeat_frequency is None and self.repeat_interval is None:
         for dt in iterdays(self.start_dt, self.end_dt):
             yield dt.date()
     else:
         for dt in ReservationOccurrence.iter_start_time(self.start_dt, self.end_dt,
                                                         (self.repeat_frequency, self.repeat_interval)):
             for offset in xrange(-self.flexible_days, self.flexible_days + 1):
                 yield (dt + timedelta(days=offset)).date()
Example #30
0
 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 test_iter_start_time_monthly_5th_monday_is_always_last():
    start_dt = date(2014, 9, 29) + relativedelta(
        hour=8)  # 5th monday of september
    end_dt = start_dt + relativedelta(days=100, hour=17)
    params = {
        'start': start_dt,
        'end': end_dt,
        'repetition': (RepeatFrequency.MONTH, 1)
    }

    days = list(ReservationOccurrence.iter_start_time(**params))
    assert len(days) == 4
    assert days[1].date() == date(2014, 10, 27)  # 4th monday of october
    assert days[2].date() == date(2014, 11, 24)  # 4th monday of october
    assert days[3].date() == date(2014, 12, 29)  # 5th monday of october
Example #32
0
    def _get_all_occurrences(self,
                             room_ids,
                             form,
                             flexible_days=0,
                             reservation_id=None):
        start_dt = form.start_dt.data
        end_dt = form.end_dt.data
        repeat_frequency = form.repeat_frequency.data
        repeat_interval = form.repeat_interval.data
        day_start_dt = datetime.combine(start_dt.date(), time())
        day_end_dt = datetime.combine(end_dt.date(), time(23, 59))
        flexible_start_dt = day_start_dt - timedelta(days=flexible_days)
        flexible_end_dt = day_end_dt + timedelta(days=flexible_days)

        occurrences = ReservationOccurrence.find(
            Reservation.room_id.in_(room_ids),
            Reservation.id != reservation_id,
            ReservationOccurrence.start_dt >= flexible_start_dt,
            ReservationOccurrence.end_dt <= flexible_end_dt,
            ReservationOccurrence.is_valid,
            _join=ReservationOccurrence.reservation,
            _eager=ReservationOccurrence.reservation).options(
                ReservationOccurrence.NO_RESERVATION_USER_STRATEGY).all()

        candidates = {}
        for days in xrange(-flexible_days, flexible_days + 1):
            offset = timedelta(days=days)
            series_start = start_dt + offset
            series_end = end_dt + offset
            if series_start < flexible_start_dt:
                continue
            candidates[series_start,
                       series_end] = ReservationOccurrence.create_series(
                           series_start, series_end,
                           (repeat_frequency, repeat_interval))
        return occurrences, candidates
Example #33
0
def get_rooms_availability(rooms, start_dt, end_dt, repeat_frequency, repeat_interval, skip_conflicts_with=None,
                           admin_override_enabled=False):
    availability = OrderedDict()
    candidates = ReservationOccurrence.create_series(start_dt, end_dt, (repeat_frequency, repeat_interval))
    date_range = sorted(set(cand.start_dt.date() for cand in candidates))
    occurrences = get_existing_rooms_occurrences(rooms, start_dt.replace(hour=0, minute=0),
                                                 end_dt.replace(hour=23, minute=59), repeat_frequency, repeat_interval)
    blocked_rooms = get_rooms_blockings(rooms, start_dt.date(), end_dt.date())
    nonoverridable_blocked_rooms = group_blocked_rooms(filter_blocked_rooms(blocked_rooms,
                                                                            nonoverridable_only=True,
                                                                            explicit=True))
    overridable_blocked_rooms = group_blocked_rooms(filter_blocked_rooms(blocked_rooms,
                                                                         overridable_only=True,
                                                                         explicit=True))
    unbookable_hours = get_rooms_unbookable_hours(rooms)
    nonbookable_periods = get_rooms_nonbookable_periods(rooms, start_dt, end_dt)
    conflicts, pre_conflicts, conflicting_candidates = get_rooms_conflicts(
        rooms, start_dt.replace(tzinfo=None), end_dt.replace(tzinfo=None),
        repeat_frequency, repeat_interval, nonoverridable_blocked_rooms,
        nonbookable_periods, unbookable_hours, skip_conflicts_with,
        allow_admin=admin_override_enabled
    )
    dates = list(candidate.start_dt.date() for candidate in candidates)
    for room in rooms:
        room_occurrences = occurrences.get(room.id, [])
        room_conflicting_candidates = conflicting_candidates.get(room.id, [])
        room_conflicts = conflicts.get(room.id, [])
        pre_room_conflicts = pre_conflicts.get(room.id, [])
        pre_bookings = [occ for occ in room_occurrences if not occ.reservation.is_accepted]
        existing_bookings = [occ for occ in room_occurrences if occ.reservation.is_accepted]
        room_nonoverridable_blocked_rooms = nonoverridable_blocked_rooms.get(room.id, [])
        room_overridable_blocked_rooms = overridable_blocked_rooms.get(room.id, [])
        room_nonbookable_periods = nonbookable_periods.get(room.id, [])
        room_unbookable_hours = unbookable_hours.get(room.id, [])

        room_candidates = get_room_candidates(candidates, room_conflicts, pre_room_conflicts)
        availability[room.id] = {'room_id': room.id,
                                 'candidates': group_by_occurrence_date(room_candidates),
                                 'conflicting_candidates': group_by_occurrence_date(room_conflicting_candidates),
                                 'pre_bookings': group_by_occurrence_date(pre_bookings),
                                 'bookings': group_by_occurrence_date(existing_bookings),
                                 'conflicts': group_by_occurrence_date(room_conflicts),
                                 'pre_conflicts': group_by_occurrence_date(pre_room_conflicts),
                                 'blockings': group_blockings(room_nonoverridable_blocked_rooms, dates),
                                 'overridable_blockings': group_blockings(room_overridable_blocked_rooms, dates),
                                 'nonbookable_periods': group_nonbookable_periods(room_nonbookable_periods, dates),
                                 'unbookable_hours': room_unbookable_hours}
    return date_range, availability
Example #34
0
    def approve(self, notify_blocker=True):
        """Approve the room blocking, rejecting all colliding reservations/occurrences."""
        self.state = BlockedRoomState.accepted

        # Get colliding reservations
        start_dt = datetime.combine(self.blocking.start_date, time())
        end_dt = datetime.combine(self.blocking.end_date, time(23, 59, 59))

        reservation_criteria = [
            Reservation.room_id == self.room_id,
            ~Reservation.is_rejected,
            ~Reservation.is_cancelled
        ]

        # Whole reservations to reject
        reservations = Reservation.find_all(
            Reservation.start_dt >= start_dt,
            Reservation.end_dt <= end_dt,
            *reservation_criteria
        )

        # Single occurrences to reject
        occurrences = ReservationOccurrence.find_all(
            ReservationOccurrence.start_dt >= start_dt,
            ReservationOccurrence.end_dt <= end_dt,
            ReservationOccurrence.is_valid,
            ~ReservationOccurrence.reservation_id.in_(map(attrgetter('id'), reservations)) if reservations else True,
            *reservation_criteria,
            _join=Reservation
        )

        reason = f'Conflict with blocking {self.blocking.id}: {self.blocking.reason}'

        for reservation in reservations:
            if self.blocking.can_override(reservation.created_by_user, room=reservation.room):
                continue
            reservation.reject(self.blocking.created_by_user, reason)

        for occurrence in occurrences:
            reservation = occurrence.reservation
            if self.blocking.can_override(reservation.created_by_user, room=reservation.room):
                continue
            occurrence.reject(self.blocking.created_by_user, reason)

        if notify_blocker:
            # We only need to notify the blocking creator if the blocked room wasn't approved yet.
            # This is the case if it's a new blocking for a room managed by the creator
            notify_request_response(self)
Example #35
0
def get_number_of_skipped_days_for_rooms(rooms, start_dt, end_dt, repeat_frequency, repeat_interval, limit=None):
    data = []
    candidates = ReservationOccurrence.create_series(start_dt, end_dt, (repeat_frequency, repeat_interval))
    blocked_rooms = get_rooms_blockings(rooms, start_dt.date(), end_dt.date())
    unbookable_hours = get_rooms_unbookable_hours(rooms)
    nonbookable_periods = get_rooms_nonbookable_periods(rooms, start_dt, end_dt)
    conflicts, _ = get_rooms_conflicts(rooms, start_dt, end_dt, repeat_frequency, repeat_interval, blocked_rooms,
                                       nonbookable_periods, unbookable_hours)
    for room in rooms:
        if limit and len(data) == limit:
            break

        number_of_conflicting_days = len(group_by_occurrence_date(conflicts.get(room.id, [])))
        if number_of_conflicting_days and number_of_conflicting_days < len(candidates):
            data.append({'room': room, 'suggestions': {'skip': number_of_conflicting_days}})
    return sorted(data, key=lambda item: item['suggestions']['skip'])
Example #36
0
    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')
Example #37
0
 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')
Example #38
0
def get_matching_events(start_dt, end_dt, repeat_frequency, repeat_interval):
    """Get events suitable for booking linking.

    This finds events that overlap with an occurrence of a booking
    with the given dates where the user is a manager.
    """
    occurrences = ReservationOccurrence.create_series(start_dt, end_dt, (repeat_frequency, repeat_interval))
    excluded_categories = rb_settings.get('excluded_categories')
    return (Event.query
            .filter(~Event.is_deleted,
                    ~Event.room_reservation_links.any(ReservationLink.reservation.has(Reservation.is_accepted)),
                    db.or_(Event.happens_between(as_utc(occ.start_dt), as_utc(occ.end_dt)) for occ in occurrences),
                    Event.timezone == config.DEFAULT_TIMEZONE,
                    db.and_(Event.category_id != cat.id for cat in excluded_categories),
                    Event.acl_entries.any(db.and_(EventPrincipal.type == PrincipalType.user,
                                                  EventPrincipal.user_id == session.user.id,
                                                  EventPrincipal.full_access)))
            .all())
Example #39
0
    def _process(self):
        form = self._form
        if self._is_submitted() and form.validate():
            if form.data.get('is_only_my_rooms'):
                form.room_ids.data = [room.id for room in Room.find_all() if room.is_owned_by(session.user)]

            occurrences = ReservationOccurrence.find_with_filters(form.data, session.user).all()
            rooms = self._filter_displayed_rooms([r for r in self._rooms if r.id in set(form.room_ids.data)],
                                                 occurrences)
            return WPRoomBookingSearchBookingsResults(self, rooms=rooms, occurrences=occurrences,
                                                      show_blockings=self.show_blockings,
                                                      start_dt=form.start_dt.data, end_dt=form.end_dt.data,
                                                      form=form, form_data=self._form_data,
                                                      menu_item=self.menu_item).display()

        my_rooms = [r.id for r in Room.get_owned_by(session.user)]
        return WPRoomBookingSearchBookings(self, errors=form.error_list, rooms=self._rooms, my_rooms=my_rooms,
                                           form=form).display()
Example #40
0
    def run(self):
        occurrences = ReservationOccurrence.find(
            Reservation.is_accepted,
            ~ReservationOccurrence.notification_sent,
            ReservationOccurrence.is_valid,
            ReservationOccurrence.start_dt >= func.now(),
            _build_notification_before_days_filter(
                settings.get('notification_before_days', 0)),
            _join=[Reservation, Room])

        try:
            for occ in occurrences:
                notify_upcoming_occurrence(occ)
                occ.notification_sent = True
                if occ.reservation.repeat_frequency == RepeatFrequency.DAY:
                    occ.reservation.occurrences.update(
                        {'notification_sent': True})
        finally:
            db.session.commit()
Example #41
0
    def _process(self):
        if self._overload:
            rooms = []
            occurrences = []
        else:
            rooms = Room.find_all(is_active=True)
            occurrences = ReservationOccurrence.find_all(
                Reservation.room_id.in_(room.id for room in rooms),
                ReservationOccurrence.start_dt >= self.start_dt,
                ReservationOccurrence.end_dt <= self.end_dt,
                ReservationOccurrence.is_valid,
                _join=Reservation,
                _eager=ReservationOccurrence.reservation)

        return WPRoomBookingCalendar(self,
                                     rooms=rooms,
                                     occurrences=occurrences,
                                     start_dt=self.start_dt,
                                     end_dt=self.end_dt,
                                     overload=self._overload,
                                     max_days=self.MAX_DAYS).display()
Example #42
0
def get_recurring_booking_suggestions(rooms,
                                      start_dt,
                                      end_dt,
                                      repeat_frequency,
                                      repeat_interval,
                                      limit=None):
    data = []
    booking_days = end_dt - start_dt
    booking_length = booking_days.days + 1
    candidates = ReservationOccurrence.create_series(
        start_dt, end_dt, (repeat_frequency, repeat_interval))
    blocked_rooms = get_rooms_blockings(rooms, start_dt.date(), end_dt.date())
    unbookable_hours = get_rooms_unbookable_hours(rooms)
    nonbookable_periods = get_rooms_nonbookable_periods(
        rooms, start_dt, end_dt)
    conflicts, _ = get_rooms_conflicts(rooms, start_dt, end_dt,
                                       repeat_frequency, repeat_interval,
                                       blocked_rooms, nonbookable_periods,
                                       unbookable_hours)
    for room in rooms:
        if limit and len(data) == limit:
            break

        suggestions = {}
        booking_limit = room.booking_limit_days or rb_settings.get(
            'booking_limit')
        limit_exceeded = booking_limit is not None and booking_limit < booking_length
        if limit_exceeded:
            excess_days = booking_length - booking_limit
            suggestions['shorten'] = excess_days

        if not limit_exceeded:
            number_of_conflicting_days = len(
                group_by_occurrence_date(conflicts.get(room.id, [])))
            if number_of_conflicting_days and number_of_conflicting_days < len(
                    candidates):
                suggestions['skip'] = number_of_conflicting_days
        if suggestions:
            data.append({'room': room, 'suggestions': suggestions})
    return data
Example #43
0
def get_existing_rooms_occurrences(rooms,
                                   start_dt,
                                   end_dt,
                                   repeat_frequency,
                                   repeat_interval,
                                   allow_overlapping=False,
                                   only_accepted=False,
                                   skip_booking_id=None):
    room_ids = [room.id for room in rooms]
    query = (ReservationOccurrence.query.filter(
        ReservationOccurrence.is_valid,
        Reservation.room_id.in_(room_ids)).join(
            ReservationOccurrence.reservation).options(
                ReservationOccurrence.NO_RESERVATION_USER_STRATEGY,
                contains_eager(ReservationOccurrence.reservation)))

    if allow_overlapping:
        query = query.filter(
            db_dates_overlap(ReservationOccurrence, 'start_dt', start_dt,
                             'end_dt', end_dt))
    else:
        query = query.filter(ReservationOccurrence.start_dt >= start_dt,
                             ReservationOccurrence.end_dt <= end_dt)
    if only_accepted:
        query = query.filter(Reservation.is_accepted)
    if repeat_frequency != RepeatFrequency.NEVER:
        candidates = ReservationOccurrence.create_series(
            start_dt, end_dt, (repeat_frequency, repeat_interval))
        dates = [candidate.start_dt for candidate in candidates]
        query = query.filter(
            db.cast(ReservationOccurrence.start_dt, db.Date).in_(dates))
    if skip_booking_id is not None:
        query = query.filter(
            ReservationOccurrence.reservation_id != skip_booking_id)

    return group_list(query,
                      key=lambda obj: obj.reservation.room_id,
                      sort_by=lambda obj:
                      (obj.reservation.room_id, obj.start_dt))
Example #44
0
def get_rooms_availability(rooms, start_dt, end_dt, repeat_frequency,
                           repeat_interval, flexibility):
    period_days = (end_dt - start_dt).days
    availability = {}
    candidates = ReservationOccurrence.create_series(
        start_dt, end_dt, (repeat_frequency, repeat_interval))
    date_range = sorted(set(cand.start_dt.date() for cand in candidates))

    for room in rooms:
        booking_limit_days = room.booking_limit_days or rb_settings.get(
            'booking_limit')
        if period_days > booking_limit_days:
            continue

        start_dt = start_dt + timedelta(days=flexibility)
        end_dt = end_dt + timedelta(days=flexibility)
        occurrences = get_existing_room_occurrences(
            room, start_dt.replace(hour=0, minute=0),
            end_dt.replace(hour=23, minute=59))
        conflicts, pre_conflicts = get_room_conflicts(
            room, start_dt.replace(tzinfo=None), end_dt.replace(tzinfo=None),
            repeat_frequency, repeat_interval)

        pre_bookings = [
            occ for occ in occurrences if not occ.reservation.is_accepted
        ]
        existing_bookings = [
            occ for occ in occurrences if occ.reservation.is_accepted
        ]
        availability[room.id] = {
            'room': room,
            'candidates': group_by_occurrence_date(candidates),
            'pre_bookings': group_by_occurrence_date(pre_bookings),
            'bookings': group_by_occurrence_date(existing_bookings),
            'conflicts': group_by_occurrence_date(conflicts),
            'pre_conflicts': group_by_occurrence_date(pre_conflicts)
        }
    return date_range, availability
Example #45
0
def get_rooms_availability(rooms, start_dt, end_dt, repeat_frequency, repeat_interval):
    period_days = (end_dt - start_dt).days
    availability = OrderedDict()
    candidates = ReservationOccurrence.create_series(start_dt, end_dt, (repeat_frequency, repeat_interval))
    date_range = sorted(set(cand.start_dt.date() for cand in candidates))
    occurrences = get_existing_rooms_occurrences(rooms, start_dt.replace(hour=0, minute=0),
                                                 end_dt.replace(hour=23, minute=59), repeat_frequency, repeat_interval)
    blocked_rooms = get_rooms_blockings(rooms, start_dt.date(), end_dt.date())
    unbookable_hours = get_rooms_unbookable_hours(rooms)
    nonbookable_periods = get_rooms_nonbookable_periods(rooms, start_dt, end_dt)
    conflicts, pre_conflicts = get_rooms_conflicts(rooms, start_dt.replace(tzinfo=None), end_dt.replace(tzinfo=None),
                                                   repeat_frequency, repeat_interval, blocked_rooms,
                                                   nonbookable_periods, unbookable_hours)
    dates = list(candidate.start_dt.date() for candidate in candidates)
    for room in rooms:
        booking_limit_days = room.booking_limit_days or rb_settings.get('booking_limit')
        if period_days > booking_limit_days:
            continue

        room_occurrences = occurrences.get(room.id, [])
        room_conflicts = conflicts.get(room.id, [])
        pre_room_conflicts = pre_conflicts.get(room.id, [])
        pre_bookings = [occ for occ in room_occurrences if not occ.reservation.is_accepted]
        existing_bookings = [occ for occ in room_occurrences if occ.reservation.is_accepted]
        room_blocked_rooms = blocked_rooms.get(room.id, [])
        room_nonbookable_periods = nonbookable_periods.get(room.id, [])
        room_unbookable_hours = unbookable_hours.get(room.id, [])
        availability[room.id] = {'room_id': room.id,
                                 'candidates': group_by_occurrence_date(candidates),
                                 'pre_bookings': group_by_occurrence_date(pre_bookings),
                                 'bookings': group_by_occurrence_date(existing_bookings),
                                 'conflicts': group_by_occurrence_date(room_conflicts),
                                 'pre_conflicts': group_by_occurrence_date(pre_room_conflicts),
                                 'blockings': group_blockings(room_blocked_rooms, dates),
                                 'nonbookable_periods': group_nonbookable_periods(room_nonbookable_periods, dates),
                                 'unbookable_hours': room_unbookable_hours}
    return date_range, availability
Example #46
0
    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})
Example #47
0
def overlapping_occurrences(create_occurrence):
    db_occ = create_occurrence(start_dt=date.today() + relativedelta(hour=2),
                               end_dt=date.today() + relativedelta(hour=4))
    occ = ReservationOccurrence(start_dt=date.today() + relativedelta(hour=1),
                                end_dt=date.today() + relativedelta(hour=5))
    return db_occ, occ
def test_iter_create_occurrences(creation_params):
    occurrences = list(ReservationOccurrence.iter_create_occurrences(**creation_params))
    assert len(occurrences) == 2
    for occ in occurrences:
        assert occ.start_dt.time() == time(8)
        assert occ.end_dt.time() == time(17)
def test_create_series(creation_params):
    for occ1, occ2 in izip(list(ReservationOccurrence.iter_create_occurrences(**creation_params)),
                           ReservationOccurrence.create_series(**creation_params)):
        assert occ1.start_dt == occ2.start_dt
        assert occ1.end_dt == occ2.end_dt
def test_iter_start_time_single(interval):
    days = list(ReservationOccurrence.iter_start_time(start=date.today() + relativedelta(hour=8),
                                                      end=date.today() + relativedelta(hour=17),
                                                      repetition=(RepeatFrequency.NEVER, interval)))
    assert len(days) == 1
def test_iter_start_time_invalid():
    invalid_frequency = -1
    assert invalid_frequency not in RepeatFrequency
    with pytest.raises(IndicoError):
        ReservationOccurrence.iter_start_time(start=date.today(), end=date.today(), repetition=(invalid_frequency, 0))
Example #52
0
def overlapping_reservation(create_reservation):
    reservation = create_reservation(start_dt=date.today() + relativedelta(hour=2),
                                     end_dt=date.today() + relativedelta(hour=4))
    occurrence = ReservationOccurrence(start_dt=date.today() + relativedelta(hour=1),
                                       end_dt=date.today() + relativedelta(hour=5))
    return reservation, occurrence
def test_find_overlapping_with_different_room(overlapping_occurrences, create_room):
    db_occ, occ = overlapping_occurrences
    assert db_occ in ReservationOccurrence.find_overlapping_with(room=db_occ.reservation.room, occurrences=[occ]).all()
    assert db_occ not in ReservationOccurrence.find_overlapping_with(room=create_room(), occurrences=[occ]).all()
def test_find_overlapping_with_skip_reservation(overlapping_occurrences):
    db_occ, occ = overlapping_occurrences
    assert db_occ in ReservationOccurrence.find_overlapping_with(room=db_occ.reservation.room, occurrences=[occ]).all()
    assert db_occ not in ReservationOccurrence.find_overlapping_with(room=db_occ.reservation.room,
                                                                     occurrences=[occ],
                                                                     skip_reservation_id=db_occ.reservation.id).all()
def test_is_valid(db, dummy_occurrence, is_rejected, is_cancelled, expected):
    dummy_occurrence.is_rejected = is_rejected
    dummy_occurrence.is_cancelled = is_cancelled
    db.session.flush()
    assert dummy_occurrence.is_valid == expected
    assert ReservationOccurrence.find_first(is_valid=expected) == dummy_occurrence
Example #56
0
def test_date(dummy_occurrence):
    assert dummy_occurrence.date == date.today()
    assert ReservationOccurrence.find_first(
        date=date.today()) == dummy_occurrence
Example #57
0
    def migrate_reservations(self):
        print cformat('%{white!}migrating reservations')
        i = 1
        for rid, v in self.rb_root['Reservations'].iteritems():
            room = Room.get(v.room.id)
            if room is None:
                print cformat(
                    '  %{red!}skipping resv for dead room {0.room.id}: {0.id} ({0._utcCreatedDT})'
                ).format(v)
                continue

            repeat_frequency, repeat_interval = RepeatMapping.convert_legacy_repeatability(
                v.repeatability)
            booked_for_id = getattr(v, 'bookedForId', None)

            r = Reservation(
                id=v.id,
                created_dt=as_utc(v._utcCreatedDT),
                start_dt=utc_to_local(v._utcStartDT),
                end_dt=utc_to_local(v._utcEndDT),
                booked_for_id=self.merged_avatars.get(booked_for_id,
                                                      booked_for_id) or None,
                booked_for_name=convert_to_unicode(v.bookedForName),
                contact_email=convert_to_unicode(v.contactEmail),
                contact_phone=convert_to_unicode(
                    getattr(v, 'contactPhone', None)),
                created_by_id=self.merged_avatars.get(v.createdBy, v.createdBy)
                or None,
                is_cancelled=v.isCancelled,
                is_accepted=v.isConfirmed,
                is_rejected=v.isRejected,
                booking_reason=convert_to_unicode(v.reason),
                rejection_reason=convert_to_unicode(
                    getattr(v, 'rejectionReason', None)),
                repeat_frequency=repeat_frequency,
                repeat_interval=repeat_interval,
                uses_vc=getattr(v, 'usesAVC', False),
                needs_vc_assistance=getattr(v, 'needsAVCSupport', False),
                needs_assistance=getattr(v, 'needsAssistance', False))

            for eq_name in getattr(v, 'useVC', []):
                eq = room.location.get_equipment_by_name(eq_name)
                if eq:
                    r.used_equipment.append(eq)

            occurrence_rejection_reasons = {}
            if getattr(v, 'resvHistory', None):
                for h in reversed(v.resvHistory._entries):
                    ts = as_utc(parse_dt_string(h._timestamp))

                    if len(h._info) == 2:
                        possible_rejection_date, possible_rejection_reason = h._info
                        m = re.match(
                            r'Booking occurrence of the (\d{1,2} \w{3} \d{4}) rejected',
                            possible_rejection_reason)
                        if m:
                            d = datetime.strptime(m.group(1), '%d %b %Y')
                            occurrence_rejection_reasons[
                                d] = possible_rejection_reason[9:].strip('\'')

                    el = ReservationEditLog(timestamp=ts,
                                            user_name=h._responsibleUser,
                                            info=map(convert_to_unicode,
                                                     h._info))
                    r.edit_logs.append(el)

            notifications = getattr(v, 'startEndNotification', []) or []
            excluded_days = getattr(v, '_excludedDays', []) or []
            ReservationOccurrence.create_series_for_reservation(r)
            for occ in r.occurrences:
                occ.notification_sent = occ.date in notifications
                occ.is_rejected = r.is_rejected
                occ.is_cancelled = r.is_cancelled or occ.date in excluded_days
                occ.rejection_reason = (
                    convert_to_unicode(occurrence_rejection_reasons[occ.date])
                    if occ.date in occurrence_rejection_reasons else None)

            event_id = getattr(v, '_ReservationBase__owner', None)
            if hasattr(event_id, '_Impersistant__obj'):  # Impersistant object
                event_id = event_id._Impersistant__obj
            if event_id is not None:
                event = self.zodb_root['conferences'].get(event_id)
                if event:
                    # For some stupid reason there are bookings in the database which have a completely unrelated parent
                    guids = getattr(event, '_Conference__roomBookingGuids', [])
                    if any(
                            int(x.id) == v.id for x in guids
                            if x.id is not None):
                        r.event_id = int(event_id)
                    else:
                        print cformat(
                            '  %{red}event {} does not contain booking {}'
                        ).format(event_id, v.id)

            print cformat(
                '- [%{cyan}{}%{reset}/%{green!}{}%{reset}]  %{grey!}{}%{reset}  {}'
            ).format(room.location_name, room.name, r.id, r.created_dt.date())

            room.reservations.append(r)
            db.session.add(room)
            i = (i + 1) % 1000
            if not i:
                db.session.commit()
        db.session.commit()
Example #58
0
    def create_occurrences(self, skip_conflicts, user=None, allow_admin=True):
        ReservationOccurrence.create_series_for_reservation(self)
        db.session.flush()

        if user is None:
            user = self.created_by_user

        # Check for conflicts with nonbookable periods
        admin = allow_admin and rb_is_admin(user)
        if not admin 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,
                                          '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,
                                     allow_admin=allow_admin):
                continue
            for occurrence in self.occurrences:
                if occurrence.is_valid and blocking.is_active_at(
                        occurrence.start_dt.date()):
                    # Cancel OUR occurrence
                    msg = '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.items():
            if not occurrence.is_valid:
                continue
            if conflicts['confirmed']:
                if not skip_conflicts:
                    raise ConflictingOccurrences()
                # Cancel OUR occurrence
                msg = '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,
                        'Rejected due to collision with a confirmed reservation'
                    )
Example #59
0
 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)
Example #60
0
def get_rooms_conflicts(rooms,
                        start_dt,
                        end_dt,
                        repeat_frequency,
                        repeat_interval,
                        blocked_rooms,
                        nonbookable_periods,
                        unbookable_hours,
                        skip_conflicts_with=None,
                        allow_admin=False,
                        skip_past_conflicts=False):
    rooms_conflicts = defaultdict(set)
    rooms_pre_conflicts = defaultdict(set)
    rooms_conflicting_candidates = defaultdict(set)
    skip_conflicts_with = skip_conflicts_with or []

    candidates = ReservationOccurrence.create_series(
        start_dt, end_dt, (repeat_frequency, repeat_interval))
    room_ids = [room.id for room in rooms]
    query = (ReservationOccurrence.query.filter(
        Reservation.room_id.in_(room_ids), ReservationOccurrence.is_valid,
        ReservationOccurrence.filter_overlap(candidates)).join(
            ReservationOccurrence.reservation).options(
                ReservationOccurrence.NO_RESERVATION_USER_STRATEGY,
                contains_eager(ReservationOccurrence.reservation)))

    if skip_conflicts_with:
        query = query.filter(~Reservation.id.in_(skip_conflicts_with))
    if skip_past_conflicts:
        query = query.filter(ReservationOccurrence.start_dt > datetime.now())

    overlapping_occurrences = group_list(
        query, key=lambda obj: obj.reservation.room.id)
    for room_id, occurrences in overlapping_occurrences.iteritems():
        conflicts = get_room_bookings_conflicts(candidates, occurrences,
                                                skip_conflicts_with)
        rooms_conflicts[room_id], rooms_pre_conflicts[
            room_id], rooms_conflicting_candidates[room_id] = conflicts
    for room_id, occurrences in blocked_rooms.iteritems():
        conflicts, conflicting_candidates = get_room_blockings_conflicts(
            room_id, candidates, occurrences)
        rooms_conflicts[room_id] |= conflicts
        rooms_conflicting_candidates[room_id] |= conflicting_candidates

    if not (allow_admin and rb_is_admin(session.user)):
        for room_id, occurrences in nonbookable_periods.iteritems():
            room = Room.get_one(room_id)
            if not room.can_override(session.user, allow_admin=allow_admin):
                conflicts, conflicting_candidates = get_room_nonbookable_periods_conflicts(
                    candidates, occurrences)
                rooms_conflicts[room_id] |= conflicts
                rooms_conflicting_candidates[room_id] |= conflicting_candidates

        for room_id, occurrences in unbookable_hours.iteritems():
            room = Room.get_one(room_id)
            if not room.can_override(session.user, allow_admin=allow_admin):
                conflicts, conflicting_candidates = get_room_unbookable_hours_conflicts(
                    candidates, occurrences)
                rooms_conflicts[room_id] |= conflicts
                rooms_conflicting_candidates[room_id] |= conflicting_candidates
    rooms_conflicting_candidates = defaultdict(
        list, ((k, list(v)) for k, v in rooms_conflicting_candidates.items()))
    return rooms_conflicts, rooms_pre_conflicts, rooms_conflicting_candidates