Skip to content

Commit bea49b7

Browse files
authored
Grace/menu fix (#409)
* load_next_menu loads 7 days in advance * updated threading to only be with API loading * makes api_wrapper thread only API calls * formatting fixes * PLEASE LINTER * now it passes the test case * updated imports
1 parent 4a26ed2 commit bea49b7

4 files changed

Lines changed: 73 additions & 14 deletions

File tree

backend/dining/api_wrapper.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import datetime
22
import json
3+
from concurrent.futures import ThreadPoolExecutor, as_completed
34

45
import requests
56
from django.conf import settings
67
from django.utils import timezone
78
from django.utils.timezone import make_aware
8-
from requests.exceptions import ConnectTimeout, ReadTimeout
9+
from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout
910

1011
from dining.models import DiningItem, DiningMenu, DiningStation, Venue
1112
from utils.errors import APIError
@@ -59,7 +60,7 @@ def get_venues(self):
5960
venues_route = OPEN_DATA_ENDPOINTS["VENUES"]
6061
response = self.request("GET", venues_route)
6162
if response.status_code != 200:
62-
raise APIError("Dining: Error connecting to API")
63+
raise APIError("Dining: error connecting to API " + response.text)
6364
venues = response.json()["result_data"]["campuses"]["203"]["cafes"]
6465
for key, value in venues.items():
6566
# Cleaning up json response
@@ -107,21 +108,47 @@ def get_venues(self):
107108
results.append(value)
108109
return results
109110

111+
def fetch_menu(self, venue_id, date):
112+
"""
113+
Calls API to fetch menu for a given venue and date
114+
"""
115+
worker = DiningAPIWrapper() # avoid shared mutable token state across threads
116+
menu_base = OPEN_DATA_ENDPOINTS["MENUS"]
117+
response = worker.request("GET", f"{menu_base}?cafe={venue_id}&date={date}")
118+
if response.status_code != 200:
119+
raise APIError("Dining: error connecting to API " + response.text)
120+
return (
121+
venue_id,
122+
response.json(),
123+
) # also storing venue_id to later access in fetched_menus list
124+
110125
def load_menu(self, date=timezone.now().date()):
111126
"""
112-
Loads the weeks menu starting from today
127+
Loads today's menu
113128
NOTE: This method should only be used in load_next_menu.py, which is
114129
run based on a cron job every day
115130
"""
116-
117131
# Venues without a menu should not be parsed
118132
skipped_venues = [747, 1163, 1731, 1732, 1733, 1464004, 1464009]
119133

120134
# TODO: Handle API responses during empty menus (holidays)
121-
menu_base = OPEN_DATA_ENDPOINTS["MENUS"]
122135
venues = [v for v in Venue.objects.all() if v.venue_id not in skipped_venues]
123-
for venue in venues:
124-
response = self.request("GET", f"{menu_base}?cafe={venue.venue_id}&date={date}").json()
136+
venue_map = {venue.venue_id: venue for venue in venues}
137+
138+
# Fetch all menus in parallel to speed up loading time.
139+
fetched_menus = []
140+
with ThreadPoolExecutor(max_workers=8) as executor: # 8 can be tuned
141+
futures = [executor.submit(self.fetch_menu, venue.venue_id, date) for venue in venues]
142+
for future in as_completed(futures):
143+
try:
144+
venue_id, response_json = future.result()
145+
fetched_menus.append((venue_id, response_json))
146+
except Exception as e:
147+
print(f"Error fetching menu: {e}")
148+
149+
# Process the fetched menus and load them into the database
150+
for venue_id, response in fetched_menus:
151+
venue = venue_map[venue_id]
125152
# Load new items into database
126153
# TODO: There is something called a "goitem" for venues like English House.
127154
# We are currently not loading them in
@@ -144,6 +171,7 @@ def load_menu(self, date=timezone.now().date()):
144171
end_time=daypart["endtime"],
145172
service=daypart["label"],
146173
)
174+
147175
# Append stations to dining menu
148176
self.load_stations(daypart["stations"], dining_menu)
149177

backend/dining/management/commands/load_next_menu.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,25 @@
88

99
class Command(BaseCommand):
1010
"""
11-
Loads Menu for 1 week in advance.
11+
Loads menu for the next 7 days, starting from today. Takes about 3 minutes to run.
1212
Invariant: For every date, the database should contain the menus for
1313
the next 7 days, including the original date.
1414
"""
1515

16-
def handle(self, *args, **kwargs):
16+
def load_one_menu(self, delta, *args, **kwargs):
17+
"""
18+
Loads menu for a single day
19+
"""
1720
d = DiningAPIWrapper()
18-
d.load_menu(timezone.now().date() + datetime.timedelta(days=6))
19-
self.stdout.write("Loaded new Dining Menu!")
21+
d.load_menu(timezone.now().date() + datetime.timedelta(days=delta))
22+
self.stdout.write(
23+
"Loaded new Dining Menu for "
24+
+ str(timezone.now().date() + datetime.timedelta(days=delta))
25+
)
26+
27+
def handle(self, *args, **kwargs):
28+
"""
29+
Load menu for the next 7 days
30+
"""
31+
for i in range(7):
32+
self.load_one_menu(i)

backend/dining/views.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
from analytics.entries import FuncEntry, ViewEntry
44
from django.core.cache import cache
5-
from django.db.models import Count
5+
from django.db.models import Count, F, Window
6+
from django.db.models.functions.window import RowNumber
67
from django.shortcuts import get_object_or_404
78
from django.utils import timezone
89
from django.utils.timezone import make_aware
@@ -54,11 +55,27 @@ def get_queryset(self):
5455
# if date_param is out of bounds
5556
if date_param := self.kwargs.get("date"):
5657
date = make_aware(datetime.datetime.strptime(date_param, "%Y-%m-%d"))
57-
return DiningMenu.objects.filter(date=date)
58+
base = DiningMenu.objects.filter(date=date)
5859
else:
5960
start_date = timezone.now().date()
6061
end_date = start_date + datetime.timedelta(days=6)
61-
return DiningMenu.objects.filter(date__gte=start_date, date__lte=end_date)
62+
base = DiningMenu.objects.filter(date__gte=start_date, date__lte=end_date)
63+
# Only returns most recently loaded menus
64+
latest = base.annotate(
65+
rn=Window(
66+
expression=RowNumber(),
67+
partition_by=[
68+
F("venue_id"),
69+
F("date"),
70+
F("service"),
71+
F("start_time"),
72+
F("end_time"),
73+
],
74+
order_by=[F("id").desc()],
75+
)
76+
).filter(rn=1)
77+
78+
return latest
6279

6380

6481
class Preferences(APIView):

backend/tests/dining/test_views.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from django.core.management import call_command
77
from django.test import TestCase
88
from django.urls import reverse
9+
from requests.exceptions import ConnectionError
910
from rest_framework.test import APIClient
1011

1112
from dining.api_wrapper import APIError, DiningAPIWrapper

0 commit comments

Comments
 (0)