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
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()
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
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
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
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)
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
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
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()
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()
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()
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()
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)]
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')
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 _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
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
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()
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
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
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
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)
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'])
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 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 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())
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()
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()
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()
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
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))
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
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
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 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))
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
def test_date(dummy_occurrence): assert dummy_occurrence.date == date.today() assert ReservationOccurrence.find_first( date=date.today()) == dummy_occurrence
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()
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' )
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 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