From 950b47c415a3bab199039c21f0ea97221cca9beb Mon Sep 17 00:00:00 2001 From: Christopher Liu Date: Fri, 6 Oct 2023 17:55:42 -0400 Subject: [PATCH] Everything is a rule --- .../management/commands/import_degreeworks.py | 2 +- .../migrations/0004_auto_20231006_1754.py | 84 +++++++++++++++++ backend/degree/models.py | 91 ++++--------------- backend/degree/utils/parse_degreeworks.py | 8 +- 4 files changed, 109 insertions(+), 76 deletions(-) create mode 100644 backend/degree/migrations/0004_auto_20231006_1754.py diff --git a/backend/degree/management/commands/import_degreeworks.py b/backend/degree/management/commands/import_degreeworks.py index 4f5b417a8..cb8e4ec47 100644 --- a/backend/degree/management/commands/import_degreeworks.py +++ b/backend/degree/management/commands/import_degreeworks.py @@ -1,7 +1,7 @@ import tqdm from django.core.management.base import BaseCommand -from degree.models import DegreePlan, Requirement, Rule +from degree.models import DegreePlan, Rule from degree.utils.request_degreeworks import audit, degree_plans_of, get_programs, write_dp diff --git a/backend/degree/migrations/0004_auto_20231006_1754.py b/backend/degree/migrations/0004_auto_20231006_1754.py new file mode 100644 index 000000000..b951af59b --- /dev/null +++ b/backend/degree/migrations/0004_auto_20231006_1754.py @@ -0,0 +1,84 @@ +# Generated by Django 3.2.20 on 2023-10-06 21:54 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ("degree", "0003_auto_20231001_1145"), + ] + + operations = [ + migrations.RemoveField( + model_name="rule", + name="max_cus", + ), + migrations.RemoveField( + model_name="rule", + name="max_num", + ), + migrations.RemoveField( + model_name="rule", + name="min_cus", + ), + migrations.RemoveField( + model_name="rule", + name="min_num", + ), + migrations.RemoveField( + model_name="rule", + name="requirement", + ), + migrations.AddField( + model_name="rule", + name="cus", + field=models.DecimalField( + decimal_places=1, + help_text="\nThe minimum number of CUs required for this rule. Only non-null\nif this is a Rule leaf.\n", + max_digits=4, + null=True, + ), + ), + migrations.AddField( + model_name="rule", + name="degree_plan", + field=models.ForeignKey( + default=0, + help_text="\nThe degree plan that has this rule.\n", + on_delete=django.db.models.deletion.CASCADE, + to="degree.degreeplan", + ), + preserve_default=False, + ), + migrations.AddField( + model_name="rule", + name="num", + field=models.IntegerField( + help_text="\nThe minimum number of courses or subrules required for this rule.\n", + null=True, + ), + ), + migrations.AddField( + model_name="rule", + name="parent", + field=models.ForeignKey( + help_text="\nThis rule's parent Rule if it has one.\n", + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="degree.rule", + ), + ), + migrations.AlterField( + model_name="rule", + name="q", + field=models.TextField( + help_text="\nString representing a Q() object that returns the set of courses\nsatisfying this rule. Only non-empty if this is a Rule leaf.\n", + max_length=1000, + ), + ), + migrations.DeleteModel( + name="Requirement", + ), + ] diff --git a/backend/degree/models.py b/backend/degree/models.py index 0189b1505..4ec4e6bbe 100644 --- a/backend/degree/models.py +++ b/backend/degree/models.py @@ -1,8 +1,5 @@ from django.db import models from textwrap import dedent -from django.contrib.auth import get_user_model - -from courses.models import Topic, Course, string_dict_to_html class DegreePlan(models.Model): @@ -61,110 +58,62 @@ def __str__(self) -> str: return f"{self.program} {self.degree} in {self.major} with conc. {self.concentration} ({self.year})" -class Requirement(models.Model): +class Rule(models.Model): """ - This model represents a degree requirement. + This model represents a degree requirement rule. """ - name = models.CharField( - max_length=256, - help_text=dedent( - """ - The name of this requirement, e.g., General Education, Foundations - """ - ), - ) - code = models.CharField( - max_length=32, + num = models.IntegerField( + null=True, help_text=dedent( """ - The canonical code for this requirement, e.g., U-GE-FND + The minimum number of courses or subrules required for this rule. """ ), ) - min_cus = models.DecimalField( + + cus = models.DecimalField( decimal_places=1, max_digits=4, null=True, help_text=dedent( """ - The minimum number of CUs required to qualify for this degree requirement + The minimum number of CUs required for this rule. Only non-null + if this is a Rule leaf. """ ), ) - degree_plan = models.ManyToManyField( + + degree_plan = models.ForeignKey( DegreePlan, + on_delete=models.CASCADE, help_text=dedent( """ - The degree plan(s) that have this requirement. + The degree plan that has this rule. """ ), ) - def __str__(self) -> str: - return f"{self.name} ({self.code}), min_cus={self.min_cus}, degree_plan={self.degree_plan}" - - -class Rule(models.Model): - """ - This model represents a degree requirement rule. A rule has a Q object - representing courses that can fulfill this rule and a number of required - courses, number of required CUs, or both. - """ - q = models.TextField( max_length=1000, help_text=dedent( """ - String representing a Q() object that returns the set of courses satisfying this rule. + String representing a Q() object that returns the set of courses + satisfying this rule. Only non-empty if this is a Rule leaf. """ ), ) - min_num = models.IntegerField( - null=True, - help_text=dedent( - """ - The minimum number of courses required for this rule. - """ - ), - ) - max_num = models.IntegerField( - null=True, - help_text=dedent( - """ - The maximum number of courses required for this rule. - """ - ), - ) - min_cus = models.DecimalField( - decimal_places=1, - max_digits=4, - null=True, - help_text=dedent( - """ - The minimum number of CUs required for this rule. - """ - ), - ) - max_cus = models.DecimalField( - decimal_places=1, - max_digits=4, + + parent = models.ForeignKey( + "self", null=True, - help_text=dedent( - """ - The maximum number of CUs required for this rule. - """ - ), - ) - requirement = models.ForeignKey( - Requirement, on_delete=models.CASCADE, help_text=dedent( """ - The degree requirement that has this rule. + This rule's parent Rule if it has one. """ ), ) def __str__(self) -> str: - return f"{self.q}, min_num={self.min_num}, max_num={self.max_num}, min_cus={self.min_cus}, max_cus={self.max_cus}, requirement={self.requirement}" + return f"{self.q}, num={self.num}, cus={self.cus}, degree_plan={self.degree_plan}" diff --git a/backend/degree/utils/parse_degreeworks.py b/backend/degree/utils/parse_degreeworks.py index 302d49a50..55c6b08a1 100644 --- a/backend/degree/utils/parse_degreeworks.py +++ b/backend/degree/utils/parse_degreeworks.py @@ -1,6 +1,6 @@ from django.db.models import Q -from degree.models import DegreePlan, Requirement, Rule +from degree.models import DegreePlan, Rule # TODO: these should not be hardcoded, but rather added to the database E_DEPTS = ["BE", "CIS", "CMPE", "EAS", "ESE", "MEAM", "MSE", "NETS", "ROBO"] # SEAS @@ -354,9 +354,9 @@ def parse_rulearray(ruleArray, degree_plan, parent_rule=None) -> list[Rule]: # TODO: Make the function names more descriptive -def parse_degreeworks(json: str, degree_plan: DegreePlan) -> list[Requirement]: +def parse_degreeworks(json: str, degree_plan: DegreePlan) -> list[Rule]: """ - Returns a list of Requirements given a DegreeWorks JSON audit and a DegreePlan. + Returns a list of Rules given a DegreeWorks JSON audit and a DegreePlan. """ blockArray = json.get("blockArray") @@ -364,7 +364,7 @@ def parse_degreeworks(json: str, degree_plan: DegreePlan) -> list[Requirement]: degree_reqs = [] for requirement in blockArray: - degree_req = Requirement( + degree_req = Rule( name=requirement["title"], code=requirement["requirementValue"], # TODO: parse min_cus