Skip to content

Commit

Permalink
Merge pull request #91 from pennlabs/studyspaces
Browse files Browse the repository at this point in the history
Rebuild study spaces
  • Loading branch information
ezwang authored Jan 28, 2018
2 parents 96011fe + 8160aec commit 0769f24
Show file tree
Hide file tree
Showing 12 changed files with 146 additions and 128 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,5 @@ MANIFEST
docs/_build
.pypirc
.idea/
*.swp
*.swo
6 changes: 5 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ python:
- "2.7"
- "3.4"
- "3.5"
script: nosetests
install:
- pip install flake8 -r requirements.txt
script:
- flake8 --max-line-length=150 --exclude=venv,docs,build --ignore=F401,F403,F405 .
- nosetests
deploy:
provider: pypi
user: 'pennappslabs'
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.6'
__version__ = '1.6.7'

from .registrar import Registrar
from .directory import Directory
Expand Down
6 changes: 3 additions & 3 deletions penn/calendar3year.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ def pull_3year(self):
"""
events = []
r = requests.get(BASE_URL).text
l = r.split("\r\n")
lines = r.split("\r\n")
d = {}
for line in l:
for line in lines:
if line == "BEGIN:VEVENT":
d = {}
elif line[:7] == "DTSTART":
Expand All @@ -32,7 +32,7 @@ def pull_3year(self):
d['start'] = start_date.strftime('%Y-%m-%d')
elif line[:5] == "DTEND":
raw_date = line.split(":")[1]
end_date = datetime.datetime.strptime(raw_date,'%Y%m%d').date()
end_date = datetime.datetime.strptime(raw_date, '%Y%m%d').date()
d['end'] = end_date.strftime('%Y-%m-%d')
elif line[:7] == "SUMMARY":
name = line.split(":")[1]
Expand Down
2 changes: 2 additions & 0 deletions penn/dining.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ def get_meals(v2_response, building_id):
meals.append({"tblStation": stations, "txtDayPartDescription": meal["label"]})
return meals


class DiningV2(WrapperBase):
"""The client for the Registrar. Used to make requests to the API.
Expand Down Expand Up @@ -141,6 +142,7 @@ def item(self, item_id):
response = self._request(V2_ENDPOINTS['ITEMS'] + item_id)
return response


class Dining(WrapperBase):
"""The client for the Registrar. Used to make requests to the API.
Expand Down
1 change: 0 additions & 1 deletion penn/directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ def standardize(res):
res['list_affiliation'] = res['list_affiliation'].replace('Faculty - ', '')
return res


def search(self, params, standardize=False):
"""Get a list of person objects for the given search params.
Expand Down
182 changes: 99 additions & 83 deletions penn/studyspaces.py
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
8 changes: 5 additions & 3 deletions requirements.txt
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
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.6',
version='1.6.7',
packages=['penn'],
license='MIT',
package_data={
Expand Down
26 changes: 13 additions & 13 deletions tests/calendar_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,34 @@ def setUp(self):
self.calendar = Calendar()

def test_pull(self):
l = self.calendar.pull_3year()
ok_(len(l) > 0)
for event in l:
cal = self.calendar.pull_3year()
ok_(len(cal) > 0)
for event in cal:
ok_(len(event) == 3)

def test_date(self):
l = self.calendar.pull_3year()
ok_(len(l) > 0)
for event in l:
cal = self.calendar.pull_3year()
ok_(len(cal) > 0)
for event in cal:
ok_(isinstance(event['name'], str))
if event['name'] == "Independence Day Observed (no classes)":
independence = event['start']
d = datetime.datetime.strptime(independence, '%Y-%m-%d').date()
ok_(d.month == 7)

def test_name(self):
l = self.calendar.pull_3year()
ok_(len(l) > 0)
for event in l:
cal = self.calendar.pull_3year()
ok_(len(cal) > 0)
for event in cal:
ok_(isinstance(event['name'], str))
start = datetime.datetime.strptime(event['start'], '%Y-%m-%d').date()
end = datetime.datetime.strptime(event['end'], '%Y-%m-%d').date()
ok_((end - start).total_seconds() >= 0)

def test_chrono(self):
l = self.calendar.pull_3year()
ok_(len(l) > 0)
for i, event in enumerate(l[:-1]):
cal = self.calendar.pull_3year()
ok_(len(cal) > 0)
for i, event in enumerate(cal[:-1]):
start = datetime.datetime.strptime(event['start'], '%Y-%m-%d').date()
nextstart = datetime.datetime.strptime(l[i]['start'], '%Y-%m-%d').date()
nextstart = datetime.datetime.strptime(cal[i]['start'], '%Y-%m-%d').date()
ok_((nextstart - start).total_seconds() >= 0)
3 changes: 1 addition & 2 deletions tests/dining_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ def test_dining(self):
self.assertTrue(len(venues) > 0)
id = str(venues["id"])
data = self.din.menu_daily(id)
self.assertTrue(
len(data["result_data"]["Document"]["tblMenu"]["tblDayPart"][0]) >= 2)
self.assertTrue(len(data["result_data"]["Document"]["tblMenu"]["tblDayPart"][0]) >= 2)

def test_dining_normalization(self):
data = self.din.menu_daily("593")
Expand Down
34 changes: 14 additions & 20 deletions tests/studyspaces_test.py
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)

0 comments on commit 0769f24

Please sign in to comment.