Documentation

Example Reservation Scripts

Table of Contents

This section includes several examples of sneq that are used to solve real problems around scheduling.

Preventing last minute reservations for the following day

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

Restricting the number of bookings per day per user

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

example_one_per_day.png

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.

Restricting loans to an entire semester for users

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'],
  },
})

You want to block reservations based on a form response

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.

forms_testing_button.png

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:

form_id.png

The form field IDs will appear beside the form fields in the editor:

form_field_ids.png

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".

form_foo_blocked.png

Automated addition of a random, available trainer for a training session

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,
    },
  ],
})