Skip to content

Commit 86c38bb

Browse files
committed
Fixed Backend
1 parent ab7fb4a commit 86c38bb

18 files changed

Lines changed: 638 additions & 143 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ pcr-backup*
2525
./package.json
2626
./yarn.lock
2727
.tool-versions
28-
pcx_test_10_2024.sql.zip
28+
pcx_test_10_2024.sql.zipbackend/course_documents.dump

backend/PennCourses/settings/base.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"""
1212

1313
import os
14+
from dotenv import load_dotenv
1415

1516
import boto3
1617
import dj_database_url
@@ -241,3 +242,5 @@
241242

242243
# Manually Set Cache Prefix
243244
CACHE_PREFIX = "MANUAL_CACHE_"
245+
246+
load_dotenv()
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Generated by Django 5.0.2 on 2026-02-19 03:01
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("degree", "0002_fulfillment_legal_fulfillment_unselected_rules_and_more"),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name="fulfillment",
15+
name="legal",
16+
field=models.BooleanField(
17+
default=True,
18+
help_text=(
19+
"\nTrue if course associated with this fulfillment isn't illegally double"
20+
" counted anywhere,\nfalse otherwise.\n"
21+
),
22+
),
23+
),
24+
migrations.AlterField(
25+
model_name="fulfillment",
26+
name="unselected_rules",
27+
field=models.ManyToManyField(
28+
blank=True,
29+
help_text=(
30+
"\nThe rules this course fulfills that should be shown in the open-ended rule"
31+
" box\n(as opposed to the expandable box). Blank if this course should not be"
32+
" included in\nany open-ended rule boxes.\n"
33+
),
34+
related_name="unselected",
35+
to="degree.rule",
36+
),
37+
),
38+
migrations.AlterField(
39+
model_name="rule",
40+
name="can_double_count_with",
41+
field=models.ManyToManyField(
42+
blank=True,
43+
help_text=(
44+
"\nParent rules that can double count with this rule.\n(i.e. if this rule is"
45+
" Quantitative Data Analysis (a College Foundations req),\nthen this field"
46+
" would contain the General Educations: Sector rule as well as\nthe Major in"
47+
" ___ rule.)\n"
48+
),
49+
to="degree.rule",
50+
),
51+
),
52+
]

backend/docker-compose.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
services:
22
db:
3-
image: postgres:15.8
3+
image: pgvector/pgvector:pg15
44
environment:
55
- POSTGRES_DB=postgres
66
- POSTGRES_USER=penn-courses
77
- POSTGRES_PASSWORD=postgres
8+
- PGDATA=/var/lib/postgresql/pgdata
89
ports:
910
- "5432:5432"
1011
volumes:
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Generated by Django 5.0.2 on 2026-02-19 03:01
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("plan", "0017_break_checked"),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name="break",
15+
name="name",
16+
field=models.CharField(
17+
help_text=(
18+
"The user's name for the break. No two breaks can match in all of the fields"
19+
" `[name, person]`\n"
20+
),
21+
max_length=255,
22+
),
23+
),
24+
]

backend/plan/models.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@
88

99

1010
class Break(models.Model):
11-
"""
12-
Holds break objects created by users on PCP.
13-
"""
11+
"""Holds break objects created by users on PCP."""
1412

1513
person = models.ForeignKey(
1614
get_user_model(),
@@ -35,9 +33,7 @@ class Break(models.Model):
3533
name = models.CharField(
3634
max_length=255,
3735
help_text=dedent(
38-
"""
39-
The user's name for the break. No two breaks can match in all of the fields
40-
`[name, person]`
36+
"""The user's name for the break. No two breaks can match in all of the fields `[name, person]`
4137
"""
4238
),
4339
)

backend/plan/util.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@
22

33

44
def get_first_matching_date(start_date_str, days):
5-
day_map = {"MO": 0, "TU": 1, "WE": 2,
6-
"TH": 3, "FR": 4, "SA": 5, "SU": 6}
7-
start_date = datetime.datetime.strptime(
8-
start_date_str, "%Y-%m-%d").date()
5+
day_map = {"MO": 0, "TU": 1, "WE": 2, "TH": 3, "FR": 4, "SA": 5, "SU": 6}
6+
start_date = datetime.datetime.strptime(start_date_str, "%Y-%m-%d").date()
97
weekdays = [day_map[code] for code in days]
108

119
for i in range(7):

backend/plan/views.py

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,7 @@ def create(self, request):
273273
res["message"] = "Primary schedule successfully unset"
274274
res["message"] = "Primary schedule was already unset"
275275
else:
276-
schedule = Schedule.objects.filter(
277-
person_id=user.id, id=schedule_id).first()
276+
schedule = Schedule.objects.filter(person_id=user.id, id=schedule_id).first()
278277
if not schedule:
279278
res["message"] = "Schedule does not exist"
280279
return JsonResponse(res, status=status.HTTP_400_BAD_REQUEST)
@@ -522,8 +521,7 @@ def validate_name(self, request, existing_schedule=None, allow_path=False):
522521
name,
523522
existing_schedule and existing_schedule.name,
524523
] and not (
525-
allow_path and isinstance(
526-
request.successful_authenticator, PlatformAuthentication)
524+
allow_path and isinstance(request.successful_authenticator, PlatformAuthentication)
527525
):
528526
raise PermissionDenied(
529527
"You cannot create/update/delete a schedule with the name "
@@ -549,8 +547,7 @@ def update(self, request, pk=None):
549547
if from_path:
550548
schedule, _ = self.get_queryset(semester).get_or_create(
551549
name=PATH_REGISTRATION_SCHEDULE_NAME,
552-
defaults={"person": self.request.user,
553-
"semester": semester},
550+
defaults={"person": self.request.user, "semester": semester},
554551
)
555552
else:
556553
schedule = self.get_queryset(semester).get(id=pk)
@@ -560,12 +557,10 @@ def update(self, request, pk=None):
560557
status=status.HTTP_403_FORBIDDEN,
561558
)
562559

563-
name = self.validate_name(
564-
request, existing_schedule=schedule, allow_path=from_path)
560+
name = self.validate_name(request, existing_schedule=schedule, allow_path=from_path)
565561

566562
try:
567-
sections = self.get_sections(
568-
request.data, semester, skip_missing=from_path)
563+
sections = self.get_sections(request.data, semester, skip_missing=from_path)
569564
except ObjectDoesNotExist:
570565
return Response(
571566
{"detail": "One or more sections not found in database."},
@@ -650,8 +645,7 @@ def create(self, request, *args, **kwargs):
650645
def get_queryset(self, semester=None):
651646
if not semester:
652647
semester = get_current_semester()
653-
queryset = Schedule.objects.filter(
654-
person=self.request.user, semester=semester)
648+
queryset = Schedule.objects.filter(person=self.request.user, semester=semester)
655649
queryset = queryset.prefetch_related(
656650
Prefetch("sections", Section.with_reviews.all()),
657651
"sections__associated_sections",
@@ -682,8 +676,7 @@ def get(self):
682676
Get all breaks for the current user.
683677
"""
684678
breaks = self.get_queryset()
685-
serializer = BreakSerializer(
686-
breaks, many=True, context=self.get_serializer_context())
679+
serializer = BreakSerializer(breaks, many=True, context=self.get_serializer_context())
687680
return Response(serializer.data, status=status.HTTP_200_OK)
688681

689682
def update(self, request, *args, **kwargs):
@@ -880,8 +873,7 @@ def get(self, *args, **kwargs):
880873
day_mapping = {"M": "MO", "T": "TU", "W": "WE", "R": "TH", "F": "FR"}
881874

882875
calendar = ICSCal(creator="Penn Labs")
883-
calendar.extra.append(ContentLine(
884-
name="X-WR-CALNAME", value=f"{schedule.name} Schedule"))
876+
calendar.extra.append(ContentLine(name="X-WR-CALNAME", value=f"{schedule.name} Schedule"))
885877

886878
for section in schedule.sections.all():
887879
e = ICSEvent()
@@ -908,10 +900,8 @@ def get(self, *args, **kwargs):
908900
start_datetime = ""
909901
end_datetime = ""
910902
else:
911-
start_datetime = get_first_matching_date(
912-
first_meeting.start_date, days) + " "
913-
end_datetime = get_first_matching_date(
914-
first_meeting.start_date, days) + " "
903+
start_datetime = get_first_matching_date(first_meeting.start_date, days) + " "
904+
end_datetime = get_first_matching_date(first_meeting.start_date, days) + " "
915905

916906
if int(first_meeting.start) < 10:
917907
start_datetime += "0"
@@ -921,11 +911,9 @@ def get(self, *args, **kwargs):
921911
start_datetime += start_time
922912
end_datetime += end_time
923913

924-
e.begin = arrow.get(start_datetime, "YYYY-MM-DD h:mm A",
925-
tzinfo="America/New_York")
914+
e.begin = arrow.get(start_datetime, "YYYY-MM-DD h:mm A", tzinfo="America/New_York")
926915

927-
e.end = arrow.get(end_datetime, "YYYY-MM-DD h:mm A",
928-
tzinfo="America/New York")
916+
e.end = arrow.get(end_datetime, "YYYY-MM-DD h:mm A", tzinfo="America/New York")
929917

930918
location = None
931919
if hasattr(first_meeting, "room") and first_meeting.room:

backend/pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,16 @@ dependencies = [
5454
"asyncio==3.4.3",
5555
"aiohttp==3.10.10",
5656
"openai>=2.6.1",
57+
"python-dotenv>=1.1.0",
58+
"pgvector>=0.4.2",
5759
]
5860

5961
[tool.black]
6062
line-length = 100
63+
preview = true
64+
65+
[tool.flake8]
66+
max-line-length = 100
6167

6268
[dependency-groups]
6369
dev = [
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import os
2+
import subprocess
3+
from pathlib import Path
4+
5+
from django.core.management.base import BaseCommand, CommandError
6+
from django.db import connection
7+
8+
9+
class Command(BaseCommand):
10+
help = "Load course_documents from a pg_dump .dump archive (data-only) into the DB."
11+
12+
def add_arguments(self, parser):
13+
parser.add_argument("dump_path", type=str)
14+
parser.add_argument("--force", action="store_true")
15+
16+
def handle(self, *args, **opts):
17+
dump_path = Path(opts["dump_path"]).expanduser().resolve()
18+
force = opts["force"]
19+
20+
if not dump_path.exists():
21+
raise CommandError(f"File not found: {dump_path}")
22+
23+
with connection.cursor() as c:
24+
c.execute("SELECT to_regclass('public.course_documents')")
25+
if c.fetchone()[0] is None:
26+
raise CommandError("course_documents table missing. Run migrations first.")
27+
28+
c.execute("SELECT COUNT(*) FROM course_documents")
29+
(count,) = c.fetchone()
30+
31+
if count > 0 and not force:
32+
raise CommandError(
33+
f"course_documents already has {count} rows. Re-run with --force to overwrite."
34+
)
35+
36+
# wipe existing rows so restore is idempotent for teammates
37+
c.execute("TRUNCATE TABLE course_documents;")
38+
39+
db = connection.settings_dict
40+
env = os.environ.copy()
41+
if db.get("PASSWORD"):
42+
env["PGPASSWORD"] = db["PASSWORD"]
43+
44+
cmd = ["pg_restore", "--data-only", "--no-owner", "--no-privileges", "-d", db["NAME"]]
45+
if db.get("HOST"):
46+
cmd += ["-h", db["HOST"]]
47+
if db.get("PORT"):
48+
cmd += ["-p", str(db["PORT"])]
49+
if db.get("USER"):
50+
cmd += ["-U", db["USER"]]
51+
cmd += [str(dump_path)]
52+
53+
self.stdout.write("Running: " + " ".join(cmd))
54+
subprocess.run(cmd, check=True, env=env)
55+
56+
with connection.cursor() as c:
57+
c.execute("SELECT COUNT(*) FROM course_documents")
58+
(new_count,) = c.fetchone()
59+
60+
self.stdout.write(self.style.SUCCESS(f"Imported {new_count} rows into course_documents."))

0 commit comments

Comments
 (0)