def split_booking(booking, new_booking_data): is_ongoing_booking = booking.start_dt.date() < date.today() < booking.end_dt.date() if not is_ongoing_booking: return cancelled_dates = [] rejected_occs = {} room = booking.room occurrences = sorted(booking.occurrences, key=attrgetter('start_dt')) old_frequency = booking.repeat_frequency occurrences_to_cancel = [occ for occ in occurrences if occ.start_dt >= datetime.now() and occ.is_valid] if old_frequency != RepeatFrequency.NEVER and new_booking_data['repeat_frequency'] == RepeatFrequency.NEVER: new_start_dt = new_booking_data['start_dt'] else: new_start_dt = datetime.combine(occurrences_to_cancel[0].start_dt.date(), new_booking_data['start_dt'].time()) cancelled_dates = [occ.start_dt.date() for occ in occurrences if occ.is_cancelled] rejected_occs = {occ.start_dt.date(): occ.rejection_reason for occ in occurrences if occ.is_rejected} new_end_dt = [occ for occ in occurrences if occ.start_dt < datetime.now()][-1].end_dt old_booking_data = { 'booking_reason': booking.booking_reason, 'booked_for_user': booking.booked_for_user, 'start_dt': booking.start_dt, 'end_dt': new_end_dt, 'repeat_frequency': booking.repeat_frequency, 'repeat_interval': booking.repeat_interval, } booking.modify(old_booking_data, session.user) for occurrence_to_cancel in occurrences_to_cancel: occurrence_to_cancel.cancel(session.user, silent=True) prebook = not room.can_book(session.user, allow_admin=False) and room.can_prebook(session.user, allow_admin=False) resv = Reservation.create_from_data(room, dict(new_booking_data, start_dt=new_start_dt), session.user, prebook=prebook, ignore_admin=True) for new_occ in resv.occurrences: new_occ_start = new_occ.start_dt.date() if new_occ_start in cancelled_dates: new_occ.cancel(None, silent=True) if new_occ_start in rejected_occs: new_occ.reject(None, rejected_occs[new_occ_start], silent=True) booking.edit_logs.append(ReservationEditLog(user_name=session.user.full_name, info=[ 'Split into a new booking', f'booking_link:{resv.id}' ])) resv.edit_logs.append(ReservationEditLog(user_name=session.user.full_name, info=[ 'Split from another booking', f'booking_link:{booking.id}' ])) return resv
def reject(self, user, reason, silent=False): self.is_rejected = True self.rejection_reason = reason self.occurrences.filter_by(is_valid=True).update({'is_rejected': True, 'rejection_reason': reason}, synchronize_session='fetch') if not silent: notify_rejection(self) log_msg = u'Reservation rejected: {}'.format(reason) self.add_edit_log(ReservationEditLog(user_name=user.full_name, info=[log_msg]))
def reject(self, user, reason, silent=False): self.is_rejected = True self.rejection_reason = reason if not silent: log = [u'Day rejected: {}'.format(format_date(self.date).decode('utf-8')), u'Reason: {}'.format(reason)] self.reservation.add_edit_log(ReservationEditLog(user_name=user.full_name, info=log)) from indico.modules.rb.notifications.reservation_occurrences import notify_rejection notify_rejection(self)
def reject(self, user, reason, silent=False): self.state = ReservationOccurrenceState.rejected self.rejection_reason = reason or None signals.rb.booking_occurrence_state_changed.send(self) if not silent: log = ['Day rejected: {}'.format(format_date(self.date)), f'Reason: {reason}'] self.reservation.add_edit_log(ReservationEditLog(user_name=user.full_name, info=log)) from indico.modules.rb.notifications.reservation_occurrences import notify_rejection notify_rejection(self)
def cancel(self, user, reason=None, silent=False): self.state = ReservationOccurrenceState.cancelled self.rejection_reason = reason or None if not silent: log = [u'Day cancelled: {}'.format(format_date(self.date).decode('utf-8'))] if reason: log.append(u'Reason: {}'.format(reason)) self.reservation.add_edit_log(ReservationEditLog(user_name=user.full_name, info=log)) from indico.modules.rb.notifications.reservation_occurrences import notify_cancellation notify_cancellation(self)
def reject(self, user, reason, silent=False): self.state = ReservationState.rejected self.rejection_reason = reason or None self.occurrences.filter_by(is_valid=True).update({ ReservationOccurrence.state: ReservationOccurrenceState.rejected, ReservationOccurrence.rejection_reason: reason }, synchronize_session='fetch') if not silent: notify_rejection(self) log_msg = u'Reservation rejected: {}'.format(reason) self.add_edit_log(ReservationEditLog(user_name=user.full_name, info=[log_msg]))
def cancel(self, user, reason=None, silent=False): self.is_cancelled = True self.rejection_reason = reason if not silent: log = [u'Day cancelled: {}'.format(format_date(self.date))] if reason: log.append(u'Reason: {}'.format(reason)) self.reservation.add_edit_log( ReservationEditLog(user_name=user.getFullName(), info=log)) from indico.modules.rb.notifications.reservation_occurrences import notify_cancellation notify_cancellation(self)
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 reject(self, user, reason, silent=False): self.state = ReservationState.rejected self.rejection_reason = reason or None self.occurrences.filter_by(is_valid=True).update({ ReservationOccurrence.state: ReservationOccurrenceState.rejected, ReservationOccurrence.rejection_reason: reason }, synchronize_session='fetch') signals.rb.booking_state_changed.send(self) if not silent: notify_rejection(self) log_msg = f'Reservation rejected: {reason}' self.add_edit_log(ReservationEditLog(user_name=user.full_name, info=[log_msg]))
def cancel(self, user, reason=None, silent=False): self.state = ReservationOccurrenceState.cancelled self.rejection_reason = reason or None signals.rb.booking_occurrence_state_changed.send(self) if not silent: log = [f'Day cancelled: {format_date(self.date)}'] if reason: log.append(f'Reason: {reason}') self.reservation.add_edit_log( ReservationEditLog(user_name=user.full_name, info=log)) from indico.modules.rb.notifications.reservation_occurrences import notify_cancellation notify_cancellation(self)
def accept(self, user, reason=None): self.state = ReservationState.accepted if reason: log_msg = f'Reservation accepted: {reason}' else: log_msg = 'Reservation accepted' self.add_edit_log(ReservationEditLog(user_name=user.full_name, info=[log_msg])) notify_confirmation(self, reason) signals.rb.booking_state_changed.send(self) pre_occurrences = get_prebooking_collisions(self) for occurrence in pre_occurrences: occurrence.reject(user, 'Rejected due to collision with a confirmed reservation')
def cancel(self, user, reason=None, silent=False): self.state = ReservationState.cancelled self.rejection_reason = reason or None criteria = (ReservationOccurrence.is_valid, ReservationOccurrence.is_within_cancel_grace_period) self.occurrences.filter(*criteria).update({ ReservationOccurrence.state: ReservationOccurrenceState.cancelled, ReservationOccurrence.rejection_reason: reason }, synchronize_session='fetch') signals.rb.booking_state_changed.send(self) if not silent: notify_cancellation(self) log_msg = u'Reservation cancelled: {}'.format(reason) if reason else 'Reservation cancelled' self.add_edit_log(ReservationEditLog(user_name=user.full_name, info=[log_msg]))
def cancel(self, user, reason=None, silent=False): self.is_cancelled = True self.rejection_reason = reason self.occurrences.filter_by(is_valid=True).update( { 'is_cancelled': True, 'rejection_reason': reason }, synchronize_session='fetch') if not silent: notify_cancellation(self) log_msg = u'Reservation cancelled: {}'.format( reason) if reason else 'Reservation cancelled' self.add_edit_log( ReservationEditLog(user_name=user.getFullName(), info=[log_msg]))
def 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 modify(self, data, user): """Modifies an existing reservation. :param data: A dict containing the booking data, usually from a :class:`ModifyBookingForm` instance :param user: The :class:`.User` who modifies the booking. """ populate_fields = ('start_dt', 'end_dt', 'repeat_frequency', 'repeat_interval', 'booked_for_user', 'booking_reason') # fields affecting occurrences occurrence_fields = {'start_dt', 'end_dt', 'repeat_frequency', 'repeat_interval'} # fields where date and time are compared separately date_time_fields = {'start_dt', 'end_dt'} # fields for the repetition repetition_fields = {'repeat_frequency', 'repeat_interval'} # pretty names for logging field_names = { 'start_dt/date': u"start date", 'end_dt/date': u"end date", 'start_dt/time': u"start time", 'end_dt/time': u"end time", 'repetition': u"booking type", 'booked_for_user': u"'Booked for' user", 'booking_reason': u"booking reason", } self.room.check_advance_days(data['end_dt'].date(), user) self.room.check_bookable_hours(data['start_dt'].time(), data['end_dt'].time(), user) changes = {} update_occurrences = False old_repetition = self.repetition for field in populate_fields: if field not in data: continue old = getattr(self, field) new = data[field] converter = unicode if old != new: # Booked for user updates the (redundant) name if field == 'booked_for_user': old = self.booked_for_name new = self.booked_for_name = data[field].full_name # Apply the change setattr(self, field, data[field]) # If any occurrence-related field changed we need to recreate the occurrences if field in occurrence_fields: update_occurrences = True # Record change for history entry if field in date_time_fields: # The date/time fields create separate entries for the date and time parts if old.date() != new.date(): changes[field + '/date'] = {'old': old.date(), 'new': new.date(), 'converter': format_date} if old.time() != new.time(): changes[field + '/time'] = {'old': old.time(), 'new': new.time(), 'converter': format_time} elif field in repetition_fields: # Repetition needs special handling since it consists of two fields but they are tied together # We simply update it whenever we encounter such a change; after the last change we end up with # the correct change data changes['repetition'] = {'old': old_repetition, 'new': self.repetition, 'converter': lambda x: RepeatMapping.get_message(*x)} else: changes[field] = {'old': old, 'new': new, 'converter': converter} if not changes: return False # Create a verbose log entry for the modification log = [u'Booking modified'] for field, change in changes.iteritems(): field_title = field_names.get(field, field) converter = change['converter'] old = to_unicode(converter(change['old'])) new = to_unicode(converter(change['new'])) if not old: log.append(u"The {} was set to '{}'".format(field_title, new)) elif not new: log.append(u"The {} was cleared".format(field_title)) else: log.append(u"The {} was changed from '{}' to '{}'".format(field_title, old, new)) self.edit_logs.append(ReservationEditLog(user_name=user.full_name, info=log)) # Recreate all occurrences if necessary if update_occurrences: cols = [col.name for col in ReservationOccurrence.__table__.columns if not col.primary_key and col.name not in {'start_dt', 'end_dt'}] old_occurrences = {occ.date: occ for occ in self.occurrences} self.occurrences.delete(synchronize_session='fetch') self.create_occurrences(True, user) db.session.flush() # Restore rejection data etc. for recreated occurrences for occurrence in self.occurrences: old_occurrence = old_occurrences.get(occurrence.date) # Copy data from old occurrence UNLESS the new one is invalid (e.g. because of collisions) # Otherwise we'd end up with valid occurrences ignoring collisions! if old_occurrence and occurrence.is_valid: for col in cols: setattr(occurrence, col, getattr(old_occurrence, col)) # Don't cause new notifications for the entire booking in case of daily repetition if self.repeat_frequency == RepeatFrequency.DAY and all(occ.notification_sent for occ in old_occurrences.itervalues()): for occurrence in self.occurrences: occurrence.notification_sent = True # Sanity check so we don't end up with an "empty" booking if not any(occ.is_valid for occ in self.occurrences): raise NoReportError(_(u'Reservation has no valid occurrences')) notify_modification(self, changes) return True
def reset_approval(self, user): self.state = ReservationState.pending notify_reset_approval(self) self.add_edit_log(ReservationEditLog(user_name=user.full_name, info=['Requiring new approval due to change']))
def 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 test_add_edit_log(db, dummy_reservation): dummy_reservation.add_edit_log( ReservationEditLog(user_name='user', info='Some change')) assert dummy_reservation.edit_logs.count() == 1