Skip to content

Commit ceb6365

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

File tree

8 files changed

+129
-7
lines changed

8 files changed

+129
-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+
]

intranet/apps/bus/models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ 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)
1314

1415
def reset_status(self):
1516
"""Reset status to (on time)"""
1617
self.status = "o"
1718
self.space = ""
18-
self.save(update_fields=["status", "space"])
19+
self.reason = ""
20+
self.save(update_fields=["status", "space", "reason"])
1921

2022
def __str__(self):
2123
return self.route_name

intranet/apps/bus/tasks.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import requests
2+
from bs4 import BeautifulSoup
13
from celery import shared_task
24
from celery.utils.log import get_task_logger
3-
5+
from django.utils import timezone
6+
from asgiref.sync import async_to_sync
7+
from channels.layers import get_channel_layer
48
from .models import Route
59

610
logger = get_task_logger(__name__)
@@ -12,3 +16,54 @@ def reset_routes() -> None:
1216

1317
for route in Route.objects.all():
1418
route.reset_status()
19+
20+
21+
@shared_task
22+
def fetch_fcps_bus_delays():
23+
now = timezone.localtime(timezone.now())
24+
# Check if the current time is within 6-9 AM or 3-6 PM from Mon-Fri
25+
if now.weekday() in (5, 6):
26+
return
27+
if not (now.hour in range(6, 9) and now.hour in range(15, 18)):
28+
return
29+
url = "https://busdelay.fcps.edu"
30+
try:
31+
response = requests.get(url, timeout=10)
32+
response.raise_for_status()
33+
except Exception as e:
34+
logger.error("Error fetching URL: %s", e)
35+
return
36+
37+
soup = BeautifulSoup(response.text, "html.parser")
38+
rows = soup.select("table tr")
39+
# In case a row is formatted incorrectly, check the amount of cells to make sure it contains all information
40+
if not rows or len(rows) < 2:
41+
logger.warning("Not a complete row with all bus information")
42+
return
43+
# Sort out the JEFFERSON HIGH bus delays
44+
for row in rows[1:]:
45+
cells = row.find_all("td")
46+
if len(cells) >= 4 and cells[0].text.strip() == "JEFFERSON HIGH":
47+
route_name = cells[1].text.strip().split()[0]
48+
reason = cells[3].text.strip()
49+
try:
50+
obj = Route.objects.get(route_name=route_name)
51+
# Only can update the status if it is on default status (on time)
52+
if obj.status != "a":
53+
obj.status = "d"
54+
obj.reason = reason
55+
obj.save(update_fields=["status", "reason"])
56+
logger.info("Updated route %s with delay: %s", route_name, reason)
57+
channel_layer = get_channel_layer()
58+
all_routes = list(Route.objects.values())
59+
async_to_sync(channel_layer.group_send)(
60+
"bus",
61+
{
62+
"type": "bus.update",
63+
"message": {
64+
"allRoutes": all_routes,
65+
},
66+
},
67+
)
68+
except Route.DoesNotExist:
69+
logger.error("Route with route_name %s does not exist", route_name)

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} 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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,11 @@ def get_log(name): # pylint: disable=redefined-outer-name; 'name' is used as th
955955
"schedule": celery.schedules.crontab(day_of_month=3, hour=1),
956956
"args": (),
957957
},
958+
"fetch-fcps-bus-delays": {
959+
"task": "intranet.apps.bus.tasks.fetch_fcps_bus_delays",
960+
"schedule": 10.0,
961+
"args": (),
962+
},
958963
"remove-old-lostfound-entries": {
959964
"task": "intranet.apps.lostfound.tasks.remove_old_lostfound",
960965
"schedule": celery.schedules.crontab(day_of_month=1, hour=1),

intranet/static/js/bus-afternoon.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -593,4 +593,4 @@ $(function() {
593593

594594
window.appView = new bus.AppView();
595595
let socket = getSocket(base_url, location, document, window, 'afternoon');
596-
});
596+
});

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 }}
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)