Skip to content

Commit

Permalink
Merge pull request #92 from pennlabs/studyspaces
Browse files Browse the repository at this point in the history
Add booking endpoint for studyspaces
  • Loading branch information
ezwang authored Feb 8, 2018
2 parents 1346ffb + 872d76d commit eeac230
Show file tree
Hide file tree
Showing 10 changed files with 168 additions and 23 deletions.
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Penn Labs
- Geoff Vedernikoff `@yefim <https://github.com/yefim>`_
- Ceasar Bautista `@Ceasar <https://github.com/Ceasar>`_
- Brandon Lin `@esqu1 <https://github.com/esqu1>`_
- Eric Wang `@ezwang <https://github.com/ezwang>`_

Patches and Suggestions
```````````````````````
Expand Down
2 changes: 1 addition & 1 deletion LICENSE.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
The MIT License (MIT)

Copyright (c) 2016 The University of Pennsylvania
Copyright (c) 2018 The University of Pennsylvania

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
Expand Down
2 changes: 1 addition & 1 deletion docs/calendar.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.. _calendar:

Penn Academic Calendar API
=====================
==========================

.. module:: penn.calendar3year

Expand Down
2 changes: 1 addition & 1 deletion penn/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = '1.6.7'
__version__ = '1.6.8'

from .registrar import Registrar
from .directory import Directory
Expand Down
2 changes: 1 addition & 1 deletion penn/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def _request(self, url, params=None):
"""Make a signed request to the API, raise any API errors, and
returning a tuple of (data, metadata)
"""
response = get(url, params=params, headers=self.headers, timeout=30)
response = get(url, params=params, headers=self.headers, timeout=60)
if response.status_code != 200:
raise ValueError('Request to {} returned {}'
.format(response.url, response.status_code))
Expand Down
13 changes: 7 additions & 6 deletions penn/laundry.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import re
import os
import csv
import requests
Expand Down Expand Up @@ -38,7 +37,7 @@ def __init__(self):

def create_hall_to_link_mapping(self):
"""
:return: Mapping from hall name to associated link in SUDS. Creates inverted index from id to hall
:return: Mapping from hall name to associated link in SUDS. Creates inverted index from id to hall.
"""
laundry_path = pkg_resources.resource_filename("penn", "data/laundry.csv")
with open(laundry_path, "r") as f:
Expand Down Expand Up @@ -76,6 +75,12 @@ def update_machine_object(cols, machine_object):
return machine_object

def parse_a_hall(self, hall):
"""Return names, hall numbers, and the washers/dryers available for a certain hall.
:param hall:
The ID of the hall to retrieve data for.
:type hall: int
"""
if hall not in self.hall_to_link:
return None # change to to empty json idk
page = requests.get(self.hall_to_link[hall])
Expand Down Expand Up @@ -110,10 +115,6 @@ def parse_a_hall(self, hall):
machines = {"washers": washers, "dryers": dryers, "details": detailed}
return machines

@staticmethod
def get_hall_no(href):
return int(re.search(r"Halls=(\d+)", href).group(1))

def all_status(self):
"""Return names, hall numbers, and the washers/dryers available for all
rooms in the system
Expand Down
111 changes: 106 additions & 5 deletions penn/studyspaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,101 @@ def get_buildings(self):

soup = BeautifulSoup(requests.get("{}/spaces".format(BASE_URL)).content, "html5lib")
options = soup.find("select", {"id": "lid"}).find_all("option")
return [{"id": int(opt["value"]), "name": str(opt.text), "service": "libcal"} for opt in options]
return [{"id": int(opt["value"]), "name": str(opt.text), "service": "libcal"} for opt in options if int(opt["value"]) > 0]

def book_room(self, building, room, start, end, firstname, lastname, email, groupname, phone, size, fake=False):
"""Books a room given the required information.
:param building:
The ID of the building the room is in.
:type building: int
:param room:
The ID of the room to book.
:type room: int
:param start:
The start time range of when to book the room.
:type start: datetime
:param end:
The end time range of when to book the room.
:type end: datetime
:param fake:
If this is set to true, don't actually book the room. Default is false.
:type fake: bool
:returns:
Boolean indicating whether the booking succeeded or not.
:raises ValueError:
If one of the fields is missing or incorrectly formatted, or if the server fails to book the room.
"""

data = {
"formData[fname]": firstname,
"formData[lname]": lastname,
"formData[email]": email,
"formData[nick]": groupname,
"formData[q2533]": phone,
"formData[q2555]": size,
"forcedEmail": ""
}

try:
room_obj = self.get_room_id_name_mapping(building)[room]
except KeyError:
raise ValueError("No room with id {} found in building with id {}!".format(room, building))

if room_obj["lid"] != building:
raise ValueError("Mismatch between building IDs! (expected {}, got {})".format(building, room_obj["lid"]))

room_data = {
"id": 1,
"eid": room,
"gid": room_obj["gid"],
"lid": room_obj["lid"],
"start": start.strftime("%Y-%m-%d %H:%M"),
"end": end.strftime("%Y-%m-%d %H:%M")
}

for key, val in room_data.items():
data["bookings[0][{}]".format(key)] = val

if fake:
return True

resp = requests.post("{}/ajax/space/book".format(BASE_URL), data)
resp_data = resp.json()
if resp_data.get("success"):
return True
else:
if "error" in resp_data:
raise ValueError(resp_data["error"])
else:
raise ValueError(re.sub('<.*?>', '', resp_data.get("msg").strip()).strip())

@staticmethod
def parse_date(date):
"""Converts library system dates into timezone aware Python datetime objects."""
"""Converts library system dates into timezone aware Python datetime objects.
:param date:
A library system date in the format '2018-01-25 12:30:00'.
:type date: datetime
:returns:
A timezone aware python datetime object.
"""

date = datetime.datetime.strptime(date, "%Y-%m-%d %H:%M:%S")
return pytz.timezone("US/Eastern").localize(date)

@staticmethod
def get_room_id_name_mapping(building):
""" Returns a dictionary mapping id to name, thumbnail, and capacity. """
"""Returns a dictionary mapping id to name, thumbnail, and capacity.
The dictionary also contains information about the lid and gid, which are used in the booking process.
:param building:
The ID of the building to fetch rooms for.
:type building: int
:returns:
A list of rooms, with each item being a dictionary that contains the room id and available times.
"""

data = requests.get("{}/spaces?lid={}".format(BASE_URL, building)).content.decode("utf8")
# find all of the javascript room definitions
Expand All @@ -59,15 +142,33 @@ def get_room_id_name_mapping(building):
thumbnail = "https:" + thumbnail

room_id = int(items["eid"])
room_gid = int(items["gid"])
room_lid = int(items["lid"])

out[room_id] = {
"name": title,
"gid": room_gid,
"lid": room_lid,
"thumbnail": thumbnail or None,
"capacity": int(items["capacity"])
}
return out

def get_rooms(self, building, start, end):
"""Returns a dictionary matching all rooms given a building id and a date range."""
"""Returns a dictionary matching all rooms given a building id and a date range.
The resulting dictionary contains both rooms that are available and rooms that already have been booked.
:param building:
The ID of the building to fetch rooms for.
:type building: int
:param start:
The start date of the range used to filter available rooms.
:type start: datetime
:param end:
The end date of the range used to filter available rooms.
:type end: datetime
"""

if start.tzinfo is None:
start = pytz.timezone("US/Eastern").localize(start)
Expand Down Expand Up @@ -105,5 +206,5 @@ def get_rooms(self, building, start, end):
}
if k in mapping:
item.update(mapping[k])
out.append(item)
out.append(item)
return out
6 changes: 3 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
beautifulsoup4==4.6.0
html5lib==0.999
html5lib==1.0.1
mock==2.0.0
nameparser==0.4.0
nameparser==0.5.6
nose==1.3.7
pytz==2017.3
requests==2.4.3
requests==2.18.4
six==1.11.0
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
url='https://github.com/pennlabs/penn-sdk-python',
author='Penn Labs',
author_email='[email protected]',
version='1.6.7',
version='1.6.8',
packages=['penn'],
license='MIT',
package_data={
Expand Down
50 changes: 46 additions & 4 deletions tests/studyspaces_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import datetime
import pytz

from nose.tools import ok_
from penn import StudySpaces
Expand All @@ -13,12 +12,55 @@ def setUp(self):
def test_buildings(self):
buildings = self.studyspaces.get_buildings()
ok_(len(buildings) > 0)
for building in buildings:
ok_(building["id"] > 0)
ok_(building["name"])
ok_(building["service"])

def test_room_name_mapping(self):
mapping = self.studyspaces.get_room_id_name_mapping(2683)
ok_(len(mapping) > 0)

def test_rooms(self):
now = pytz.timezone("US/Eastern").localize(datetime.datetime.now())
rooms = self.studyspaces.get_rooms(2683, now, now + datetime.timedelta(days=3))
ok_(len(rooms) > 0)
""" Make sure that at least 3 buildings have at least one room. """

now = datetime.datetime.now()
buildings = self.studyspaces.get_buildings()
for building in buildings[:3]:
rooms = self.studyspaces.get_rooms(building["id"], now, now + datetime.timedelta(days=3))
ok_(len(rooms) > 0, "The building {} does not have any rooms!".format(building))
for room in rooms:
ok_(room["room_id"] > 0)
ok_(len(room["times"]) > 0)

def test_booking(self):
""" Test the checks before booking the room, but don't actually book a room. """

buildings = self.studyspaces.get_buildings()
# get the first building
building_id = buildings[0]["id"]

now = datetime.datetime.now()
rooms = self.studyspaces.get_rooms(building_id, now, now + datetime.timedelta(days=1))

if not rooms:
return

# get the first room
room_id = rooms[0]["room_id"]
room_time = rooms[0]["times"][0]

result = self.studyspaces.book_room(
building_id,
room_id,
datetime.datetime.strptime(room_time["start"][:-6], "%Y-%m-%dT%H:%M:%S"),
datetime.datetime.strptime(room_time["end"][:-6], "%Y-%m-%dT%H:%M:%S"),
"John",
"Doe",
"[email protected]",
"Test Meeting",
"000-000-0000",
"2-3",
fake=True
)
ok_(result)

0 comments on commit eeac230

Please sign in to comment.