This section includes several examples of sneq that are used to solve real problems around scheduling.
You have a site and you would like to prevent last minute reservations for the following day. "Last minute" is considered to be after 5 PM, and you only want to restrict bookings for the next day after 5 PM in the same day.
# Define the datetimes that will provide "borders"
# for determining if the reservation takes place
# tomorrow.
now = datetime.now()
tomorrow = datetime.add(
dt=datetime.end_of(dt=now, unit='day'),
seconds=1)
day_after_tomorrow = datetime.add(
dt=datetime.end_of(dt=now, unit='day'),
days=1,
seconds=1)
# For each resource in the reservation...
for resv in util.reservations.all():
# Is the reservation start time tomorrow and is
# the current time greater than 5 PM?
if resv['start'] >= tomorrow and \
resv['start'] < day_after_tomorrow and \
datetime.info(dt=now)['hour'] >= 17:
return Exception('Reservations for the next day ' +
'must be booked before 5 PM today.')
return True
You have a site and you would like that all resources would have only one booking per day per user. The following script can achieve this outcome:
# Check the consumption of an individual reservation.
def check_consumption(reservation=None):
MAX_UNITS_PER_DAY = 1
reserved_for_id = reservation['reserved_for']['user_id']
reservable_id = reservation['reservable']['reservable_id']
start_of_day = datetime.start_of(dt=reservation['start'], unit='day')
end_of_day = datetime.end_of(dt=reservation['end'], unit='day')
# This returns a <dec> if reservable_id filter is given.
units = api.reservations.units_consumed(
reservable_id=reservable_id,
user_id=reserved_for_id,
start=start_of_day,
end=end_of_day)
if units > MAX_UNITS_PER_DAY:
return False
return True
# Iterate through all the reservations. It will be a single
# reservation if this is a single-resource reservation, or
# all the child reservations if this is a multi-resource
# reservation.
for resv in util.reservations.all():
if not check_consumption(reservation=resv):
return Exception('Can only book one unit per day.')
return True
One "gotcha" with this script is that if another resource doesn't have this script set, and you add it to a reservation containing a resource that does have this script yet, this script will apply to all resources in the reservation. Keep in mind, a script attached to a resource will apply to the whole reservation, not just a single resource reservation in the reservation. You can restrict it to a group of resources for multi-resource reservations by using an API call to fetch the tags
for a resource.
Here is the script adjusted so that it only affects resources that have the tag "one-per-day". Note that tags are case sensitive, but you could match case insensitive using the util.strings
library!
# Check the consumption of an individual reservation.
def check_consumption(reservation=None):
MAX_UNITS_PER_DAY = 1
# Fetch a list of tags for a resource, then see if the
# resource has the "one-per-day" tag on it.
def resource_has_restriction_tag(reservable_id=None):
tags = api.resources.tags(reservable_id=reservable_id)
return "one-per-day" in tags
reserved_for_id = reservation['reserved_for']['user_id']
reservable_id = reservation['reservable']['reservable_id']
if not resource_has_restriction_tag(reservable_id=reservable_id):
return True
start_of_day = datetime.start_of(dt=reservation['start'], unit='day')
end_of_day = datetime.end_of(dt=reservation['end'], unit='day')
# This returns a <dec> if reservable_id filter is given.
units = api.reservations.units_consumed(
reservable_id=reservable_id,
user_id=reserved_for_id,
start=start_of_day,
end=end_of_day)
if units > MAX_UNITS_PER_DAY:
return False
return True
# Iterate through all the reservations. It will be a single
# reservation if this is a single-resource reservation, or
# all the child reservations if this is a multi-resource
# reservation.
for resv in util.reservations.all():
if not check_consumption(reservation=resv):
return Exception('Can only book one unit per day.')
return True
Another important thing to keep in mind is that when the script is executed, all API calls are executed from that database perspective of the reservation having already been made. Hence when we query api.reservations.units_consumed
and set our units to 2 in the booking modal, the number of units consumed returned is 2
even though no previous reservations have been made. That is over the limit, so our script returns the conflict message.
You have a site with loanable resources and you would like the duration of all loans to be until the end of the semester. The semesters are defined arbitrarily. You would also like site moderators and administrators to be able to bypass the loan duration requirement.
# Moderators have no restrictions.
if user_logged_in['is_moderator'] == True:
return True
# Semesters MUST be listed in ascending chronological order.
# Old semesters can be deleted once you are past their end
# dates for the sake of loans.
# TODO Change me to actual semester dates!
semesters = [
{ # First semester
'start': datetime.from_date_and_time(
year=2021,
month=1,
day=1),
'end': datetime.from_date_and_time(
year=2021,
month=5,
day=15),
},
{ # Second semester
'start': datetime.from_date_and_time(
year=2021,
month=5,
day=15),
'end': datetime.from_date_and_time(
year=2021,
month=9,
day=1),
},
# ... add more semesters here!
]
# Find the first matching semester by start time.
# 'key' == a single semester dict.
def match_semester(key=None, start=reservation['start']):
if start >= key['start'] and start < key['end']:
return True
return False
current_semester = find(semesters, match_semester)
if isinstance(current_semester, 'none'):
return Exception('Not in a known semester.')
# If the end of the loan is where we want it to be (midnight
# ending of the semester), then the reservation is fine.
if reservation['end'] == current_semester['end']:
return True
# Else we lock the times and set the end to the end date.
return Exception({
'times': {
'lockEndPermanent': True,
'lockStartPermanent': True,
'end': current_semester['end'],
},
})
Form responses are a little harder to work with in sneq because they are only tested after the reservation has been completed. To emulate this in sneq, a button has been added in the testing modal.
After this is clicked, a conflict check with the forms will be executed. This will populate the form_responses
key in the reservation or request dictionary.
In order to know which fields to access in the form response data, you will need to know the ID of your form and your form field. The IDs of both can be found by going to the form editor and clicking the "Enable Advanced Mode" at the top.
The form ID appears near the top of the form editor:
The form field IDs will appear beside the form fields in the editor:
Using these we can find the individual field response in the reservation dictionary. The util.forms
library provides us with easy access to form response values. Our script will raise an exception if we set the value of field "A Text Input Field" to "foo".
form_response_value = util.forms.form_response_value_from_form_id_and_field_id(
form_id='50pc8hjgnhomku4l08rqgxetg9ektri5p70ih',
field_id='b0363d56-b682-471a-a049-fc502cdea0cb')
if form_response_value == 'foo':
return Exception('This form field may not be set to foo!')
return True
To test, go to the testing modal, click "Next" to get to the form, enter "foo" in the form field, then click "Trigger Conflict Check With Forms".
You might have a training resource that requires a related trainer to be booked with it, and you want to automate the process of finding and adding an available trainer. Scripts can be used to solve this as well. The script below will automatically add a random trainer to a training resource when a user selects the training for some time slot.
# Check and ensure that if there is more than one resource and a
# training is present, we have an appropriate trainer for that training.
if 'reservation_children' in reservation:
# Search for resources with training.
training_resource_ids = []
for child in reservation['reservation_children']:
if util.strings.match(s=child['reservable']['name'], test='*Training'):
training_resource_ids = training_resource_ids + [child['reservable']['reservable_id']]
# We have no training resources, so the user can book as is.
if len(training_resource_ids) == 0:
return True
# Ensure that there is one appropriate trainer for all the training
# sessions present.
for training_resource_id in training_resource_ids:
trainer_ids = api.resources.related(reservable_id=training_resource_id)
for child in reservation['reservation_children']:
num_trainers = 0
if child['reservable']['reservable_id'] in trainer_ids:
num_trainers = num_trainers + 1
if num_trainers == 0:
return Exception('One or more training sessions is missing an appropriate trainer.')
if num_trainers > 1:
return Exception('Too many trainers for one of the training sessions.')
return True
# We have one reason and it isn't a training. Abort.
if not util.strings.match(s=reservation['reservable']['name'], test='*Training'):
return True
# The function below finds a free resource from a list of resources
# and returns a random one, or None if none of them are free.
def find_free_resource(
start=None,
end=None,
reservable_ids=None,
user_id=reservation['reserved_for']['user_id']):
requested = [ [ rid, 1 ] for rid in reservable_ids ]
available = api.resources.available(
check_self_blocks=False,
end=end,
requested=requested,
start=start,
user_id=user_id)
if len(available['availability_combined']) == 0:
return None
return util.random.choice(lst=available['availability_combined'])
# Look for trainers. In this site, the trainers are related resources
# of the training resource.
trainer_ids = api.resources.related(reservable_id=reservation['reservable']['reservable_id'])
end_minus_one_sec = datetime.add(dt=reservation['end'], seconds=-1)
free_trainer_id = find_free_resource(start=reservation['start'],
end=end_minus_one_sec,
reservable_ids=trainer_ids)
if isinstance(free_trainer_id, 'none'):
return Exception('No trainer is available for this resource at the currently selected time and date.')
# Set the random trainer in the booking client, which will retrigger
# script execution and the verification code at the top of this script.
return Exception({
'reservables': [
{
'reservable_id': reservation['reservable']['reservable_id'],
},
{
'reservable_id': free_trainer_id,
},
],
})