Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions intranet/apps/bus/migrations/0009_busdelay.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 3.2.25 on 2025-05-20 23:50

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('bus', '0008_alter_busannouncement_message'),
]

operations = [
migrations.CreateModel(
name='BusDelay',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('bus_number', models.CharField(max_length=10)),
('reason', models.TextField()),
('timestamp', models.DateTimeField(auto_now_add=True)),
],
),
]
21 changes: 21 additions & 0 deletions intranet/apps/bus/migrations/0010_auto_20250520_2301.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 3.2.25 on 2025-05-21 03:01

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('bus', '0009_busdelay'),
]

operations = [
migrations.DeleteModel(
name='BusDelay',
),
migrations.AddField(
model_name='route',
name='reason',
field=models.CharField(blank=True, max_length=50),
),
]
18 changes: 18 additions & 0 deletions intranet/apps/bus/migrations/0011_route_estimated_time_delay.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.2.25 on 2025-05-22 14:00

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('bus', '0010_auto_20250520_2301'),
]

operations = [
migrations.AddField(
model_name='route',
name='estimated_time_delay',
field=models.CharField(blank=True, max_length=5),
),
]
6 changes: 5 additions & 1 deletion intranet/apps/bus/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ class Route(models.Model):
space = models.CharField(max_length=4, blank=True)
bus_number = models.CharField(max_length=5, blank=True)
status = models.CharField("arrival status", choices=ARRIVAL_STATUSES, max_length=1, default="o")
reason = models.CharField(max_length=50, blank=True)
estimated_time_delay = models.CharField(max_length=5, blank=True)

def reset_status(self):
"""Reset status to (on time)"""
self.status = "o"
self.space = ""
self.save(update_fields=["status", "space"])
self.reason = ""
self.estimated_time_delay = ""
self.save(update_fields=["status", "space", "reason", "estimated_time_delay"])

def __str__(self):
return self.route_name
Expand Down
124 changes: 123 additions & 1 deletion intranet/apps/bus/tasks.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
from datetime import timedelta

import requests
from asgiref.sync import async_to_sync
from bs4 import BeautifulSoup
from celery import shared_task
from celery.utils.log import get_task_logger
from channels.layers import get_channel_layer
from django.conf import settings
from django.utils import timezone

from ..schedule.models import Day
from .models import Route

logger = get_task_logger(__name__)
Expand All @@ -9,6 +18,119 @@
@shared_task
def reset_routes() -> None:
logger.info("Resetting bus routes")

for route in Route.objects.all():
route.reset_status()


@shared_task
def schedule_all_bus_delay_fetches():
"""Schedules tasks to fetch FCPS bus delay information throughout the day.

This task calculates multiple intervals based on the start and end times of the day
and schedules the fetch_fcps_bus_delays task to run at both
one-minute and 15-second intervals during idle and active windows.
"""
day = Day.objects.today()
if day is None:
logger.error("No Day found for today")
return

tz = timezone.get_current_timezone()
start_datetime = timezone.make_aware(day.start_time.date_obj(day.date), tz)
end_datetime = timezone.make_aware(day.end_time.date_obj(day.date), tz)

# 1 minute intervals (idle windows)
# Idle window 1: 2.5h before start to 1h after start
first_window_start = start_datetime - timedelta(hours=settings.FIRST_WINDOW_START_BUFFER)
first_window_end = start_datetime + timedelta(hours=settings.FIRST_WINDOW_END_BUFFER)
# Idle window 2: 1h before end to 5m before end
second_window_start = end_datetime - timedelta(minutes=settings.SECOND_WINDOW_START_BUFFER)
second_window_end = end_datetime - timedelta(minutes=settings.SECOND_WINDOW_END_BUFFER)
# Idle window 3: 20m after end to 2h after end
third_window_start = end_datetime + timedelta(minutes=settings.THIRD_WINDOW_START_BUFFER)
third_window_end = end_datetime + timedelta(hours=settings.THIRD_WINDOW_END_BUFFER)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you code these timedelta's in as settings? Also, I feel like checking 1 hour before is unnecessary. Most people are in class and it doesn't really matter if their bus is delayed until 10-15 minutes before dismissal. This is just a suggestion of course

Copy link
Author

@mktaher mktaher May 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand how you want me to code the timedelta's in settings, would you want it like BUS_DELAY_THIRD_WINDOW_START = timedelta(minutes=20)? Because I can't do the windows in as settings because it needs to use start_datetime and end_datetime, or I could just leave it as is?

Copy link
Member

@aarushtools aarushtools May 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah something like this in settings

# Check for bus delays from start of day - MORNING_START_BUFFER to start of day + MORNING_END_BUFFER (minutes)
MORNING_START_BUFFER = 20
MORNING_END_BUFFER = 30

and then in the task

    first_window_start = start_datetime - timedelta(minutes=MORNING_START_BUFFER)
    first_window_end = start_datetime + timedelta(minutes=MORNING_END_BUFFER)

Copy link
Author

@mktaher mktaher May 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

# Bus Delays
# Buffer are used in timedelta for when to schedule the bus delay fetches that occur every 15s seconds or 1 minute respectively.
BUS_DELAY_URL = "https://busdelay.fcps.edu/"
# First Window Buffer: 2.5 hours before start of school to 1 hour after start of school (for 1 minute task)
FIRST_WINDOW_START_BUFFER = 2.5
FIRST_WINDOW_END_BUFFER = 1
# Second Window Buffer: 20 minutes before end of school to 5 minutes before end of school (for 1 minute task)
SECOND_WINDOW_START_BUFFER = 20
SECOND_WINDOW_END_BUFFER = 5
# Third Window Buffer: 20 minutes after end of school to 2 hours after end of school (for 1 minute task)
THIRD_WINDOW_START_BUFFER = 20
THIRD_WINDOW_END_BUFFER = 2
# Active Window Buffer: 5 minutes before end of school to 20 minutes after end of school (for 15 second task)
ACTIVE_WINDOW_START_BUFFER = 5
ACTIVE_WINDOW_END_BUFFER = 20

Is this fine? Please tell me if any of the comments or variable names need changing for any formatting reasons.

# Helper to schedule at 1 minute intervals
def schedule_minutely(start, end):
t = start
while t <= end:
logger.info("Scheduling fetch_fcps_bus_delays to run at %s", t)
fetch_fcps_bus_delays.apply_async(eta=t)
t += timedelta(minutes=1)

schedule_minutely(first_window_start, first_window_end)
schedule_minutely(second_window_start, second_window_end)
schedule_minutely(third_window_start, third_window_end)

# 15 second intervals (active window)
active_window_start = end_datetime - timedelta(minutes=settings.ACTIVE_WINDOW_START_BUFFER)
active_window_end = end_datetime + timedelta(minutes=settings.ACTIVE_WINDOW_END_BUFFER)
t = active_window_start
while t <= active_window_end:
fetch_fcps_bus_delays.apply_async(eta=t)
t += timedelta(seconds=15)

logger.info("Scheduled all bus delay fetches for today.")


@shared_task
def fetch_fcps_bus_delays():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add docstrings

"""Fetches bus delay data from the FCPS website and updates the corresponding bus Route record.

This task retrieves HTML data from the configured BUS_DELAY_URL, parses the HTML to extract
delay information for the "JEFFERSON HIGH" bus, and updates the Route in the database if there is a delay.
"""

url = settings.BUS_DELAY_URL

try:
response = requests.get(url, timeout=10)
response.raise_for_status()
except Exception as e:
logger.error("Error fetching URL: %s", e)
return

try:
soup = BeautifulSoup(response.text, "html.parser")
rows = soup.select("table tr")
except Exception as e:
logger.error("Error parsing HTML: %s", e)
return

if not rows or len(rows) < 2:
logger.warning("Not a complete row with all bus information")
return

# Sort out the JEFFERSON HIGH bus delays
try:
for row in rows[1:]:
cells = row.find_all("td")
if len(cells) >= 4 and cells[0].text.strip() == "JEFFERSON HIGH":
route_name = cells[1].text.strip().split()[0][:100]
reason = cells[3].text.strip()[:150]
estimated_time_delay = cells[2].text.strip()[:10]
try:
obj = Route.objects.get(route_name=route_name)
# Only update if current status isn't "on time"
if obj.status != "a":
obj.status = "d"
obj.reason = reason
obj.estimated_time_delay = estimated_time_delay
obj.save(update_fields=["status", "reason", "estimated_time_delay"])
logger.info("Updated route %s with delay: %s and ETA: %s", route_name, reason, estimated_time_delay)
channel_layer = get_channel_layer()
all_routes = list(Route.objects.values())
async_to_sync(channel_layer.group_send)(
"bus",
{
"type": "bus.update",
"message": {
"allRoutes": all_routes,
},
},
)
except Route.DoesNotExist:
logger.error("Route with route_name %s does not exist", route_name)
except Exception as e:
logger.error("Error processing bus delays: %s", e)
return
13 changes: 9 additions & 4 deletions intranet/apps/bus/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,14 @@ def morning(request, on_home=False):
raise Http404("Bus app not enabled.")
is_bus_admin = request.user.has_admin_permission("bus")
routes = Route.objects.all()
bus_delays_queryset = Route.objects.filter(status="d")
bus_delays = {delay.route_name: {"reason": delay.reason, "estimated_time_delay": delay.estimated_time_delay} for delay in bus_delays_queryset}
ctx = {
"admin": is_bus_admin,
"enable_bus_driver": False,
"bus_list": routes,
"changeover_time": settings.BUS_PAGE_CHANGEOVER_HOUR,
"bus_delays": bus_delays,
"on_home": on_home,
}
return render(request, "bus/morning.html", context=ctx)
Expand All @@ -45,19 +48,21 @@ def afternoon(request, on_home=False):
raise Http404("Bus app not enabled.")
is_bus_admin = request.user.has_admin_permission("bus")

now = timezone.localtime()
current_time = timezone.localtime()
day = Day.objects.today()
if day is not None and day.end_time is not None:
end_of_day = day.end_time.date_obj(now.date())
end_of_day = day.end_time.date_obj(current_time.date())
else:
end_of_day = datetime.datetime(now.year, now.month, now.day, settings.SCHOOL_END_HOUR, settings.SCHOOL_END_MINUTE)

end_of_day = datetime.datetime(current_time.year, current_time.month, current_time.day, settings.SCHOOL_END_HOUR, settings.SCHOOL_END_MINUTE)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you change this to calculate based on schedule too (I know this is old)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean calculate this with the schedule, this else statement only runs if a day doesn't exist (holiday or smth) so how would I use the schedule app because isn't this supposed to be coded like this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops sorry you're right

bus_delays_queryset = Route.objects.filter(status="d")
bus_delays = {delay.route_name: {"reason": delay.reason, "estimated_time_delay": delay.estimated_time_delay} for delay in bus_delays_queryset}
ctx = {
"admin": is_bus_admin,
"enable_bus_driver": True,
"changeover_time": settings.BUS_PAGE_CHANGEOVER_HOUR,
"school_end_hour": end_of_day.hour,
"school_end_time": end_of_day.minute,
"bus_delays": bus_delays,
"on_home": on_home,
}
return render(request, "bus/home.html", context=ctx)
Expand Down
22 changes: 22 additions & 0 deletions intranet/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,23 @@
SENIOR_DESTS_BANNER_LINK = "https://tinyurl.com/tjseniors2025"

""" -------- END UPDATE ANNUALLY -------- """

# Bus Delays
# Buffer are used in timedelta for when to schedule the bus delay fetches that occur every 15s seconds or 1 minute respectively.
BUS_DELAY_URL = "https://busdelay.fcps.edu/"
# First Wwindow Buffer: 2.5 hours before start of school to 1 hour after start of school (for 1 minute task)
FIRST_WINDOW_START_BUFFER = 2.5
FIRST_WINDOW_END_BUFFER = 1
# Second Window Buffer: 20 minutes before end of school to 5 minutes before end of school (for 1 minute task)
SECOND_WINDOW_START_BUFFER = 20
SECOND_WINDOW_END_BUFFER = 5
# Third Window Buffer: 20 minutes after end of school to 2 hours after end of school (for 1 minute task)
THIRD_WINDOW_START_BUFFER = 20
THIRD_WINDOW_END_BUFFER = 2
# Active Window Buffer: 5 minutes before end of school to 20 minutes after end of school (for 15 second task)
ACTIVE_WINDOW_START_BUFFER = 5
ACTIVE_WINDOW_END_BUFFER = 20

# fmt: on

# Default fallback time for start and end of school if no schedule is available
Expand Down Expand Up @@ -955,6 +972,11 @@ def get_log(name): # pylint: disable=redefined-outer-name; 'name' is used as th
"schedule": celery.schedules.crontab(day_of_month=3, hour=1),
"args": (),
},
"schedule-all-bus-delay-fetches": {
"task": "intranet.apps.bus.tasks.schedule_all_bus_delay_fetches",
"schedule": celery.schedules.crontab(hour=0,minute=1),
"args": (),
},
"remove-old-lostfound-entries": {
"task": "intranet.apps.lostfound.tasks.remove_old_lostfound",
"schedule": celery.schedules.crontab(day_of_month=1, hour=1),
Expand Down
15 changes: 15 additions & 0 deletions intranet/templates/bus/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,21 @@ <h2 class="bordered-element bus-announcement-header">
JT-100, JT100, jt-100, jt100, or 100.
</p>
{% endif %}
<div class="delay-announcements">
<h3>Current Bus Delays</h3>
{% if bus_delays %}
{% for route, delay in bus_delays.items %}
<div class="delay-card">
<i class="fas fa-exclamation-triangle"></i>
<span>
<strong>Bus {{ route }}</strong> delayed due to: {{ delay.reason|lower }} <br/> ETA: {{ delay.estimated_time_delay }} minutes.
</span>
</div>
{% endfor %}
{% else %}
<p class="no-delays">No current delays reported.</p>
{% endif %}
</div>
</div>
</div>
<script type="text/template" id="map-view">
Expand Down
15 changes: 15 additions & 0 deletions intranet/templates/bus/morning.html
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,21 @@ <h2 class="bordered-element bus-announcement-header">
JT-100, JT100, jt-100, jt100, or 100.
</p>
{% endif %}
<div class="delay-announcements">
<h3>Current Bus Delays</h3>
{% if bus_delays %}
{% for route, delay in bus_delays.items %}
<div class="delay-card">
<i class="fas fa-exclamation-triangle"></i>
<span>
<strong>Bus {{ route }}</strong> delayed due to: {{ delay.reason|lower }} <br/> ETA: {{ delay.estimated_time_delay }} minutes.
</span>
</div>
{% endfor %}
{% else %}
<p class="no-delays">No current delays reported.</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}