Skip to content

Commit ccf6791

Browse files
committed
Merge branch 'master' into agh-gsr-booking
2 parents 887695f + 1279ee4 commit ccf6791

11 files changed

Lines changed: 406 additions & 194 deletions

File tree

Pipfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[[source]]
2+
url = "https://pypi.org/simple"
3+
verify_ssl = true
4+
name = "pypi"
5+
6+
[packages]
7+
8+
[dev-packages]
9+
10+
[requires]
11+
python_version = "3.11"

backend/gsr_booking/serializers.py

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,11 @@ class Meta:
8585
class GSRShareCodeSerializer(serializers.ModelSerializer):
8686
status = serializers.SerializerMethodField()
8787
expires_at = serializers.SerializerMethodField()
88-
booking_id = serializers.PrimaryKeyRelatedField(
89-
source="booking", queryset=GSRBooking.objects.all(), write_only=True
88+
booking_id = serializers.SlugRelatedField(
89+
slug_field="booking_id",
90+
source="booking",
91+
queryset=GSRBooking.objects.all(),
92+
write_only=True,
9093
)
9194

9295
class Meta:
@@ -124,13 +127,34 @@ def create(self, validated_data):
124127

125128
class SharedGSRBookingSerializer(serializers.ModelSerializer):
126129

127-
building = serializers.CharField(source="gsr.name")
128130
is_valid = serializers.SerializerMethodField()
131+
owner_name = serializers.SerializerMethodField()
132+
gsr = GSRSerializer(read_only=True)
129133

130134
class Meta:
131135
model = GSRBooking
132-
fields = ["room_name", "building", "start", "end", "is_valid"]
136+
fields = [
137+
"booking_id",
138+
"gsr",
139+
"room_id",
140+
"room_name",
141+
"start",
142+
"end",
143+
"is_valid",
144+
"owner_name",
145+
]
133146
read_only_fields = fields
134147

148+
def get_owner_name(self, obj):
149+
user = obj.reservation.creator if obj.reservation else obj.user
150+
if not user:
151+
return "Unknown"
152+
153+
full_name = f"{user.first_name} {user.last_name}".strip()
154+
if full_name:
155+
return full_name
156+
157+
return user.username
158+
135159
def get_is_valid(self, obj):
136160
return obj.end and obj.end > timezone.now()

backend/penndata/management/commands/get_calendar.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,17 @@ def handle(self, *args, **kwargs):
1717
# Clears out previous CalendarEvents
1818
CalendarEvent.objects.all().delete()
1919

20+
headers = {
21+
"User-Agent": (
22+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
23+
"AppleWebKit/537.36 (KHTML, like Gecko) "
24+
"Chrome/143.0.0.0 Safari/537.36"
25+
)
26+
}
27+
2028
# Scrapes UPenn Almanac
2129
try:
22-
resp = requests.get(UPENN_ALMANAC_WEBSITE)
30+
resp = requests.get(UPENN_ALMANAC_WEBSITE, headers=headers)
2331
except ConnectionError:
2432
return None
2533

backend/penndata/management/commands/get_fitness_snapshot.py

Lines changed: 47 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import zoneinfo
2+
13
import requests
2-
from bs4 import BeautifulSoup
3-
from dateutil import parser
4+
from django.conf import settings
45
from django.core.management.base import BaseCommand
5-
from django.utils import timezone
6+
from django.db.models import Q
7+
from django.utils.dateparse import parse_datetime
8+
from django.utils.timezone import make_aware
69

710
from penndata.models import FitnessRoom, FitnessSnapshot
811

@@ -12,77 +15,68 @@ def cap_string(s):
1215

1316

1417
def get_usages():
15-
16-
# count/capacities default to 0 since spreadsheet number appears blank if no one there
17-
locations = [
18-
"4th Floor Fitness",
19-
"3rd Floor Fitness",
20-
"2nd Floor Strength",
21-
"Basketball Courts",
22-
"MPR",
23-
"Climbing Wall",
24-
"1st Floor Fitness",
25-
"Pool-Shallow",
26-
"Pool-Deep",
27-
]
28-
usages = {location: {"count": 0, "capacity": 0} for location in locations}
29-
30-
date = timezone.localtime() # default if can't get date from spreadsheet
31-
3218
try:
3319
resp = requests.get(
34-
(
35-
"https://docs.google.com/spreadsheets/u/0/d/e/"
36-
"2PACX-1vSX91_MlAjJo5uVLznuy7BFnUgiBOI28oBCReLRKKo76L"
37-
"-k8EFgizAYXpIKPBX_c76wC3aztn3BogD4"
38-
"/pubhtml/sheet?headers=false&gid=0"
39-
)
20+
"https://goboardapi.azurewebsites.net/api/FacilityCount/GetCountsByAccount",
21+
params={"AccountAPIKey": settings.FITNESS_TOKEN},
4022
)
23+
data = resp.json()
4124
except ConnectionError:
4225
return None
43-
44-
html = resp.content.decode("utf8")
45-
soup = BeautifulSoup(html, "html5lib")
46-
if not (embedded_spreadsheet := soup.find("tbody")):
26+
except requests.exceptions.JSONDecodeError:
4727
return None
4828

49-
table_rows = embedded_spreadsheet.findChildren("tr")
50-
for i, row in enumerate(table_rows):
51-
cells = row.findChildren("td")
52-
if i == 0:
53-
date = timezone.make_aware(parser.parse(cells[1].getText()))
54-
elif (location := cap_string(cells[0].getText())) in usages:
55-
try:
56-
count = int(cells[1].getText())
57-
capacity = float(cells[2].getText().strip("%"))
58-
usages[location] = {"count": count, "capacity": capacity}
59-
except ValueError:
60-
pass
61-
else:
62-
print(f"Unknown location: {location}")
63-
return usages, date
29+
def location_aware_datetime(time_str):
30+
date = parse_datetime(time_str)
31+
timezone = zoneinfo.ZoneInfo("America/New_York")
32+
return make_aware(date, timezone=timezone)
33+
34+
usages = {
35+
location["LocationName"]: {
36+
"count": location["LastCount"],
37+
"capacity": location["TotalCapacity"],
38+
"last_updated": location_aware_datetime(location["LastUpdatedDateAndTime"]),
39+
}
40+
for location in data
41+
}
42+
return usages
6443

6544

6645
class Command(BaseCommand):
67-
help = "Captures a new Fitness Snapshot for every Laundry room."
46+
help = "Captures a new Fitness Snapshot for every Fitness room."
6847

6948
def handle(self, *args, **kwargs):
70-
usage_by_location, date = get_usages()
49+
# Don't update locations for which we already have a room with a matching last_updated date.
50+
# Fixed the O(n^2) issue by loading everything into memory. Should be fine since there's
51+
# not many rooms, and 1 snapshot returned per room
52+
all_rooms = FitnessRoom.objects.all()
53+
all_room_names = set(room.name for room in all_rooms)
54+
query = Q()
55+
for room_name, room_usage in get_usages().items():
56+
query |= Q(room__name=room_name, date=room_usage["last_updated"])
57+
existing_snapshots = FitnessSnapshot.objects.filter(query)
58+
existing_room_date_pairs = set(
59+
(snapshot.room.name, snapshot.date) for snapshot in existing_snapshots
60+
)
7161

72-
# prevent double creating FitnessSnapshots
73-
if FitnessSnapshot.objects.filter(date=date).exists():
74-
self.stdout.write("FitnessSnapshots already exist for this date!")
75-
return
62+
def exists(record):
63+
(name, usage) = record
64+
if name not in all_room_names:
65+
return False
66+
if (name, usage["last_updated"]) in existing_room_date_pairs:
67+
return False
68+
return True
7669

70+
usage_by_location = filter(exists, get_usages().items())
7771
FitnessSnapshot.objects.bulk_create(
7872
[
7973
FitnessSnapshot(
8074
room=FitnessRoom.objects.get_or_create(name=room_name)[0],
81-
date=date,
75+
date=room_usage["last_updated"],
8276
count=room_usage["count"],
8377
capacity=room_usage["capacity"],
8478
)
85-
for room_name, room_usage in usage_by_location.items()
79+
for (room_name, room_usage) in usage_by_location
8680
]
8781
)
8882

backend/penndata/management/commands/load_fitness_rooms.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,23 @@
66
class Command(BaseCommand):
77
def handle(self, *args, **kwargs):
88
fitness_rooms = [
9-
"4th Floor Fitness",
10-
"3rd Floor Fitness",
11-
"2nd Floor Strength",
12-
"Basketball Courts",
13-
"MPR",
149
"Climbing Wall",
10+
"Rec Lounge",
1511
"1st Floor Fitness",
16-
"Pool-Shallow",
17-
"Pool-Deep",
12+
"Court 1",
13+
"Court 2",
14+
"Court 3",
15+
"Multipurpose Room",
16+
"2nd Floor Weight Room",
17+
"3rd Floor Fitness Room",
18+
"4th Floor Fitness Room",
19+
"Studio 409",
20+
"Sheerr Pool",
1821
]
1922
for room in fitness_rooms:
2023
obj, _ = FitnessRoom.objects.get_or_create(name=room)
2124
if obj.image_url == "":
22-
s3_image_name = (
23-
room.replace(" ", "_") + (".png" if "2nd" in room else ".jpg")
24-
if "Pool" not in room
25-
else "Pool.jpeg"
26-
)
25+
s3_image_name = room.replace(" ", "_") + ".jpg"
2726
obj.image_url = (
2827
f"https://s3.us-east-2.amazonaws.com/penn.mobile/pottruck/{s3_image_name}"
2928
)

backend/pennmobile/settings/base.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,9 @@
186186
PENNGROUPS_USERNAME = os.environ.get("PENNGROUPS_USERNAME", None)
187187
PENNGROUPS_PASSWORD = os.environ.get("PENNGROUPS_PASSWORD", None)
188188

189+
# Fitness Token
190+
FITNESS_TOKEN = os.environ.get("FITNESS_TOKEN", None)
191+
189192
# Upload file storage
190193
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
191194
AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID", None)

backend/pennmobile/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def universal_identifier_link(request):
4444
"VU59R57FGM.org.pennlabs.PennMobile.dev",
4545
],
4646
"components": [
47-
{"/": "ios/gsr/share/*", "?": {"data": "*"}, "comment": "GSR Sharing."}
47+
{"/": "/gsr/share/*", "?": {"data": "*"}, "comment": "GSR Sharing."}
4848
],
4949
}
5050
]

backend/tests/gsr_booking/test_share_codes.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def setUp(self):
6363
def test_create_share_code_success(self):
6464
# Creates a gsr share code successfully
6565
self.client.force_authenticate(user=self.owner)
66-
response = self.client.post("/api/gsr/share/", {"booking_id": self.booking.id})
66+
response = self.client.post("/api/gsr/share/", {"booking_id": self.booking.booking_id})
6767
self.assertEqual(response.status_code, 201)
6868
payload = json.loads(response.content)
6969

@@ -79,13 +79,13 @@ def test_create_share_code_duplicate(self):
7979
self.client.force_authenticate(user=self.owner)
8080

8181
# First creation
82-
response1 = self.client.post("/api/gsr/share/", {"booking_id": self.booking.id})
82+
response1 = self.client.post("/api/gsr/share/", {"booking_id": self.booking.booking_id})
8383
self.assertEqual(response1.status_code, 201)
8484
payload1 = json.loads(response1.content)
8585
first_code = payload1["code"]
8686

8787
# Second creation (should return existing code)
88-
response2 = self.client.post("/api/gsr/share/", {"booking_id": self.booking.id})
88+
response2 = self.client.post("/api/gsr/share/", {"booking_id": self.booking.booking_id})
8989
self.assertEqual(response2.status_code, 201) # Changed from 200 to 201
9090
payload2 = json.loads(response2.content)
9191

@@ -96,7 +96,7 @@ def test_create_share_code_duplicate(self):
9696
self.assertEqual(GSRShareCode.objects.filter(booking=self.booking).count(), 1)
9797

9898
def test_create_share_code_without_auth(self):
99-
response = self.client.post("/api/gsr/share/", {"booking_id": self.booking.id})
99+
response = self.client.post("/api/gsr/share/", {"booking_id": self.booking.booking_id})
100100
self.assertEqual(response.status_code, 403)
101101
self.assertEqual(GSRShareCode.objects.count(), 0)
102102

@@ -124,13 +124,22 @@ def test_view_shared_booking_public_access(self):
124124
payload = json.loads(response.content)
125125

126126
# Should only contain booking info and not owner info
127+
print("Payload: ", payload)
128+
self.assertIn("booking_id", payload)
129+
self.assertIn("gsr", payload)
130+
self.assertIn("lid", payload["gsr"])
131+
self.assertIn("gid", payload["gsr"])
132+
self.assertIn("name", payload["gsr"])
133+
self.assertIn("kind", payload["gsr"])
134+
self.assertIn("image_url", payload["gsr"])
127135
self.assertIn("room_name", payload)
128-
self.assertIn("building", payload)
136+
self.assertIn("room_id", payload)
129137
self.assertIn("start", payload)
130138
self.assertIn("end", payload)
131139
self.assertIn("is_valid", payload)
140+
self.assertIn("owner_name", payload)
132141
self.assertEqual(payload["room_name"], self.booking.room_name)
133-
self.assertEqual(payload["building"], self.booking.gsr.name)
142+
self.assertEqual(payload["gsr"]["name"], self.booking.gsr.name)
134143
self.assertEqual(payload["is_valid"], True)
135144

136145
def test_view_shared_booking_invalid_code(self):
@@ -224,7 +233,7 @@ def test_create_share_code_for_expired_booking_code_invalid(self):
224233
self.booking.save(update_fields=["end"])
225234

226235
self.client.force_authenticate(user=self.owner)
227-
response = self.client.post("/api/gsr/share/", {"booking_id": self.booking.id})
236+
response = self.client.post("/api/gsr/share/", {"booking_id": self.booking.booking_id})
228237
self.assertEqual(response.status_code, 201)
229238
payload = json.loads(response.content)
230239
self.assertEqual(payload["status"], "expired")
@@ -254,7 +263,7 @@ def test_create_share_code_replaces_expired(self):
254263

255264
# Create again
256265
self.client.force_authenticate(user=self.owner)
257-
response = self.client.post("/api/gsr/share/", {"booking_id": self.booking.id})
266+
response = self.client.post("/api/gsr/share/", {"booking_id": self.booking.booking_id})
258267
self.assertEqual(response.status_code, 201) # Changed from 200 to 201
259268
payload = json.loads(response.content)
260269

@@ -314,17 +323,24 @@ def test_shared_booking_serializer(self):
314323
data = serializer.data
315324

316325
# Should have booking details
326+
self.assertIn("booking_id", data)
327+
self.assertIn("gsr", data)
328+
self.assertIn("lid", data["gsr"])
329+
self.assertIn("gid", data["gsr"])
330+
self.assertIn("name", data["gsr"])
331+
self.assertIn("kind", data["gsr"])
332+
self.assertIn("image_url", data["gsr"])
317333
self.assertIn("room_name", data)
318-
self.assertIn("building", data)
334+
self.assertIn("room_id", data)
319335
self.assertIn("start", data)
320336
self.assertIn("end", data)
321337
self.assertIn("is_valid", data)
338+
self.assertIn("owner_name", data)
322339

323340
# Should not have owner info
324341
self.assertNotIn("user", data)
325342
self.assertNotIn("owner", data)
326343
self.assertNotIn("reservation", data)
327-
self.assertNotIn("booking_id", data)
328344

329345
def test_is_valid_method(self):
330346
share_code = GSRShareCode.objects.create(

0 commit comments

Comments
 (0)