def run_reserve( self, data, approve_manually, dates=None, group=None, quota=1, rrule=None, description=None ): assert dates or group assert not (dates and group) email = self.email(data) session_id = self.session_id() additional_data = self.additional_data(data, add_manager_defaults=True) # only store forms defined in the formsets list additional_data = dict( ( form, additional_data[form] ) for form in self.context.formsets if form in additional_data ) if dates: for start, end in utils.pairs(dates): run_pre_reserve_script( self.context, start, end, additional_data ) else: run_pre_reserve_script(self.context, None, None, additional_data) def run(): if dates: return self.scheduler.reserve( email, dates, data=additional_data, session_id=session_id, quota=quota, rrule=rrule, description=description ) else: return self.scheduler.reserve( email, group=group, data=additional_data, session_id=session_id, quota=quota, description=description ) token = throttled(run, 'reserve')() if approve_manually: self.flash(_(u'Added to waitinglist')) else: self.scheduler.approve_reservation(token) self.flash(_(u'Reservation successful'))
def reserve(self, email, dates=None, group=None, data=None, session_id=None, quota=1): """ First step of the reservation. Seantis.reservation uses a two-step reservation process. The first step is reserving what is either an open spot or a place on the waiting list. The second step is to actually write out the reserved slots, which is done by approving an existing reservation. Most checks are done in the reserve functions. The approval step only fails if there's no open spot. This function returns a reservation token which can be used to approve the reservation in approve_reservation. """ assert (dates or group) and not (dates and group) validate_email(email) if group: dates = self.dates_by_group(group) dates = utils.pairs(dates) # First, the request is checked for saneness. If any requested # date cannot be reserved the request as a whole fails. for start, end in dates: # are the parameters valid? if abs((end - start).days) >= 1: raise ReservationTooLong if start > end or (end - start).seconds < 5 * 60: raise ReservationParametersInvalid # can all allocations be reserved? for allocation in self.allocations_in_range(start, end): # start and end are not rasterized, so we need this check if not allocation.overlaps(start, end): continue assert allocation.is_master # with manual approval the reservation ends up on the # waitinglist and does not yet need a spot if not allocation.approve_manually: if not self.find_spot(allocation, start, end): raise AlreadyReservedError free = self.free_allocations_count(allocation, start, end) if free < quota: raise AlreadyReservedError if allocation.reservation_quota_limit > 0: if allocation.reservation_quota_limit < quota: raise QuotaOverLimit if allocation.quota < quota: raise QuotaImpossible if quota < 1: raise InvalidQuota # ok, we're good to go token = new_uuid() found = 0 # groups are reserved by group-identifier - so all members of a group # or none of them. As such there's no start / end date which is defined # implicitly by the allocation if group: found = 1 reservation = Reservation() reservation.token = token reservation.target = group reservation.status = u'pending' reservation.target_type = u'group' reservation.resource = self.uuid reservation.data = data reservation.session_id = session_id reservation.email = email reservation.quota = quota Session.add(reservation) else: groups = [] for start, end in dates: for allocation in self.allocations_in_range(start, end): if not allocation.overlaps(start, end): continue found += 1 reservation = Reservation() reservation.token = token reservation.start, reservation.end = rasterize_span( start, end, allocation.raster ) reservation.target = allocation.group reservation.status = u'pending' reservation.target_type = u'allocation' reservation.resource = self.uuid reservation.data = data reservation.session_id = session_id reservation.email = email reservation.quota = quota Session.add(reservation) groups.append(allocation.group) # check if no group reservation is made with this request. # reserve by group in this case (or make this function # do that automatically) assert len(groups) == len(set(groups)), \ 'wrongly trying to reserve a group' if found: notify(ReservationMadeEvent(reservation, self.language)) else: raise InvalidReservationError return token
def allocate(self, dates, raster=15, quota=None, partly_available=False, grouped=False, approve_manually=True, reservation_quota_limit=0, whole_day=False ): """Allocates a spot in the calendar. An allocation defines a timerange which can be reserved. No reservations can exist outside of existing allocations. In fact any reserved slot will link to an allocation. An allocation may be available as a whole (to reserve all or nothing). It may also be partly available which means reservations can be made for parts of the allocation. If an allocation is partly available a raster defines the granularity with which a reservation can be made (e.g. a raster of 15min will ensure that reservations are at least 15 minutes long and start either at :00, :15, :30 or :45) The reason for the raster is mainly to ensure that different reservations trying to reserve overlapping times need the same keys in the reserved_slots table, ensuring integrity at the database level. Allocations may have a quota, which determines how many times an allocation may be reserved. Quotas are enabled using a master-mirrors relationship. The master is the first allocation to be created. The mirrors copies of that allocation. See Scheduler.__doc__ """ dates = utils.pairs(dates) group = new_uuid() quota = quota or 1 # if the allocation is not partly available the raster is set to lowest # possible raster value raster = partly_available and raster or MIN_RASTER_VALUE # the whole day option results in the dates being aligned to # the beginning of the day / end of it -> not timezone aware! if whole_day: for ix, (start, end) in enumerate(dates): dates[ix] = utils.align_range_to_day(start, end) # Ensure that the list of dates contains no overlaps inside for start, end in dates: if utils.count_overlaps(dates, start, end) > 1: raise InvalidAllocationError # Make sure that this span does not overlap another master for start, end in dates: start, end = rasterize_span(start, end, raster) existing = self.allocations_in_range(start, end).first() if existing: raise OverlappingAllocationError(start, end, existing) # Write the master allocations allocations = [] for start, end in dates: allocation = Allocation(raster=raster) allocation.start = start allocation.end = end allocation.resource = self.uuid allocation.quota = quota allocation.mirror_of = self.uuid allocation.partly_available = partly_available allocation.approve_manually = approve_manually allocation.reservation_quota_limit = reservation_quota_limit if grouped: allocation.group = group else: allocation.group = new_uuid() allocations.append(allocation) Session.add_all(allocations) return allocations
def test_pairs(self): one = ('aa', 'bb', 'cc', 'dd') two = (('aa', 'bb'), ('cc', 'dd')) self.assertEqual(utils.pairs(one), utils.pairs(two))
def test_pairs(self): one = ("aa", "bb", "cc", "dd") two = (("aa", "bb"), ("cc", "dd")) self.assertEqual(utils.pairs(one), utils.pairs(two))