Skip to content

Commit 7c3d2fc

Browse files
committed
feat(bus): fetch fcps afternoon bus delays
Closes tjcsl#1413
1 parent 0feeb1b commit 7c3d2fc

File tree

8 files changed

+208
-7
lines changed

8 files changed

+208
-7
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Generated by Django 3.2.25 on 2025-05-20 23:50
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('bus', '0008_alter_busannouncement_message'),
10+
]
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name='BusDelay',
15+
fields=[
16+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17+
('bus_number', models.CharField(max_length=10)),
18+
('reason', models.TextField()),
19+
('timestamp', models.DateTimeField(auto_now_add=True)),
20+
],
21+
),
22+
]
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Generated by Django 3.2.25 on 2025-05-21 03:01
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('bus', '0009_busdelay'),
10+
]
11+
12+
operations = [
13+
migrations.DeleteModel(
14+
name='BusDelay',
15+
),
16+
migrations.AddField(
17+
model_name='route',
18+
name='reason',
19+
field=models.CharField(blank=True, max_length=50),
20+
),
21+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 3.2.25 on 2025-05-22 14:00
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('bus', '0010_auto_20250520_2301'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='route',
15+
name='estimated_time_delay',
16+
field=models.CharField(blank=True, max_length=5),
17+
),
18+
]

intranet/apps/bus/models.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@ class Route(models.Model):
1010
space = models.CharField(max_length=4, blank=True)
1111
bus_number = models.CharField(max_length=5, blank=True)
1212
status = models.CharField("arrival status", choices=ARRIVAL_STATUSES, max_length=1, default="o")
13+
reason = models.CharField(max_length=50, blank=True)
14+
estimated_time_delay = models.CharField(max_length=5, blank=True)
1315

1416
def reset_status(self):
1517
"""Reset status to (on time)"""
1618
self.status = "o"
1719
self.space = ""
18-
self.save(update_fields=["status", "space"])
20+
self.reason = ""
21+
self.estimated_time_delay = ""
22+
self.save(update_fields=["status", "space", "reason", "estimated_time_delay"])
1923

2024
def __str__(self):
2125
return self.route_name

intranet/apps/bus/tasks.py

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1+
from datetime import timedelta
2+
3+
import requests
4+
from asgiref.sync import async_to_sync
5+
from bs4 import BeautifulSoup
16
from celery import shared_task
27
from celery.utils.log import get_task_logger
8+
from channels.layers import get_channel_layer
9+
from django.conf import settings
10+
from django.utils import timezone
311

12+
from ..schedule.models import Day
413
from .models import Route
514

615
logger = get_task_logger(__name__)
@@ -9,6 +18,107 @@
918
@shared_task
1019
def reset_routes() -> None:
1120
logger.info("Resetting bus routes")
12-
1321
for route in Route.objects.all():
1422
route.reset_status()
23+
24+
25+
@shared_task
26+
def schedule_all_bus_delay_fetches():
27+
# This task runs once daily (12:01am) and schedules all bus delay fetches for the day
28+
day = Day.objects.today()
29+
if day is None:
30+
logger.error("No Day found for today")
31+
return
32+
33+
tz = timezone.get_current_timezone()
34+
start_datetime = timezone.make_aware(day.start_time.date_obj(day.date), tz)
35+
end_datetime = timezone.make_aware(day.end_time.date_obj(day.date), tz)
36+
37+
# 1 minute intervals (idle windows)
38+
# Idle window 1: 2.5h before start to 1h after start
39+
first_window_start = start_datetime - timedelta(hours=2.5)
40+
first_window_end = start_datetime + timedelta(hours=1)
41+
# Idle window 2: 1h before end to 5m before end
42+
second_window_start = end_datetime - timedelta(hours=1)
43+
second_window_end = end_datetime - timedelta(minutes=5)
44+
# Idle window 3: 20m after end to 2h after end
45+
third_window_start = end_datetime + timedelta(minutes=20)
46+
third_window_end = end_datetime + timedelta(hours=2)
47+
48+
# Helper to schedule at 1 minute intervals
49+
def schedule_minutely(start, end):
50+
t = start
51+
while t <= end:
52+
fetch_fcps_bus_delays.apply_async(eta=t)
53+
t += timedelta(minutes=1)
54+
55+
schedule_minutely(first_window_start, first_window_end)
56+
schedule_minutely(second_window_start, second_window_end)
57+
schedule_minutely(third_window_start, third_window_end)
58+
59+
# 15 second intervals (active window)
60+
active_window_start = end_datetime - timedelta(minutes=5)
61+
active_window_end = end_datetime + timedelta(minutes=20)
62+
t = active_window_start
63+
while t <= active_window_end:
64+
fetch_fcps_bus_delays.apply_async(eta=t)
65+
t += timedelta(seconds=15)
66+
67+
logger.info("Scheduled all bus delay fetches for today.")
68+
69+
70+
@shared_task
71+
def fetch_fcps_bus_delays():
72+
url = settings.BUS_DELAY_URL
73+
74+
try:
75+
response = requests.get(url, timeout=10)
76+
response.raise_for_status()
77+
except Exception as e:
78+
logger.error("Error fetching URL: %s", e)
79+
return
80+
81+
try:
82+
soup = BeautifulSoup(response.text, "html.parser")
83+
rows = soup.select("table tr")
84+
except Exception as e:
85+
logger.error("Error parsing HTML: %s", e)
86+
return
87+
88+
if not rows or len(rows) < 2:
89+
logger.warning("Not a complete row with all bus information")
90+
return
91+
92+
# Sort out the JEFFERSON HIGH bus delays
93+
try:
94+
for row in rows[1:]:
95+
cells = row.find_all("td")
96+
if len(cells) >= 4 and cells[0].text.strip() == "JEFFERSON HIGH":
97+
route_name = cells[1].text.strip().split()[0][:100]
98+
reason = cells[3].text.strip()[:150]
99+
estimated_time_delay = cells[2].text.strip()[:10]
100+
try:
101+
obj = Route.objects.get(route_name=route_name)
102+
# Only update if current status isn't "on time"
103+
if obj.status != "a":
104+
obj.status = "d"
105+
obj.reason = reason
106+
obj.estimated_time_delay = estimated_time_delay
107+
obj.save(update_fields=["status", "reason", "estimated_time_delay"])
108+
logger.info("Updated route %s with delay: %s and ETA: %s", route_name, reason, estimated_time_delay)
109+
channel_layer = get_channel_layer()
110+
all_routes = list(Route.objects.values())
111+
async_to_sync(channel_layer.group_send)(
112+
"bus",
113+
{
114+
"type": "bus.update",
115+
"message": {
116+
"allRoutes": all_routes,
117+
},
118+
},
119+
)
120+
except Route.DoesNotExist:
121+
logger.error("Route with route_name %s does not exist", route_name)
122+
except Exception as e:
123+
logger.error("Error processing bus delays: %s", e)
124+
return

intranet/apps/bus/views.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,21 @@ def afternoon(request, on_home=False):
4545
raise Http404("Bus app not enabled.")
4646
is_bus_admin = request.user.has_admin_permission("bus")
4747

48-
now = timezone.localtime()
48+
current_time = timezone.localtime()
4949
day = Day.objects.today()
5050
if day is not None and day.end_time is not None:
51-
end_of_day = day.end_time.date_obj(now.date())
51+
end_of_day = day.end_time.date_obj(current_time.date())
5252
else:
53-
end_of_day = datetime.datetime(now.year, now.month, now.day, settings.SCHOOL_END_HOUR, settings.SCHOOL_END_MINUTE)
54-
53+
end_of_day = datetime.datetime(current_time.year, current_time.month, current_time.day, settings.SCHOOL_END_HOUR, settings.SCHOOL_END_MINUTE)
54+
bus_delays_queryset = Route.objects.filter(status="d")
55+
bus_delays = {delay.route_name: {"reason": delay.reason, "estimated_time_delay": delay.estimated_time_delay} for delay in bus_delays_queryset}
5556
ctx = {
5657
"admin": is_bus_admin,
5758
"enable_bus_driver": True,
5859
"changeover_time": settings.BUS_PAGE_CHANGEOVER_HOUR,
5960
"school_end_hour": end_of_day.hour,
6061
"school_end_time": end_of_day.minute,
62+
"bus_delays": bus_delays,
6163
"on_home": on_home,
6264
}
6365
return render(request, "bus/home.html", context=ctx)

intranet/settings/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@
6767
SENIOR_DESTS_BANNER_LINK = "https://tinyurl.com/tjseniors2025"
6868

6969
""" -------- END UPDATE ANNUALLY -------- """
70+
71+
#Bus Delays URL
72+
BUS_DELAY_URL = "https://busdelay.fcps.edu/"
73+
7074
# fmt: on
7175

7276
# Default fallback time for start and end of school if no schedule is available
@@ -955,6 +959,11 @@ def get_log(name): # pylint: disable=redefined-outer-name; 'name' is used as th
955959
"schedule": celery.schedules.crontab(day_of_month=3, hour=1),
956960
"args": (),
957961
},
962+
"schedule-all-bus-delay-fetches": {
963+
"task": "intranet.apps.bus.tasks.schedule_all_bus_delay_fetches",
964+
"schedule": celery.schedules.crontab(hour=0, minute=1),
965+
"args": (),
966+
},
958967
"remove-old-lostfound-entries": {
959968
"task": "intranet.apps.lostfound.tasks.remove_old_lostfound",
960969
"schedule": celery.schedules.crontab(day_of_month=1, hour=1),
@@ -1016,4 +1025,4 @@ def get_log(name): # pylint: disable=redefined-outer-name; 'name' is used as th
10161025
sentry_logging = LoggingIntegration(
10171026
level=logging.INFO, event_level=logging.ERROR # Capture info and above as breadcrumbs # Send errors as events
10181027
)
1019-
sentry_sdk.init(SENTRY_PUBLIC_DSN, integrations=[DjangoIntegration(), sentry_logging, CeleryIntegration()], send_default_pii=True)
1028+
sentry_sdk.init(SENTRY_PUBLIC_DSN, integrations=[DjangoIntegration(), sentry_logging, CeleryIntegration()], send_default_pii=True)

intranet/templates/bus/home.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,21 @@ <h2 class="bordered-element bus-announcement-header">
113113
JT-100, JT100, jt-100, jt100, or 100.
114114
</p>
115115
{% endif %}
116+
<div class="delay-announcements">
117+
<h3>Current Bus Delays</h3>
118+
{% if bus_delays %}
119+
{% for route, delay in bus_delays.items %}
120+
<div class="delay-card">
121+
<i class="fas fa-exclamation-triangle"></i>
122+
<span>
123+
<strong>Bus {{ route }}</strong> delayed due to: {{ delay.reason|lower }} <br/> ETA: {{ delay.estimated_time_delay }} minutes.
124+
</span>
125+
</div>
126+
{% endfor %}
127+
{% else %}
128+
<p class="no-delays">No current delays reported.</p>
129+
{% endif %}
130+
</div>
116131
</div>
117132
</div>
118133
<script type="text/template" id="map-view">

0 commit comments

Comments
 (0)