Skip to content

Commit 60bb104

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

File tree

120 files changed

+305
-207
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

120 files changed

+305
-207
lines changed

config/scripts/create_users.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@
55
import random
66
from datetime import datetime
77

8+
import django
89
import names
910
from dateutil.relativedelta import relativedelta
1011

11-
import django
12-
1312
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "intranet.settings")
1413
django.setup()
1514

fabfile.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def _choose_from_list(options, question):
1515
"""Choose an item from a list."""
1616
message = ""
1717
for index, value in enumerate(options):
18-
message += "[{}] {}\n".format(index, value)
18+
message += f"[{index}] {value}\n"
1919
message += "\n" + question
2020

2121
def valid(n):
@@ -44,7 +44,7 @@ def runserver(
4444
yes_or_no = ("debug_toolbar", "werkzeug", "dummy_cache", "short_cache", "template_warnings", "insecure")
4545
for s in yes_or_no:
4646
if locals()[s].lower() not in ("yes", "no"):
47-
abort("Specify 'yes' or 'no' for {} option.".format(s))
47+
abort(f"Specify 'yes' or 'no' for {s} option.")
4848

4949
_log_levels = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
5050
if log_level not in _log_levels:
@@ -120,9 +120,9 @@ def clear_sessions(venv=None):
120120
return 0
121121

122122
if count > 0:
123-
local("{0}| xargs redis-cli -n " "{1} DEL".format(keys_command, REDIS_SESSION_DB))
123+
local(f"{keys_command}| xargs redis-cli -n " f"{REDIS_SESSION_DB} DEL")
124124

125-
puts("Destroyed {} session{}.".format(count, plural))
125+
puts(f"Destroyed {count} session{plural}.")
126126

127127

128128
def clear_cache(input=None):
@@ -133,9 +133,9 @@ def clear_cache(input=None):
133133
n = _choose_from_list(["Production cache", "Sandbox cache"], "Which cache would you like to clear?")
134134

135135
if n == 0:
136-
local("redis-cli -n {} FLUSHDB".format(REDIS_PRODUCTION_CACHE_DB))
136+
local(f"redis-cli -n {REDIS_PRODUCTION_CACHE_DB} FLUSHDB")
137137
else:
138-
local("redis-cli -n {} FLUSHDB".format(REDIS_SANDBOX_CACHE_DB))
138+
local(f"redis-cli -n {REDIS_SANDBOX_CACHE_DB} FLUSHDB")
139139

140140

141141
def contributors():
@@ -156,7 +156,7 @@ def deploy():
156156
with lcd(PRODUCTION_DOCUMENT_ROOT):
157157
with shell_env(PRODUCTION="TRUE"):
158158
local("git pull")
159-
with open("requirements.txt", "r") as req_file:
159+
with open("requirements.txt") as req_file:
160160
requirements = req_file.read().strip().split()
161161
try:
162162
pkg_resources.require(requirements)
@@ -179,8 +179,8 @@ def forcemigrate(app=None):
179179
"""Force migrations to apply for a given app."""
180180
if app is None:
181181
abort("No app name given.")
182-
local("./manage.py migrate {} --fake".format(app))
183-
local("./manage.py migrate {}".format(app))
182+
local(f"./manage.py migrate {app} --fake")
183+
local(f"./manage.py migrate {app}")
184184

185185

186186
def inspect_decorators():

intranet/apps/announcements/api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
class IsAnnouncementAdminOrReadOnly(permissions.BasePermission):
1212
def has_permission(self, request, view):
1313
return (
14-
request.user and request.user.is_authenticated and not request.user.is_restricted and request.user.oauth_and_api_access or request.auth
15-
) and (request.method in permissions.SAFE_METHODS or request.user and request.user.is_announcements_admin)
14+
(request.user and request.user.is_authenticated and not request.user.is_restricted and request.user.oauth_and_api_access) or request.auth
15+
) and (request.method in permissions.SAFE_METHODS or (request.user and request.user.is_announcements_admin))
1616

1717

1818
class ListCreateAnnouncement(generics.ListCreateAPIView):

intranet/apps/announcements/models.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -175,14 +175,12 @@ def is_visible(self, user):
175175
return self in Announcement.objects.visible_to_user(user)
176176

177177
def can_modify(self, user):
178-
return (
179-
user.is_announcements_admin
180-
or self.is_club_announcement
178+
return user.is_announcements_admin or (
179+
self.is_club_announcement
181180
and (
182181
user in self.activity.officers.all()
183182
or user in self.activity.club_sponsors.all()
184-
or EighthSponsor.objects.filter(user=user).exists()
185-
and user.sponsor_obj in self.activity.sponsors.all()
183+
or (EighthSponsor.objects.filter(user=user).exists() and user.sponsor_obj in self.activity.sponsors.all())
186184
)
187185
)
188186

@@ -205,7 +203,7 @@ def is_visible_requester(self, user):
205203

206204
def is_visible_submitter(self, user):
207205
try:
208-
return self.user == user or self.announcementrequest and user == self.announcementrequest.user
206+
return self.user == user or (self.announcementrequest and user == self.announcementrequest.user)
209207
except get_user_model().DoesNotExist:
210208
return False
211209

intranet/apps/announcements/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ def delete_announcement_view(request, announcement_id):
436436
"""
437437
announcement = get_object_or_404(Announcement, id=announcement_id)
438438

439-
if not (request.user.is_announcements_admin or announcement.is_club_announcement and announcement.can_modify(request.user)):
439+
if not (request.user.is_announcements_admin or (announcement.is_club_announcement and announcement.can_modify(request.user))):
440440
messages.error(request, "You do not have permission to delete this announcement.")
441441
return redirect("index")
442442

intranet/apps/auth/helpers.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ def change_password(form_data):
1111
if form_data:
1212
form_data["username"] = re.sub(r"\W", "", form_data["username"])
1313
if (
14-
form_data
15-
and form_data["username"] == "unknown"
14+
(form_data and form_data["username"] == "unknown")
1615
or form_data["old_password"] is None
1716
or form_data["new_password"] is None
1817
or form_data["new_password_confirm"] is None
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 & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
import requests
2+
from asgiref.sync import async_to_sync
3+
from bs4 import BeautifulSoup
14
from celery import shared_task
25
from celery.utils.log import get_task_logger
6+
from channels.layers import get_channel_layer
7+
from django.utils import timezone
38

49
from .models import Route
510

@@ -12,3 +17,54 @@ def reset_routes() -> None:
1217

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

0 commit comments

Comments
 (0)