diff --git a/backend/degree/admin.py b/backend/degree/admin.py index 7d136fde9..21266a375 100644 --- a/backend/degree/admin.py +++ b/backend/degree/admin.py @@ -4,7 +4,14 @@ from django.urls import reverse from django.utils.html import format_html -from degree.models import Degree, DegreePlan, DoubleCountRestriction, Rule, SatisfactionStatus, Fulfillment +from degree.models import ( + Degree, + DegreePlan, + DoubleCountRestriction, + Rule, + SatisfactionStatus, + Fulfillment, +) # Register your models here. @@ -18,10 +25,12 @@ class RuleAdmin(admin.ModelAdmin): admin.site.register(DegreePlan) admin.site.register(SatisfactionStatus) + @admin.register(Fulfillment) class FulfillmentAdmin(admin.ModelAdmin): autocomplete_fields = ["rules"] + @admin.register(DoubleCountRestriction) class DoubleCountRestrictionAdmin(admin.ModelAdmin): autocomplete_fields = ["rule", "other_rule"] diff --git a/backend/degree/migrations/0001_initial.py b/backend/degree/migrations/0001_initial.py index 10098e3a4..94fc107e8 100644 --- a/backend/degree/migrations/0001_initial.py +++ b/backend/degree/migrations/0001_initial.py @@ -11,91 +11,307 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('courses', '0061_merge_20231112_1524'), + ("courses", "0061_merge_20231112_1524"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( - name='Degree', + name="Degree", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('program', models.CharField(choices=[('EU_BSE', 'Engineering BSE'), ('EU_BAS', 'Engineering BAS'), ('AU_BA', 'College BA'), ('WU_BS', 'Wharton BS')], help_text='\nThe program code for this degree, e.g., EU_BSE\n', max_length=10)), - ('degree', models.CharField(help_text='\nThe degree code for this degree, e.g., BSE\n', max_length=4)), - ('major', models.CharField(help_text='\nThe major code for this degree, e.g., BIOL\n', max_length=4)), - ('concentration', models.CharField(help_text='\nThe concentration code for this degree, e.g., BMAT\n', max_length=4, null=True)), - ('year', models.IntegerField(help_text='\nThe effective year of this degree, e.g., 2023\n')), + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "program", + models.CharField( + choices=[ + ("EU_BSE", "Engineering BSE"), + ("EU_BAS", "Engineering BAS"), + ("AU_BA", "College BA"), + ("WU_BS", "Wharton BS"), + ], + help_text="\nThe program code for this degree, e.g., EU_BSE\n", + max_length=10, + ), + ), + ( + "degree", + models.CharField( + help_text="\nThe degree code for this degree, e.g., BSE\n", max_length=4 + ), + ), + ( + "major", + models.CharField( + help_text="\nThe major code for this degree, e.g., BIOL\n", max_length=4 + ), + ), + ( + "concentration", + models.CharField( + help_text="\nThe concentration code for this degree, e.g., BMAT\n", + max_length=4, + null=True, + ), + ), + ( + "year", + models.IntegerField( + help_text="\nThe effective year of this degree, e.g., 2023\n" + ), + ), ], ), migrations.CreateModel( - name='DegreePlan', + name="DegreePlan", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(help_text="The user's nickname for the degree plan.", max_length=255)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('degrees', models.ManyToManyField(help_text='The degrees this degree plan is associated with.', to='degree.Degree')), - ('person', models.ForeignKey(help_text='The user the degree plan belongs to.', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "name", + models.CharField( + help_text="The user's nickname for the degree plan.", max_length=255 + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "degrees", + models.ManyToManyField( + help_text="The degrees this degree plan is associated with.", + to="degree.Degree", + ), + ), + ( + "person", + models.ForeignKey( + help_text="The user the degree plan belongs to.", + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.CreateModel( - name='Rule', + name="Rule", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(blank=True, help_text='\nThe title for this rule.\n', max_length=200)), - ('num', models.PositiveSmallIntegerField(help_text='\nThe minimum number of courses or subrules required for this rule.\n', null=True)), - ('credits', models.DecimalField(decimal_places=2, 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)), - ('q', models.TextField(blank=True, help_text='\nString representing a Q() object that returns the set of courses\nsatisfying this rule. Non-empty iff this is a Rule leaf.\nThis Q object is expected to be normalized before it is serialized\nto a string.\n', max_length=1000)), - ('parent', models.ForeignKey(help_text="\nThis rule's parent Rule if it has one. Null if this is a top level rule\n(i.e., this rule belongs to some Degree's `.rules` set).\n", null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='degree.rule')), + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "title", + models.CharField( + blank=True, help_text="\nThe title for this rule.\n", max_length=200 + ), + ), + ( + "num", + models.PositiveSmallIntegerField( + help_text="\nThe minimum number of courses or subrules required for this rule.\n", + null=True, + ), + ), + ( + "credits", + models.DecimalField( + decimal_places=2, + 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, + ), + ), + ( + "q", + models.TextField( + blank=True, + help_text="\nString representing a Q() object that returns the set of courses\nsatisfying this rule. Non-empty iff this is a Rule leaf.\nThis Q object is expected to be normalized before it is serialized\nto a string.\n", + max_length=1000, + ), + ), + ( + "parent", + models.ForeignKey( + help_text="\nThis rule's parent Rule if it has one. Null if this is a top level rule\n(i.e., this rule belongs to some Degree's `.rules` set).\n", + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="children", + to="degree.rule", + ), + ), ], ), migrations.CreateModel( - name='SatisfactionStatus', + name="SatisfactionStatus", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('satisfied', models.BooleanField(default=False, help_text='Whether the rule is satisfied')), - ('last_updated', models.DateTimeField(auto_now=True)), - ('last_checked', models.DateTimeField(default=django.utils.timezone.now)), - ('degree_plan', models.ForeignKey(help_text='The degree plan that leads to the satisfaction of the rule', on_delete=django.db.models.deletion.CASCADE, related_name='satisfactions', to='degree.degreeplan')), - ('rule', models.ForeignKey(help_text='The rule that is satisfied', on_delete=django.db.models.deletion.CASCADE, related_name='+', to='degree.rule')), + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "satisfied", + models.BooleanField(default=False, help_text="Whether the rule is satisfied"), + ), + ("last_updated", models.DateTimeField(auto_now=True)), + ("last_checked", models.DateTimeField(default=django.utils.timezone.now)), + ( + "degree_plan", + models.ForeignKey( + help_text="The degree plan that leads to the satisfaction of the rule", + on_delete=django.db.models.deletion.CASCADE, + related_name="satisfactions", + to="degree.degreeplan", + ), + ), + ( + "rule", + models.ForeignKey( + help_text="The rule that is satisfied", + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="degree.rule", + ), + ), ], ), migrations.CreateModel( - name='Fulfillment', + name="Fulfillment", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('full_code', models.CharField(blank=True, db_index=True, help_text='The dash-joined department and code of the course, e.g., `CIS-120`', max_length=16)), - ('semester', models.CharField(help_text='\nThe semester of the course (of the form YYYYx where x is A [for spring],\nB [summer], or C [fall]), e.g. `2019C` for fall 2019. Null if this fulfillment\ndoes not yet have a semester.\n', max_length=5, null=True)), - ('degree_plan', models.ForeignKey(help_text='The degree plan with which this fulfillment is associated', on_delete=django.db.models.deletion.CASCADE, related_name='fulfillments', to='degree.degreeplan')), - ('historical_course', models.ForeignKey(help_text='\nThe last offering of the course with the full code, or null if\nthere is no such historical course.\n', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='courses.course')), - ('rules', models.ManyToManyField(blank=True, help_text='\nThe rules this course fulfills. Blank if this course does not apply\nto any rules.\n', related_name='_degree_fulfillment_rules_+', to='degree.Rule')), + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "full_code", + models.CharField( + blank=True, + db_index=True, + help_text="The dash-joined department and code of the course, e.g., `CIS-120`", + max_length=16, + ), + ), + ( + "semester", + models.CharField( + help_text="\nThe semester of the course (of the form YYYYx where x is A [for spring],\nB [summer], or C [fall]), e.g. `2019C` for fall 2019. Null if this fulfillment\ndoes not yet have a semester.\n", + max_length=5, + null=True, + ), + ), + ( + "degree_plan", + models.ForeignKey( + help_text="The degree plan with which this fulfillment is associated", + on_delete=django.db.models.deletion.CASCADE, + related_name="fulfillments", + to="degree.degreeplan", + ), + ), + ( + "historical_course", + models.ForeignKey( + help_text="\nThe last offering of the course with the full code, or null if\nthere is no such historical course.\n", + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="courses.course", + ), + ), + ( + "rules", + models.ManyToManyField( + blank=True, + help_text="\nThe rules this course fulfills. Blank if this course does not apply\nto any rules.\n", + related_name="_degree_fulfillment_rules_+", + to="degree.Rule", + ), + ), ], ), migrations.CreateModel( - name='DoubleCountRestriction', + name="DoubleCountRestriction", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('max_courses', models.PositiveSmallIntegerField(help_text='\nThe maximum number of courses you can count for both rules.\nIf null, there is no limit, and max_credits must not be null.\n', null=True)), - ('max_credits', models.DecimalField(decimal_places=2, help_text='\nThe maximum number of CUs you can count for both rules.\nIf null, there is no limit, and max_credits must not be null.\n', max_digits=4, null=True)), - ('other_rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='degree.rule')), - ('rule', models.ForeignKey(help_text='\nA rule in the double count restriction.\n', on_delete=django.db.models.deletion.CASCADE, related_name='+', to='degree.rule')), + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "max_courses", + models.PositiveSmallIntegerField( + help_text="\nThe maximum number of courses you can count for both rules.\nIf null, there is no limit, and max_credits must not be null.\n", + null=True, + ), + ), + ( + "max_credits", + models.DecimalField( + decimal_places=2, + help_text="\nThe maximum number of CUs you can count for both rules.\nIf null, there is no limit, and max_credits must not be null.\n", + max_digits=4, + null=True, + ), + ), + ( + "other_rule", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="degree.rule", + ), + ), + ( + "rule", + models.ForeignKey( + help_text="\nA rule in the double count restriction.\n", + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + to="degree.rule", + ), + ), ], ), migrations.AddField( - model_name='degree', - name='rules', - field=models.ManyToManyField(blank=True, help_text='\nThe rules for this degree. Blank if this degree has no rules.\n', related_name='degrees', to='degree.Rule'), + model_name="degree", + name="rules", + field=models.ManyToManyField( + blank=True, + help_text="\nThe rules for this degree. Blank if this degree has no rules.\n", + related_name="degrees", + to="degree.Rule", + ), ), migrations.AddConstraint( - model_name='satisfactionstatus', - constraint=models.UniqueConstraint(fields=('degree_plan', 'rule'), name='unique_satisfaction'), + model_name="satisfactionstatus", + constraint=models.UniqueConstraint( + fields=("degree_plan", "rule"), name="unique_satisfaction" + ), ), migrations.AddConstraint( - model_name='degreeplan', - constraint=models.UniqueConstraint(fields=('name', 'person'), name='degreeplan_name_person'), + model_name="degreeplan", + constraint=models.UniqueConstraint( + fields=("name", "person"), name="degreeplan_name_person" + ), ), migrations.AddConstraint( - model_name='degree', - constraint=models.UniqueConstraint(fields=('program', 'degree', 'major', 'concentration', 'year'), name='unique degree'), + model_name="degree", + constraint=models.UniqueConstraint( + fields=("program", "degree", "major", "concentration", "year"), name="unique degree" + ), ), ] diff --git a/backend/degree/migrations/0002_alter_degreeplan_degrees.py b/backend/degree/migrations/0002_alter_degreeplan_degrees.py index 6e7b4f918..68cbdd9a5 100644 --- a/backend/degree/migrations/0002_alter_degreeplan_degrees.py +++ b/backend/degree/migrations/0002_alter_degreeplan_degrees.py @@ -6,13 +6,17 @@ class Migration(migrations.Migration): dependencies = [ - ('degree', '0001_initial'), + ("degree", "0001_initial"), ] operations = [ migrations.AlterField( - model_name='degreeplan', - name='degrees', - field=models.ManyToManyField(blank=True, help_text='The degrees this degree plan is associated with.', to='degree.Degree'), + model_name="degreeplan", + name="degrees", + field=models.ManyToManyField( + blank=True, + help_text="The degrees this degree plan is associated with.", + to="degree.Degree", + ), ), ] diff --git a/backend/degree/migrations/0003_alter_fulfillment_unique_together.py b/backend/degree/migrations/0003_alter_fulfillment_unique_together.py index a0a26d221..11865cfff 100644 --- a/backend/degree/migrations/0003_alter_fulfillment_unique_together.py +++ b/backend/degree/migrations/0003_alter_fulfillment_unique_together.py @@ -6,12 +6,12 @@ class Migration(migrations.Migration): dependencies = [ - ('degree', '0002_alter_degreeplan_degrees'), + ("degree", "0002_alter_degreeplan_degrees"), ] operations = [ migrations.AlterUniqueTogether( - name='fulfillment', - unique_together={('degree_plan', 'full_code')}, + name="fulfillment", + unique_together={("degree_plan", "full_code")}, ), ] diff --git a/backend/degree/migrations/0004_remove_fulfillment_historical_course.py b/backend/degree/migrations/0004_remove_fulfillment_historical_course.py index efaaf59f6..5f8e0ba99 100644 --- a/backend/degree/migrations/0004_remove_fulfillment_historical_course.py +++ b/backend/degree/migrations/0004_remove_fulfillment_historical_course.py @@ -6,12 +6,12 @@ class Migration(migrations.Migration): dependencies = [ - ('degree', '0003_alter_fulfillment_unique_together'), + ("degree", "0003_alter_fulfillment_unique_together"), ] operations = [ migrations.RemoveField( - model_name='fulfillment', - name='historical_course', + model_name="fulfillment", + name="historical_course", ), ] diff --git a/backend/degree/migrations/0005_degree_credits.py b/backend/degree/migrations/0005_degree_credits.py index cd4bf7ae5..c3e78dcf4 100644 --- a/backend/degree/migrations/0005_degree_credits.py +++ b/backend/degree/migrations/0005_degree_credits.py @@ -6,14 +6,19 @@ class Migration(migrations.Migration): dependencies = [ - ('degree', '0004_remove_fulfillment_historical_course'), + ("degree", "0004_remove_fulfillment_historical_course"), ] operations = [ migrations.AddField( - model_name='degree', - name='credits', - field=models.DecimalField(decimal_places=2, default=32, help_text='\nThe minimum number of CUs required for this degree.\n', max_digits=4), + model_name="degree", + name="credits", + field=models.DecimalField( + decimal_places=2, + default=32, + help_text="\nThe minimum number of CUs required for this degree.\n", + max_digits=4, + ), preserve_default=False, ), ] diff --git a/backend/degree/migrations/0006_auto_20240229_1903.py b/backend/degree/migrations/0006_auto_20240229_1903.py index 015131bf7..0ff27adfe 100644 --- a/backend/degree/migrations/0006_auto_20240229_1903.py +++ b/backend/degree/migrations/0006_auto_20240229_1903.py @@ -9,20 +9,41 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('degree', '0005_degree_credits'), + ("degree", "0005_degree_credits"), ] operations = [ migrations.CreateModel( - name='DockedCourse', + name="DockedCourse", fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('full_code', models.CharField(blank=True, help_text='The dash-joined department and code of the course, e.g., `CIS-120`', max_length=16)), - ('person', models.ForeignKey(help_text='The user the docked course belongs to.', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "id", + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ( + "full_code", + models.CharField( + blank=True, + help_text="The dash-joined department and code of the course, e.g., `CIS-120`", + max_length=16, + ), + ), + ( + "person", + models.ForeignKey( + help_text="The user the docked course belongs to.", + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), ], ), migrations.AddConstraint( - model_name='dockedcourse', - constraint=models.UniqueConstraint(fields=('person', 'full_code'), name='unique docked course'), + model_name="dockedcourse", + constraint=models.UniqueConstraint( + fields=("person", "full_code"), name="unique docked course" + ), ), ] diff --git a/backend/degree/migrations/0007_alter_dockedcourse_full_code.py b/backend/degree/migrations/0007_alter_dockedcourse_full_code.py index a1af594fc..783c49ff6 100644 --- a/backend/degree/migrations/0007_alter_dockedcourse_full_code.py +++ b/backend/degree/migrations/0007_alter_dockedcourse_full_code.py @@ -6,13 +6,18 @@ class Migration(migrations.Migration): dependencies = [ - ('degree', '0006_auto_20240229_1903'), + ("degree", "0006_auto_20240229_1903"), ] operations = [ migrations.AlterField( - model_name='dockedcourse', - name='full_code', - field=models.CharField(blank=True, db_index=True, help_text='The dash-joined department and code of the course, e.g., `CIS-120`', max_length=16), + model_name="dockedcourse", + name="full_code", + field=models.CharField( + blank=True, + db_index=True, + help_text="The dash-joined department and code of the course, e.g., `CIS-120`", + max_length=16, + ), ), ] diff --git a/backend/degree/serializers.py b/backend/degree/serializers.py index cbffca7ca..4472170bf 100644 --- a/backend/degree/serializers.py +++ b/backend/degree/serializers.py @@ -5,14 +5,23 @@ from courses.models import Course from courses.serializers import CourseListSerializer, CourseDetailSerializer -from degree.models import Degree, DegreePlan, DoubleCountRestriction, Fulfillment, Rule, DockedCourse +from degree.models import ( + Degree, + DegreePlan, + DoubleCountRestriction, + Fulfillment, + Rule, + DockedCourse, +) from courses.util import get_current_semester + class DegreeListSerializer(serializers.ModelSerializer): class Meta: model = Degree fields = "__all__" + class SimpleCourseSerializer(serializers.ModelSerializer): id = serializers.ReadOnlyField( source="full_code", @@ -24,19 +33,19 @@ class SimpleCourseSerializer(serializers.ModelSerializer): ) course_quality = serializers.DecimalField( - max_digits=4, decimal_places=3, read_only=True, help_text='course_quality_help' + max_digits=4, decimal_places=3, read_only=True, help_text="course_quality_help" ) difficulty = serializers.DecimalField( - max_digits=4, decimal_places=3, read_only=True, help_text='difficulty_help' + max_digits=4, decimal_places=3, read_only=True, help_text="difficulty_help" ) instructor_quality = serializers.DecimalField( max_digits=4, decimal_places=3, read_only=True, - help_text='instructor_quality_help', + help_text="instructor_quality_help", ) work_required = serializers.DecimalField( - max_digits=4, decimal_places=3, read_only=True, help_text='work_required_help' + max_digits=4, decimal_places=3, read_only=True, help_text="work_required_help" ) class Meta: @@ -53,13 +62,14 @@ class Meta: ] read_only_fields = fields + class RuleSerializer(serializers.ModelSerializer): q_json = serializers.ReadOnlyField(help_text="JSON representation of the q object") class Meta: model = Rule fields = "__all__" - + def to_representation(self, instance): data = super(RuleSerializer, self).to_representation(instance) data.q = "" @@ -89,14 +99,23 @@ class Meta: class FulfillmentSerializer(serializers.ModelSerializer): course = serializers.SerializerMethodField() + def get_course(self, obj): - course = Course.with_reviews.filter(full_code=obj.full_code, semester__lte=get_current_semester()).order_by("-semester").first() + course = ( + Course.with_reviews.filter( + full_code=obj.full_code, semester__lte=get_current_semester() + ) + .order_by("-semester") + .first() + ) if course is not None: return SimpleCourseSerializer(course).data return None - + # TODO: add a get_queryset method to only allow rules from the degree plan - rules = serializers.PrimaryKeyRelatedField(many=True, queryset=Rule.objects.all(), required=False) + rules = serializers.PrimaryKeyRelatedField( + many=True, queryset=Rule.objects.all(), required=False + ) def to_internal_value(self, data): data = data.copy() @@ -149,7 +168,7 @@ class DegreePlanListSerializer(serializers.ModelSerializer): # degree_ids = serializers.PrimaryKeyRelatedField( # many=True, - # required=False, + # required=False, # source="degrees", # queryset=Degree.objects.all(), # help_text="The degree_id this degree plan belongs to.", @@ -167,8 +186,7 @@ class DegreePlanDetailSerializer(serializers.ModelSerializer): # help_text="The courses used to fulfill degree plan.", # ) degrees = DegreeDetailSerializer( - many=True, - help_text="The degrees belonging to this degree plan" + many=True, help_text="The degrees belonging to this degree plan" ) person = serializers.HiddenField(default=serializers.CurrentUserDefault()) @@ -184,4 +202,4 @@ class DockedCourseSerializer(serializers.ModelSerializer): class Meta: model = DockedCourse - fields = "__all__" \ No newline at end of file + fields = "__all__" diff --git a/backend/degree/urls.py b/backend/degree/urls.py index c09e4c3f9..3169af441 100644 --- a/backend/degree/urls.py +++ b/backend/degree/urls.py @@ -2,14 +2,22 @@ from rest_framework.routers import DefaultRouter from rest_framework_nested.routers import NestedDefaultRouter -from degree.views import DegreePlanViewset, DegreeViewset, FulfillmentViewSet, courses_for_rule, DockedCourseViewset +from degree.views import ( + DegreePlanViewset, + DegreeViewset, + FulfillmentViewSet, + courses_for_rule, + DockedCourseViewset, +) router = DefaultRouter(trailing_slash=False) router.register(r"degreeplans", DegreePlanViewset, basename="degreeplan") router.register(r"degrees", DegreeViewset, basename="degree") router.register(r"docked", DockedCourseViewset) -fulfillments_router = NestedDefaultRouter(router, r"degreeplans", lookup="degreeplan", trailing_slash=False) +fulfillments_router = NestedDefaultRouter( + router, r"degreeplans", lookup="degreeplan", trailing_slash=False +) fulfillments_router.register(r"fulfillments", FulfillmentViewSet, basename="degreeplan-fulfillment") urlpatterns = [ diff --git a/backend/degree/utils/model_utils.py b/backend/degree/utils/model_utils.py index 6e0af177b..189ad4cb5 100644 --- a/backend/degree/utils/model_utils.py +++ b/backend/degree/utils/model_utils.py @@ -68,6 +68,7 @@ def q(self, n): (clause,) = n return clause + class JSONTransformer(Transformer): """ This class transforms the Abstract Syntax Tree (AST) generated by the parser @@ -83,36 +84,20 @@ def array(self, n): def and_clause(self, clauses): if len(clauses) == 1: return clauses[0] - return { - 'type': 'AND', - 'clauses': clauses - } + return {"type": "AND", "clauses": clauses} def or_clause(self, clauses): - return { - 'type': 'OR', - 'clauses': clauses - } + return {"type": "OR", "clauses": clauses} def not_clause(self, clauses): - return { - 'type': 'NOT', - 'clauses': clauses - } + return {"type": "NOT", "clauses": clauses} def condition(self, n): key, value = n - if key == 'full_code': - return { - 'type': 'COURSE', - 'full_code': value - } - else: - return { - 'type': 'LEAF', - 'key': key, - 'value': value - } + if key == "full_code": + return {"type": "COURSE", "full_code": value} + else: + return {"type": "LEAF", "key": key, "value": value} def string(self, s): (s,) = s @@ -183,4 +168,4 @@ def q(self, n): start="q", transformer=JSONTransformer(), parser="lalr", -) \ No newline at end of file +) diff --git a/backend/degree/utils/parse_degreeworks.py b/backend/degree/utils/parse_degreeworks.py index 88835a371..8c9c7d16c 100644 --- a/backend/degree/utils/parse_degreeworks.py +++ b/backend/degree/utils/parse_degreeworks.py @@ -300,7 +300,7 @@ def parse_degreeworks(json: dict, degree: Degree) -> list[Rule] | None: # check if this requirement actually has anything in it if degree_req == rules[-1] and not degree_req.q: rules.pop() - + # special case for Additional majors if degree.credits is None: logging.error("Skipped degree because it has not total credits requirement.") diff --git a/backend/degree/views.py b/backend/degree/views.py index e0223c289..b5af8d024 100644 --- a/backend/degree/views.py +++ b/backend/degree/views.py @@ -18,7 +18,7 @@ DegreePlanDetailSerializer, DegreePlanListSerializer, FulfillmentSerializer, - DockedCourseSerializer + DockedCourseSerializer, ) @@ -34,18 +34,19 @@ class DegreeViewset(viewsets.ReadOnlyModelViewSet): def get_queryset(self): queryset = Degree.objects.all() - degree_id = self.request.query_params.get('id', None) + degree_id = self.request.query_params.get("id", None) if degree_id is not None: queryset = queryset.filter(id=degree_id) return queryset - + def get_serializer_class(self): if self.action == "list": - if self.request.query_params.get('id', None) is not None: + if self.request.query_params.get("id", None) is not None: return DegreeDetailSerializer return DegreeListSerializer return DegreeDetailSerializer + class DegreePlanViewset(AutoPrefetchViewSetMixin, viewsets.ModelViewSet): """ List, retrieve, create, destroy, and update a DegreePlan. @@ -72,7 +73,7 @@ def get_serializer_context(self): context = super().get_serializer_context() context.update({"request": self.request}) # used to get the user return context - + def retrieve(self, request, *args, **kwargs): degree_plan = self.get_object() serializer = self.get_serializer(degree_plan) @@ -80,32 +81,31 @@ def retrieve(self, request, *args, **kwargs): def create(self, request, *args, **kwargs): if request.data.get("name") is None: - raise ValidationError({ "name": "This field is required." }) + raise ValidationError({"name": "This field is required."}) new_degree_plan = DegreePlan(name=request.data.get("name"), person=self.request.user) new_degree_plan.save() serializer = self.get_serializer(new_degree_plan) return Response(serializer.data, status=status.HTTP_201_CREATED) - @action(detail=True, methods=["post"]) def copy(self, request, pk=None): """ Copy a degree plan. """ if request.data.get("name") is None: - raise ValidationError({ "name": "This field is required." }) + raise ValidationError({"name": "This field is required."}) degree_plan = self.get_object() new_degree_plan = degree_plan.copy(request.data["name"]) serializer = self.get_serializer(new_degree_plan) return Response(serializer.data, status=status.HTTP_201_CREATED) - + @action(detail=True, methods=["post", "delete"]) def degrees(self, request, pk=None): degree_ids = request.data.get("degree_ids") if not isinstance(degree_ids, list): raise ValidationError({"degree_ids": "This field must be a list."}) if degree_ids is None: - raise ValidationError({ "degree_ids": "This field is required." }) + raise ValidationError({"degree_ids": "This field is required."}) degree_plan = self.get_object() try: @@ -151,16 +151,17 @@ def get_queryset(self): degree_plan_id=self.get_degree_plan_id(), ) return queryset - + def create(self, request, *args, **kwargs): if request.data.get("full_code") is None: - raise ValidationError({ "full_code": "This field is required." }) + raise ValidationError({"full_code": "This field is required."}) self.kwargs["full_code"] = request.data["full_code"] try: return self.partial_update(request, *args, **kwargs) except Http404: return super().create(request, *args, **kwargs) - + + @api_view(["GET"]) def courses_for_rule(request, rule_id: int): """ @@ -172,6 +173,7 @@ class DockedCourseViewset(viewsets.ModelViewSet): """ List, retrieve, create, destroy, and update docked courses """ + permission_classes = [IsAuthenticated] serializer_class = DockedCourseSerializer # http_method_names = ["get", "post", "head", "delete"] @@ -186,32 +188,32 @@ def get_serializer_context(self): context = super().get_serializer_context() context.update({"request": self.request}) # used to get the user return context - + # def retrieve(self, request, *args, **kwargs): # dockedCourse = self.get_object() # serializer = self.get_serializer(dockedCourse) # return Response(serializer.data, status=status.HTTP_200_OK) - + def create(self, request, *args, **kwargs): if request.data.get("full_code") is None: - raise ValidationError({ "full_code": "This field is required." }) + raise ValidationError({"full_code": "This field is required."}) self.kwargs["full_code"] = request.data["full_code"] self.kwargs["person"] = self.request.user try: return self.partial_update(request, *args, **kwargs) except Http404: return super().create(request, *args, **kwargs) - + def destroy(self, request, *args, **kwargs): if kwargs["full_code"] is None: - raise ValidationError({ "full_code": "This field is required." }) + raise ValidationError({"full_code": "This field is required."}) instances_to_delete = self.get_queryset().filter(full_code=kwargs["full_code"]) - + if not instances_to_delete.exists(): raise Http404("No instances matching the provided full_code were found.") for instance in instances_to_delete: self.perform_destroy(instance) - + return Response(status.HTTP_200_OK)