Skip to content

Commit

Permalink
Merge branch 'master' into jhawk0224/dock-clear-button
Browse files Browse the repository at this point in the history
  • Loading branch information
JHawk0224 authored Apr 10, 2024
2 parents bff1828 + 98f39b8 commit 27b1128
Show file tree
Hide file tree
Showing 127 changed files with 6,222 additions and 40,925 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/shared-build-deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:
strategy:
fail-fast: true
matrix:
product: [review, plan, alert]
product: [review, plan, alert, degree-plan]
with:
path: frontend/${{ matrix.product }}
nodeVersion: 18.19.0
Expand All @@ -62,7 +62,7 @@ jobs:
strategy:
fail-fast: true
matrix:
product: [{name: pcr, path: review}, {name: pcp, path: plan}, {name: pca, path: alert}]
product: [{name: pcr, path: review}, {name: pcp, path: plan}, {name: pca, path: alert}, {name: pdp, path: degree-plan}]
with:
# Inputs
imageName: "${{ matrix.product.name }}-frontend"
Expand Down
2,359 changes: 2,357 additions & 2 deletions backend/Pipfile.lock

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions backend/courses/management/commands/recompute_soft_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@ def recompute_enrollment():
)


# course credits = sum(section credis for all activities)
# course credits = sum(section credis for all activities for sections below 500)
# the < 500 heuristic comes from here:
# https://provider.www.upenn.edu/computing/da/dw/student/enrollment_section_type.e.html
COURSE_CREDITS_RAW_SQL = dedent(
"""
WITH CourseCredits AS (
Expand All @@ -108,6 +110,7 @@ def recompute_enrollment():
INNER JOIN (
SELECT MAX(U1."credits") AS "activity_cus", U1."course_id"
FROM "courses_section" U1
WHERE U1."code" < '500' AND (U1."status" <> 'X' OR U1."status" <> '')
GROUP BY U1."course_id", U1."activity"
) AS U2
ON U0."id" = U2."course_id"
Expand All @@ -125,7 +128,6 @@ def recompute_enrollment():
def recompute_course_credits(
model=Course, # so this function can be used in migrations (see django.db.migrations.RunPython)
):

with connection.cursor() as cursor:
cursor.execute(COURSE_CREDITS_RAW_SQL)

Expand Down
28 changes: 26 additions & 2 deletions backend/courses/management/commands/recompute_topics.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from django.core.management.base import BaseCommand
from django.db import transaction
from django.db.models import Count, OuterRef, Subquery
from tqdm import tqdm

from courses.models import Course, Topic
from courses.util import all_semesters
from courses.util import all_semesters, historical_semester_probability


def garbage_collect_topics():
Expand Down Expand Up @@ -151,5 +152,28 @@ def handle(self, *args, **kwargs):
assert (
min_semester in all_semesters()
), f"--min-semester={min_semester} is not a valid semester."

semesters = sorted(
[sem for sem in all_semesters() if not min_semester or sem >= min_semester]
)
recompute_topics(min_semester, verbose=True, allow_null_parent_topic=bool(min_semester))
recompute_historical_semester_probabilities(current_semester=semesters[-1], verbose=True)


def recompute_historical_semester_probabilities(current_semester, verbose=False):
"""
Recomputes the historical probabilities for all topics.
"""
if verbose:
print("Recomputing historical probabilities for all topics...")
topics = Topic.objects.all()
# Iterate over each Topic
for i, topic in tqdm(enumerate(topics), disable=not verbose, total=topics.count()):
# Calculate historical_year_probability for the current topic
ordered_courses = topic.courses.all().order_by("semester")
ordered_semester = [course.semester for course in ordered_courses]
historical_prob = historical_semester_probability(current_semester, ordered_semester)
# Update the historical_probabilities field for the current topic
topic.historical_probabilities_spring = historical_prob[0]
topic.historical_probabilities_summer = historical_prob[1]
topic.historical_probabilities_fall = historical_prob[2]
topic.save()
66 changes: 66 additions & 0 deletions backend/courses/migrations/0064_auto_20240225_1331.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Generated by Django 3.2.23 on 2024-02-25 18:31

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("courses", "0063_auto_20231212_1750"),
]

operations = [
migrations.AddField(
model_name="topic",
name="historical_probabilities_fall",
field=models.FloatField(
default=0,
help_text="\nThe historical probability of a student taking a course in this topic in the fall\nsemester, based on historical data. This field is recomputed nightly from the\n`parent_course` graph (in the recompute_soft_state cron job).\n",
),
),
migrations.AddField(
model_name="topic",
name="historical_probabilities_spring",
field=models.FloatField(
default=0,
help_text="\nThe historical probability of a student taking a course in this topic in the spring\nsemester, based on historical data. This field is recomputed nightly from the\n`parent_course` graph (in the recompute_soft_state cron job).\n",
),
),
migrations.AddField(
model_name="topic",
name="historical_probabilities_summer",
field=models.FloatField(
default=0,
help_text="\nThe historical probability of a student taking a course in this topic in the summer\nsemester, based on historical data. This field is recomputed nightly from the\n`parent_course` graph (in the recompute_soft_state cron job).\n",
),
),
migrations.AlterField(
model_name="section",
name="activity",
field=models.CharField(
choices=[
("", "Undefined"),
("CLN", "Clinic"),
("CRT", "Clinical Rotation"),
("DAB", "Dissertation Abroad"),
("DIS", "Dissertation"),
("DPC", "Doctoral Program Exchange"),
("FLD", "Field Work"),
("HYB", "Hybrid"),
("IND", "Independent Study"),
("LAB", "Lab"),
("LEC", "Lecture"),
("MST", "Masters Thesis"),
("ONL", "Online"),
("PRC", "Practicum"),
("REC", "Recitation"),
("SEM", "Seminar"),
("SRT", "Senior Thesis"),
("STU", "Studio"),
],
db_index=True,
help_text='The section activity, e.g. `LEC` for CIS-120-001 (2020A). Options and meanings: <table width=100%><tr><td>""</td><td>"Undefined"</td></tr><tr><td>"CLN"</td><td>"Clinic"</td></tr><tr><td>"CRT"</td><td>"Clinical Rotation"</td></tr><tr><td>"DAB"</td><td>"Dissertation Abroad"</td></tr><tr><td>"DIS"</td><td>"Dissertation"</td></tr><tr><td>"DPC"</td><td>"Doctoral Program Exchange"</td></tr><tr><td>"FLD"</td><td>"Field Work"</td></tr><tr><td>"HYB"</td><td>"Hybrid"</td></tr><tr><td>"IND"</td><td>"Independent Study"</td></tr><tr><td>"LAB"</td><td>"Lab"</td></tr><tr><td>"LEC"</td><td>"Lecture"</td></tr><tr><td>"MST"</td><td>"Masters Thesis"</td></tr><tr><td>"ONL"</td><td>"Online"</td></tr><tr><td>"PRC"</td><td>"Practicum"</td></tr><tr><td>"REC"</td><td>"Recitation"</td></tr><tr><td>"SEM"</td><td>"Seminar"</td></tr><tr><td>"SRT"</td><td>"Senior Thesis"</td></tr><tr><td>"STU"</td><td>"Studio"</td></tr></table>',
max_length=50,
),
),
]
30 changes: 30 additions & 0 deletions backend/courses/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,36 @@ class Topic(models.Model):
),
)

historical_probabilities_spring = models.FloatField(
default=0,
help_text=dedent(
"""
The historical probability of a student taking a course in this topic in the spring
semester, based on historical data. This field is recomputed nightly from the
`parent_course` graph (in the recompute_soft_state cron job).
"""
),
)
historical_probabilities_summer = models.FloatField(
default=0,
help_text=dedent(
"""
The historical probability of a student taking a course in this topic in the summer
semester, based on historical data. This field is recomputed nightly from the
`parent_course` graph (in the recompute_soft_state cron job).
"""
),
)
historical_probabilities_fall = models.FloatField(
default=0,
help_text=dedent(
"""
The historical probability of a student taking a course in this topic in the fall
semester, based on historical data. This field is recomputed nightly from the
`parent_course` graph (in the recompute_soft_state cron job).
"""
),
)
branched_from = models.ForeignKey(
"Topic",
related_name="branched_to",
Expand Down
19 changes: 18 additions & 1 deletion backend/courses/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
StatusUpdate,
UserProfile,
)
from plan.management.commands.recommendcourses import cosine_similarity


class MeetingSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -251,7 +252,23 @@ def get_num_sections(self, obj):
return obj.sections.count()

def get_recommendation_score(self, obj):
return 0
user_vector = self.context.get("user_vector")
curr_course_vectors_dict = self.context.get("curr_course_vectors_dict")

if user_vector is None or curr_course_vectors_dict is None:
# NOTE: there should be no case in which user_vector is None
# but curr_course_vectors_dict is not None. However, for
# stability in production, recommendation_score is None when
# either is None
return None

course_vector = curr_course_vectors_dict.get(obj.full_code)
if course_vector is None:
# Fires when the curr_course_vectors_dict is defined (ie, the user is authenticated)
# but the course code is not in the model
return None

return cosine_similarity(course_vector, user_vector)

course_quality = serializers.DecimalField(
max_digits=4, decimal_places=3, read_only=True, help_text=course_quality_help
Expand Down
50 changes: 50 additions & 0 deletions backend/courses/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -721,3 +721,53 @@ def get_semesters(semesters: str = None) -> list[str]:
if s not in possible_semesters:
raise ValueError(f"Provided semester {s} was not found in the db.")
return sorted(semesters)


def historical_semester_probability(current_semester: str, semesters: list[str]):
"""
:param current: The current semester represented in the 20XX(A|B|C) format.
:type current: str
:param courses: A list of Course objects sorted by date in ascending order.
:type courses: list
:returns: A list of 3 probabilities representing the likelihood of
taking a course in each semester.
:rtype: list
"""
PROB_DISTRIBUTION = [0.4, 0.3, 0.15, 0.1, 0.05]

def normalize_and_round(prob, i):
"""Modifies the probability distribution to account for the
fact that the last course was taken i years ago."""
truncate = PROB_DISTRIBUTION[:i]
total = sum(truncate)
return list(map(lambda x: round(x / total, 3), truncate))

semester_probabilities = {"A": 0.0, "B": 0.0, "C": 0.0}
current_year = int(current_semester[:-1])
semesters = [
semester
for semester in semesters
if semester < str(current_year) and semester > str(current_year - 5)
]
if not semesters:
return [0, 0, 0]
if current_year - int(semesters[0][:-1]) < 5:
# If the class hasn't been offered in the last 5 years,
# we make sure the resulting probabilities sum to 1
modified_prob_distribution = normalize_and_round(
PROB_DISTRIBUTION, current_year - int(semesters[0][:-1])
)
else:
modified_prob_distribution = PROB_DISTRIBUTION
for historical_semester in semesters:
historical_year = int(historical_semester[:-1])
sem_char = historical_semester[-1].upper() # A, B, C
semester_probabilities[sem_char] += modified_prob_distribution[
current_year - historical_year - 1
]
return list(
map(
lambda x: min(round(x, 2), 1.00),
[semester_probabilities["A"], semester_probabilities["B"], semester_probabilities["C"]],
)
)
18 changes: 16 additions & 2 deletions backend/courses/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
)
from courses.util import get_current_semester
from PennCourses.docs_settings import PcxAutoSchema
from plan.management.commands.recommendcourses import retrieve_course_clusters, vectorize_user


SEMESTER_PARAM_DESCRIPTION = (
Expand Down Expand Up @@ -66,8 +67,12 @@ def filter_by_semester(self, queryset):
semester = self.get_semester()
if semester != "all":
queryset = queryset.filter(**{self.get_semester_field(): semester})
else:
queryset = queryset.order_by("full_code", "-semester").distinct("full_code")
else: # Only used for Penn Degree Plan (as of 4/10/2024)
queryset = (
queryset.exclude(credits=None) # heuristic: if the credits are empty, then ignore
.order_by("full_code", "-semester")
.distinct("full_code")
)
return queryset

def get_queryset(self):
Expand Down Expand Up @@ -209,6 +214,15 @@ def get_serializer_context(self):

if self.request is None or not self.request.user or not self.request.user.is_authenticated:
return context

_, _, curr_course_vectors_dict, past_course_vectors_dict = retrieve_course_clusters()
user_vector, _ = vectorize_user(
self.request.user, curr_course_vectors_dict, past_course_vectors_dict
)
context.update(
{"user_vector": user_vector, "curr_course_vectors_dict": curr_course_vectors_dict}
)

return context

filter_backends = [TypedCourseSearchBackend, CourseSearchFilterBackend]
Expand Down
23 changes: 11 additions & 12 deletions backend/degree/admin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from django.conf.urls import url
from django.contrib import admin
from django.template.response import TemplateResponse
from django.urls import reverse
from django.urls import re_path, reverse
from django.utils.html import format_html

from degree.models import (
Expand All @@ -16,34 +15,25 @@


# Register your models here.
@admin.register(Rule)
class RuleAdmin(admin.ModelAdmin):
search_fields = ["title", "id"]
list_display = ["title", "id", "parent"]
list_select_related = ["parent"]


admin.site.register(DegreePlan)
admin.site.register(SatisfactionStatus)


@admin.register(PDPBetaUser)
class PDPBetaUserAdmin(admin.ModelAdmin):
search_fields = ("person__username", "person__id")
autocomplete_fields = ("person",)


@admin.register(Fulfillment)
class FulfillmentAdmin(admin.ModelAdmin):
autocomplete_fields = ["rules"]


@admin.register(DoubleCountRestriction)
class DoubleCountRestrictionAdmin(admin.ModelAdmin):
autocomplete_fields = ["rule", "other_rule"]


@admin.register(Degree)
class DegreeAdmin(admin.ModelAdmin):
autocomplete_fields = ["rules"]
list_display = ["program", "degree", "major", "concentration", "year", "view_degree_editor"]
Expand All @@ -59,7 +49,7 @@ def get_urls(self):
# get the default urls
urls = super().get_urls()
custom_urls = [
url(
re_path(
r"^degree-editor/$",
self.admin_site.admin_view(self.degree_editor),
name="degree-editor",
Expand All @@ -70,3 +60,12 @@ def get_urls(self):
def degree_editor(self, request):
context = dict(self.admin_site.each_context(request))
return TemplateResponse(request, "degree-editor.html", context)


admin.site.register(Rule, RuleAdmin)
admin.site.register(DegreePlan)
admin.site.register(SatisfactionStatus)
admin.site.register(PDPBetaUser, PDPBetaUserAdmin)
admin.site.register(Fulfillment, FulfillmentAdmin)
admin.site.register(DoubleCountRestriction, DoubleCountRestrictionAdmin)
admin.site.register(Degree, DegreeAdmin)
Loading

0 comments on commit 27b1128

Please sign in to comment.