-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #91 from pennlabs/studyspaces
Rebuild study spaces
- Loading branch information
Showing
12 changed files
with
146 additions
and
128 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -60,3 +60,5 @@ MANIFEST | |
docs/_build | ||
.pypirc | ||
.idea/ | ||
*.swp | ||
*.swo |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
__version__ = '1.6.6' | ||
__version__ = '1.6.7' | ||
|
||
from .registrar import Registrar | ||
from .directory import Directory | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,93 +1,109 @@ | ||
from bs4 import BeautifulSoup | ||
import requests | ||
import datetime | ||
import json | ||
import pytz | ||
import re | ||
import six | ||
|
||
from bs4 import BeautifulSoup | ||
|
||
|
||
BASE_URL = "http://libcal.library.upenn.edu" | ||
BASE_URL = "https://libcal.library.upenn.edu" | ||
|
||
|
||
class StudySpaces(object): | ||
"""Used for interacting with the UPenn library GSR booking system. | ||
Usage:: | ||
>>> from penn import StudySpaces | ||
>>> s = StudySpaces() | ||
""" | ||
|
||
def __init__(self): | ||
pass | ||
|
||
def get_buildings(self): | ||
"""Returns a list of building IDs, building names, and services.""" | ||
|
||
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] | ||
|
||
@staticmethod | ||
def parse_date(date): | ||
"""Converts library system dates into timezone aware Python datetime objects.""" | ||
|
||
date = datetime.datetime.strptime(date, "%Y-%m-%d %H:%M:%S") | ||
return pytz.timezone("US/Eastern").localize(date) | ||
|
||
@staticmethod | ||
def date_parse(original): | ||
"""Parses the date to dashed format. | ||
:param original: string with date in the format MM/DD/YYYY. | ||
""" | ||
l = original.split("-") | ||
final = [l[1], l[2], l[0]] | ||
return '-'.join(final) | ||
|
||
def get_id_json(self): | ||
"""Makes JSON with each element associating URL, ID, and building | ||
name. | ||
""" | ||
group_study_codes = [] | ||
url = BASE_URL + "/booking/vpdlc" | ||
soup = BeautifulSoup(requests.get(url).text, 'html5lib') | ||
l = soup.find('select', {'id': 'lid'}).find_all('option') | ||
for element in l: | ||
if element['value'] != '0': | ||
url2 = "{}/spaces?lid={}".format(BASE_URL, str(element['value'])) | ||
new_dict = {} | ||
new_dict['id'] = int(str(element['value'])) | ||
new_dict['name'] = str(element.contents[0]) | ||
new_dict['url'] = url2 | ||
group_study_codes.append(new_dict) | ||
return group_study_codes | ||
|
||
def get_id_dict(self): | ||
"""Extracts the ID's of the room into a dictionary. Used as a | ||
helper for the extract_times method. | ||
""" | ||
group_study_codes = {} | ||
url = BASE_URL + "/booking/vpdlc" | ||
soup = BeautifulSoup(requests.get(url).text, 'html5lib') | ||
options = soup.find('select', {'id': 'lid'}).find_all('option') | ||
for element in options: | ||
if element['value'] != '0': | ||
group_study_codes[int(str(element['value']))] = str(element.contents[0]) | ||
return group_study_codes | ||
|
||
def extract_times(self, id, date, name): | ||
"""Scrapes the avaiable rooms with the given ID and date. | ||
:param id: the ID of the building | ||
:param date: the date to acquire available rooms from | ||
:param name: the name of the building; obtained via get_id_dict | ||
""" | ||
url = BASE_URL + "/rooms_acc.php?gid=%s&d=%s&cap=0" % (int(id), date) | ||
soup = BeautifulSoup(requests.get(url).text, 'html5lib') | ||
|
||
time_slots = soup.find_all('form') | ||
unparsed_rooms = time_slots[1].contents[2:-2] | ||
|
||
roomTimes = [] | ||
|
||
for i in unparsed_rooms: | ||
room = BeautifulSoup(str(i), 'html5lib') | ||
try: | ||
# extract room names | ||
roomName = room.fieldset.legend.h2.contents[0] | ||
except AttributeError: | ||
# in case the contents aren't a list | ||
continue | ||
newRoom = str(roomName)[:-1] | ||
times = [] | ||
|
||
filtered = room.fieldset.find_all('label') | ||
|
||
for t in filtered: | ||
# getting the individual times for each room | ||
dict_item = {} | ||
dict_item['room_name'] = newRoom | ||
time = str(t).split("\t\t\t\t\t")[2][1:-1] | ||
times.append(time) | ||
startAndEnd = time.split(" - ") | ||
dict_item['start_time'] = startAndEnd[0].upper() | ||
dict_item['end_time'] = startAndEnd[1].upper() | ||
roomTimes.append(dict_item) | ||
dict_item['date'] = self.date_parse(date) | ||
dict_item['building'] = name | ||
return roomTimes | ||
def get_room_id_name_mapping(building): | ||
""" Returns a dictionary mapping id to name, thumbnail, and capacity. """ | ||
|
||
data = requests.get("{}/spaces?lid={}".format(BASE_URL, building)).content.decode("utf8") | ||
# find all of the javascript room definitions | ||
out = {} | ||
for item in re.findall(r"resources.push\(((?s).*?)\);", data, re.MULTILINE): | ||
# parse all of the room attributes | ||
items = {k: v for k, v in re.findall(r'(\w+?):\s*(.*?),', item)} | ||
|
||
# room name formatting | ||
title = items["title"][1:-1] | ||
title = title.encode().decode("unicode_escape" if six.PY3 else "string_escape") | ||
title = re.sub(r" \(Capacity [0-9]+\)", r"", title) | ||
|
||
# turn thumbnail into proper url | ||
thumbnail = items["thumbnail"][1:-1] | ||
if thumbnail: | ||
thumbnail = "https:" + thumbnail | ||
|
||
room_id = int(items["eid"]) | ||
out[room_id] = { | ||
"name": title, | ||
"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.""" | ||
|
||
if start.tzinfo is None: | ||
start = pytz.timezone("US/Eastern").localize(start) | ||
if end.tzinfo is None: | ||
end = pytz.timezone("US/Eastern").localize(end) | ||
|
||
mapping = self.get_room_id_name_mapping(building) | ||
room_endpoint = "{}/process_equip_p_availability.php".format(BASE_URL) | ||
data = { | ||
"lid": building, | ||
"gid": 0, | ||
"start": start.strftime("%Y-%m-%d"), | ||
"end": (end + datetime.timedelta(days=1)).strftime("%Y-%m-%d"), | ||
"bookings": [] | ||
} | ||
resp = requests.post(room_endpoint, data=json.dumps(data), headers={'Referer': "{}/spaces?lid={}".format(BASE_URL, building)}) | ||
rooms = {} | ||
for row in resp.json(): | ||
room_id = int(row["resourceId"][4:]) | ||
if room_id not in rooms: | ||
rooms[room_id] = [] | ||
room_start = self.parse_date(row["start"]) | ||
room_end = self.parse_date(row["end"]) | ||
if start <= room_start <= end: | ||
rooms[room_id].append({ | ||
"start": room_start.isoformat(), | ||
"end": room_end.isoformat(), | ||
"available": row["status"] == 0 | ||
}) | ||
out = [] | ||
for k, v in rooms.items(): | ||
item = { | ||
"room_id": k, | ||
"times": v | ||
} | ||
if k in mapping: | ||
item.update(mapping[k]) | ||
out.append(item) | ||
return out |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,8 @@ | ||
nose==1.3.4 | ||
requests==2.4.3 | ||
beautifulsoup4==4.4.1 | ||
beautifulsoup4==4.6.0 | ||
html5lib==0.999 | ||
mock==2.0.0 | ||
nameparser==0.4.0 | ||
nose==1.3.7 | ||
pytz==2017.3 | ||
requests==2.4.3 | ||
six==1.11.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,7 @@ | |
url='https://github.com/pennlabs/penn-sdk-python', | ||
author='Penn Labs', | ||
author_email='[email protected]', | ||
version='1.6.6', | ||
version='1.6.7', | ||
packages=['penn'], | ||
license='MIT', | ||
package_data={ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,30 +1,24 @@ | ||
import datetime | ||
import pytz | ||
|
||
from nose.tools import ok_ | ||
from penn import StudySpaces | ||
import datetime | ||
|
||
|
||
class TestStudySpaces(): | ||
|
||
def setUp(self): | ||
self.studyspaces = StudySpaces() | ||
|
||
def test_json(self): | ||
json_id = self.studyspaces.get_id_json() | ||
ok_(len(json_id) > 0) | ||
for i in json_id: | ||
ok_(i['id'] > 0) | ||
ok_(i['name'] != '') | ||
ok_(i['url'] != '') | ||
def test_buildings(self): | ||
buildings = self.studyspaces.get_buildings() | ||
ok_(len(buildings) > 0) | ||
|
||
def test_room_name_mapping(self): | ||
mapping = self.studyspaces.get_room_id_name_mapping(2683) | ||
ok_(len(mapping) > 0) | ||
|
||
def test_extraction(self): | ||
dict_id = self.studyspaces.get_id_dict() | ||
ok_(len(dict_id) > 0) | ||
d = datetime.datetime.now() + datetime.timedelta(days=1) | ||
next_date = d.strftime("%Y-%m-%d") | ||
s = self.studyspaces.extract_times(1799, next_date, "Van Pelt-Dietrich Library Center Group Study Rooms") | ||
for i in s: | ||
ok_("building" in i) | ||
ok_("start_time" in i) | ||
ok_("end_time" in i) | ||
ok_("date" in i) | ||
ok_("room_name" in i) | ||
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) |