diff --git a/backend/degree/admin.py b/backend/degree/admin.py index 824d2b0f0..d8bad6539 100644 --- a/backend/degree/admin.py +++ b/backend/degree/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from degree.models import Degree, Rule, DegreePlan, SatisfactionStatus, DoubleCountRestriction +from degree.models import Degree, DegreePlan, DoubleCountRestriction, Rule, SatisfactionStatus # Register your models here. diff --git a/backend/degree/models.py b/backend/degree/models.py index 50c44bb91..6765eba50 100644 --- a/backend/degree/models.py +++ b/backend/degree/models.py @@ -8,11 +8,11 @@ from django.db import models from django.db.models import Count, DecimalField, Q, Sum from django.db.models.functions import Coalesce +from django.db.models.signals import m2m_changed from django.utils import timezone from courses.models import Course from degree.utils.model_utils import q_object_parser -from django.db.models.signals import m2m_changed program_choices = [ @@ -203,7 +203,7 @@ def evaluate(self, full_codes: Iterable[str]) -> bool: if self.num is not None and count < self.num: return False return True - + def get_q_object(self) -> Q | None: if not self.q: return None @@ -316,7 +316,8 @@ class Fulfillment(models.Model): related_name="+", help_text=dedent( """ - The last offering of the course with the full code, or null if there is no such historical course. + The last offering of the course with the full code, or null if + there is no such historical course. """ ), ) @@ -363,11 +364,12 @@ def save(self, *args, **kwargs): ) if course is not None: course = course.topic.most_recent - + self.historical_course = course super().save(*args, **kwargs) + def update_satisfaction_statuses(sender, instance, action, pk_set, **kwargs): """ This function updates the SatisfactionStatuses associated with a DegreePlan when the rules @@ -375,16 +377,18 @@ def update_satisfaction_statuses(sender, instance, action, pk_set, **kwargs): """ if action == "pre_clear" or action == "pre_remove": instance.degree_plan.satisfactions.filter(rule__in=pk_set).delete() - return + return if action == "post_add" or action == "post_remove" or action == "post_clear": degree_plan = instance.degree_plan for rule in degree_plan.degree.rules.all(): - status, _ = SatisfactionStatus.objects.get_or_create( - degree_plan=degree_plan, rule=rule + status, _ = SatisfactionStatus.objects.get_or_create(degree_plan=degree_plan, rule=rule) + status.satisfied = rule.evaluate( + [fulfillment.full_code for fulfillment in degree_plan.fulfillments.all()] ) - status.satisfied = rule.evaluate([fulfillment.full_code for fulfillment in degree_plan.fulfillments.all()]) status.save() + + m2m_changed.connect(update_satisfaction_statuses, sender=Fulfillment.rules.through) diff --git a/backend/degree/serializers.py b/backend/degree/serializers.py index 09b8bcbd9..a785616bd 100644 --- a/backend/degree/serializers.py +++ b/backend/degree/serializers.py @@ -1,22 +1,25 @@ from textwrap import dedent +from django.db.models import Q from rest_framework import serializers -from rest_framework.fields import empty from courses.models import Course from courses.serializers import CourseListSerializer from degree.models import Degree, DegreePlan, DoubleCountRestriction, Fulfillment, Rule -from django.db.models import Q + class DegreeListSerializer(serializers.ModelSerializer): class Meta: model = Degree fields = "__all__" + class RuleSerializer(serializers.ModelSerializer): class Meta: model = Rule fields = "__all__" + + # Allow recursive serialization of rules RuleSerializer._declared_fields["rules"] = RuleSerializer( many=True, read_only=True, source="children" @@ -39,6 +42,7 @@ class Meta: fields = "__all__" read_only_fields = "__all__" + class FulfillmentSerializer(serializers.ModelSerializer): course = CourseListSerializer( read_only=True, @@ -48,7 +52,7 @@ class FulfillmentSerializer(serializers.ModelSerializer): The details of the fulfilling course. This is the most recent course with the full code, or null if no course exists with the full code. """ - ) + ), ) # TODO: add a get_queryset method to only allow rules from the degree plan rules = serializers.PrimaryKeyRelatedField(many=True, queryset=Rule.objects.all()) @@ -64,12 +68,12 @@ class Meta: def validate(self, data): data = super().validate(data) - rules = data.get("rules") # for patch requests without a rules field + rules = data.get("rules") # for patch requests without a rules field full_code = data.get("full_code") degree_plan = data.get("degree_plan") if rules is None and full_code is None: - return data # Nothing to validate + return data # Nothing to validate if rules is None: rules = self.instance.rules.all() if full_code is None: @@ -83,7 +87,7 @@ def validate(self, data): raise serializers.ValidationError( f"Course {full_code} does not satisfy rule {rule.id}" ) - + # Check for double count restrictions double_count_restrictions = DoubleCountRestriction.objects.filter( Q(rule__in=rules) | Q(other_rule__in=rules) @@ -93,7 +97,7 @@ def validate(self, data): raise serializers.ValidationError( f"Double count restriction {restriction.id} violated" ) - + return data diff --git a/backend/degree/urls.py b/backend/degree/urls.py index f5c7f51cd..16c3825f7 100644 --- a/backend/degree/urls.py +++ b/backend/degree/urls.py @@ -1,6 +1,7 @@ from django.urls import include, path from rest_framework.routers import DefaultRouter from rest_framework_nested.routers import NestedDefaultRouter + from degree.views import ( DegreeDetail, DegreeList, @@ -9,10 +10,11 @@ courses_for_rule, ) + router = DefaultRouter() router.register(r"degreeplans", DegreePlanViewset, basename="degreeplan") -fulfillments_router = NestedDefaultRouter(router, r'degreeplans', lookup='degreeplan') -fulfillments_router.register(r'fulfillments', FulfillmentViewSet, basename='degreeplan-fulfillment') +fulfillments_router = NestedDefaultRouter(router, r"degreeplans", lookup="degreeplan") +fulfillments_router.register(r"fulfillments", FulfillmentViewSet, basename="degreeplan-fulfillment") urlpatterns = [ path("degrees/", DegreeList.as_view(), name="degree-list"), diff --git a/backend/degree/views.py b/backend/degree/views.py index 1dd767da7..ade665c94 100644 --- a/backend/degree/views.py +++ b/backend/degree/views.py @@ -1,10 +1,10 @@ from django.core.exceptions import ObjectDoesNotExist from django_auto_prefetching import AutoPrefetchViewSetMixin -from rest_framework import generics, mixins, status, viewsets +from rest_framework import generics, status, viewsets from rest_framework.decorators import api_view +from rest_framework.exceptions import ValidationError from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from rest_framework.exceptions import ValidationError from courses.models import Course from courses.serializers import CourseListSerializer diff --git a/backend/tests/degree/test_api.py b/backend/tests/degree/test_api.py index bc55fca2c..58afb6d85 100644 --- a/backend/tests/degree/test_api.py +++ b/backend/tests/degree/test_api.py @@ -1,17 +1,24 @@ +from django.db.models import Q from django.test import TestCase +from rest_framework.reverse import reverse from rest_framework.test import APIClient + +from courses.models import User from courses.serializers import CourseListSerializer from courses.util import get_or_create_course_and_section -from degree.models import Degree, Rule, DegreePlan, Fulfillment, DoubleCountRestriction, SatisfactionStatus -from courses.models import User -from django.urls import reverse -from django.db.models import Q -from rest_framework.reverse import reverse -import json +from degree.models import ( + Degree, + DegreePlan, + DoubleCountRestriction, + Fulfillment, + Rule, + SatisfactionStatus, +) TEST_SEMESTER = "2023C" + class DegreeViewsetTest(TestCase): def test_list_degrees(self): pass @@ -42,11 +49,14 @@ def test_delete_user_degree_plan(self): def test_delete_user_degree_plan_with_fulfillments(self): pass + class FulfillmentViewsetTest(TestCase): def assertSerializedFulfillmentEquals(self, fulfillment: dict, expected: Fulfillment): self.assertEqual(len(fulfillment), 6) self.assertEqual(fulfillment["id"], expected.id) - self.assertEqual(fulfillment["course"], CourseListSerializer(expected.historical_course).data) + self.assertEqual( + fulfillment["course"], CourseListSerializer(expected.historical_course).data + ) self.assertEqual(fulfillment["rules"], [rule.id for rule in expected.rules.all()]) self.assertEqual(fulfillment["semester"], expected.semester) self.assertEqual(fulfillment["degree_plan"], expected.degree_plan.id) @@ -66,9 +76,7 @@ def setUp(self): "CIS-1920-001", TEST_SEMESTER, course_defaults={"credits": 1} ) - self.degree = Degree.objects.create( - program="EU_BSE", degree="BSE", major="CIS", year=2023 - ) + self.degree = Degree.objects.create(program="EU_BSE", degree="BSE", major="CIS", year=2023) self.parent_rule = Rule.objects.create(degree=self.degree) self.rule1 = Rule.objects.create( degree=self.degree, @@ -91,8 +99,8 @@ def setUp(self): ) self.double_count_restriction = DoubleCountRestriction.objects.create( - rule=self.rule2, # CIS-19XX - other_rule=self.rule3, # CIS-XXXX + rule=self.rule2, # CIS-19XX + other_rule=self.rule3, # CIS-XXXX max_credits=1, ) @@ -107,7 +115,7 @@ def setUp(self): self.bad_degree_plan = DegreePlan.objects.create( name="Bad Degree Plan", person=self.user, - degree=self.other_degree, # empty degree + degree=self.other_degree, # empty degree ) self.client = APIClient() self.client.force_login(self.user) @@ -115,10 +123,12 @@ def setUp(self): def test_create_fulfillment(self): response = self.client.post( reverse("degreeplan-fulfillment-list", kwargs={"degreeplan_pk": self.degree_plan.id}), - {"full_code": "CIS-1200", "semester": TEST_SEMESTER, "rules": [self.rule1.id]} + {"full_code": "CIS-1200", "semester": TEST_SEMESTER, "rules": [self.rule1.id]}, ) self.assertEqual(response.status_code, 201, response.json()) - self.assertSerializedFulfillmentEquals(response.data, Fulfillment.objects.get(full_code="CIS-1200")) + self.assertSerializedFulfillmentEquals( + response.data, Fulfillment.objects.get(full_code="CIS-1200") + ) fulfillment = Fulfillment.objects.get(full_code="CIS-1200") self.assertEqual(fulfillment.degree_plan, self.degree_plan) @@ -146,9 +156,13 @@ def test_list_fulfillment(self): b.save() b.rules.add(self.rule2) - response = self.client.get(reverse("degreeplan-fulfillment-list", kwargs={"degreeplan_pk": self.degree_plan.id})) + response = self.client.get( + reverse("degreeplan-fulfillment-list", kwargs={"degreeplan_pk": self.degree_plan.id}) + ) self.assertEqual(response.status_code, 200, response.json()) - response_a, response_b = sorted((dict(d) for d in response.data), key=lambda d: d["full_code"]) + response_a, response_b = sorted( + (dict(d) for d in response.data), key=lambda d: d["full_code"] + ) self.assertSerializedFulfillmentEquals(response_a, a) self.assertSerializedFulfillmentEquals(response_b, b) @@ -162,11 +176,15 @@ def test_retrieve_fulfillment(self): a.save() a.rules.add(self.rule1) - response = self.client.get(reverse("degreeplan-fulfillment-detail", kwargs={"degreeplan_pk": self.degree_plan.id, "pk": a.id})) + response = self.client.get( + reverse( + "degreeplan-fulfillment-detail", + kwargs={"degreeplan_pk": self.degree_plan.id, "pk": a.id}, + ) + ) self.assertEqual(response.status_code, 200, response.json()) self.assertSerializedFulfillmentEquals(response.data, a) - def test_update_fulfillment_replace_rule(self): a = Fulfillment( degree_plan=self.degree_plan, @@ -177,15 +195,18 @@ def test_update_fulfillment_replace_rule(self): a.rules.add(self.rule1) response = self.client.patch( - reverse("degreeplan-fulfillment-detail", kwargs={"degreeplan_pk": self.degree_plan.id, "pk": a.id}), - {"rules": [self.rule3.id]} + reverse( + "degreeplan-fulfillment-detail", + kwargs={"degreeplan_pk": self.degree_plan.id, "pk": a.id}, + ), + {"rules": [self.rule3.id]}, ) self.assertEqual(response.status_code, 200, response.json()) self.assertSerializedFulfillmentEquals(response.data, a) a.refresh_from_db() self.assertEqual(a.rules.count(), 1) self.assertEqual(a.rules.first(), self.rule3) - + def test_update_semester(self): a = Fulfillment( degree_plan=self.degree_plan, @@ -196,14 +217,17 @@ def test_update_semester(self): a.rules.add(self.rule1) response = self.client.patch( - reverse("degreeplan-fulfillment-detail", kwargs={"degreeplan_pk": self.degree_plan.id, "pk": a.id}), - {"semester": "2022B"} + reverse( + "degreeplan-fulfillment-detail", + kwargs={"degreeplan_pk": self.degree_plan.id, "pk": a.id}, + ), + {"semester": "2022B"}, ) self.assertEqual(response.status_code, 200, response.json()) a.refresh_from_db() self.assertSerializedFulfillmentEquals(response.data, a) self.assertEqual(a.semester, "2022B") - + def test_update_fulfillment_full_code(self): a = Fulfillment( degree_plan=self.degree_plan, @@ -214,14 +238,17 @@ def test_update_fulfillment_full_code(self): a.rules.add(self.rule3) response = self.client.patch( - reverse("degreeplan-fulfillment-detail", kwargs={"degreeplan_pk": self.degree_plan.id, "pk": a.id}), - {"full_code": "CIS-1910"} + reverse( + "degreeplan-fulfillment-detail", + kwargs={"degreeplan_pk": self.degree_plan.id, "pk": a.id}, + ), + {"full_code": "CIS-1910"}, ) self.assertEqual(response.status_code, 200, response.json()) a.refresh_from_db() self.assertSerializedFulfillmentEquals(response.data, a) self.assertEqual(a.full_code, "CIS-1910") - + def test_update_fulfillment_rule(self): a = Fulfillment( degree_plan=self.degree_plan, @@ -232,8 +259,11 @@ def test_update_fulfillment_rule(self): a.rules.add(self.rule1) response = self.client.patch( - reverse("degreeplan-fulfillment-detail", kwargs={"degreeplan_pk": self.degree_plan.id, "pk": a.id}), - {"rules": [self.rule3.id, self.rule1.id]} + reverse( + "degreeplan-fulfillment-detail", + kwargs={"degreeplan_pk": self.degree_plan.id, "pk": a.id}, + ), + {"rules": [self.rule3.id, self.rule1.id]}, ) self.assertEqual(response.status_code, 200, response.json()) a.refresh_from_db() @@ -251,15 +281,18 @@ def test_update_fulfillment_add_violated_rule(self): a.rules.add(self.rule1) response = self.client.patch( - reverse("degreeplan-fulfillment-detail", kwargs={"degreeplan_pk": self.degree_plan.id, "pk": a.id}), - {"rules": [self.rule2.id, self.rule1.id]} + reverse( + "degreeplan-fulfillment-detail", + kwargs={"degreeplan_pk": self.degree_plan.id, "pk": a.id}, + ), + {"rules": [self.rule2.id, self.rule1.id]}, ) self.assertEqual(response.status_code, 400, response.json()) self.assertEqual( - response.data["non_field_errors"][0], - f"Course CIS-1200 does not satisfy rule {self.rule2.id}" + response.data["non_field_errors"][0], + f"Course CIS-1200 does not satisfy rule {self.rule2.id}", ) - + def test_update_fulfillment_full_code_violates_rule(self): a = Fulfillment( degree_plan=self.degree_plan, @@ -270,13 +303,16 @@ def test_update_fulfillment_full_code_violates_rule(self): a.rules.add(self.rule1) response = self.client.patch( - reverse("degreeplan-fulfillment-detail", kwargs={"degreeplan_pk": self.degree_plan.id, "pk": a.id}), - {"full_code": "CIS-1910"} + reverse( + "degreeplan-fulfillment-detail", + kwargs={"degreeplan_pk": self.degree_plan.id, "pk": a.id}, + ), + {"full_code": "CIS-1910"}, ) self.assertEqual(response.status_code, 400, response.json()) self.assertEqual( - response.data["non_field_errors"][0], - f"Course CIS-1910 does not satisfy rule {self.rule1.id}" + response.data["non_field_errors"][0], + f"Course CIS-1910 does not satisfy rule {self.rule1.id}", ) def test_delete_fulfillment(self): @@ -288,10 +324,13 @@ def test_delete_fulfillment(self): a.save() a.rules.add(self.rule1) response = self.client.delete( - reverse("degreeplan-fulfillment-detail", kwargs={"degreeplan_pk": self.degree_plan.id, "pk": a.id}) + reverse( + "degreeplan-fulfillment-detail", + kwargs={"degreeplan_pk": self.degree_plan.id, "pk": a.id}, + ) ) self.assertEqual(response.status_code, 204) - + def test_create_fulfillment_with_wrong_users_degreeplan(self): pass diff --git a/backend/tests/degree/test_degreeworks_parser.py b/backend/tests/degree/test_degreeworks_parser.py index 3a46a094c..00f900cd6 100644 --- a/backend/tests/degree/test_degreeworks_parser.py +++ b/backend/tests/degree/test_degreeworks_parser.py @@ -6,7 +6,7 @@ class ParserEvaluationTest(TestCase): def test_comparators(self): - json = {} + pass def test_and(self): pass diff --git a/backend/tests/degree/test_models.py b/backend/tests/degree/test_models.py index 7a21e89c7..b7e520350 100644 --- a/backend/tests/degree/test_models.py +++ b/backend/tests/degree/test_models.py @@ -1,17 +1,15 @@ -from rest_framework import serializers from django.db.models import Q from django.test import TestCase from lark.exceptions import LarkError -from courses.models import User from courses.util import get_or_create_course_and_section -from degree.models import Degree, DegreePlan, DoubleCountRestriction, Rule, Fulfillment +from degree.models import Degree, Rule from degree.utils.model_utils import q_object_parser -from django.db import IntegrityError TEST_SEMESTER = "2023C" + class QObjectParserTest(TestCase): def assertParsedEqual(self, q: Q): self.assertEqual(q_object_parser.parse(repr(q)), q) @@ -102,13 +100,9 @@ def setUp(self): "CIS-1910-001", TEST_SEMESTER, course_defaults={"credits": 0.5} ) - self.degree = Degree.objects.create( - program="EU_BSE", degree="BSE", major="CIS", year=2023 - ) + self.degree = Degree.objects.create(program="EU_BSE", degree="BSE", major="CIS", year=2023) - self.root_rule = Rule.objects.create( - degree=self.degree - ) + self.root_rule = Rule.objects.create(degree=self.degree) self.rule1 = Rule.objects.create( degree=self.degree, parent=self.root_rule, @@ -181,263 +175,7 @@ def test_parent_rule_unsatisfied(self): self.assertFalse(self.root_rule.evaluate([self.cis_1200.full_code])) def test_parent_rule_satisfied(self): - self.assertTrue( - self.root_rule.evaluate([self.cis_1200.full_code, self.cis_1910.full_code]) - ) - -class FulfillmentTest(TestCase): - """ - Test cases for the Fulfillment model. - - Most tests are related to the overriden `.save()` method, which relies on - logic from the DoubleCountRestriction and Rule models; that logic is tested - in the test cases for those respective rules. - """ - - def setUp(self): - self.user = User.objects.create_user( - username="test", password="top_secret", email="test@example.com" - ) - self.cis_1200, self.cis_1200_001, _, _ = get_or_create_course_and_section( - "CIS-1200-001", TEST_SEMESTER, course_defaults={"credits": 1} - ) - self.cis_1910, self.cis_1910_001, _, _ = get_or_create_course_and_section( - "CIS-1910-001", TEST_SEMESTER, course_defaults={"credits": 0.5} - ) - self.cis_1930, self.cis_1930_001, _, _ = get_or_create_course_and_section( - "CIS-1920-001", TEST_SEMESTER, course_defaults={"credits": 1} - ) - - self.degree = Degree.objects.create( - program="EU_BSE", degree="BSE", major="CIS", year=2023 - ) - self.parent_rule = Rule.objects.create(degree=self.degree) - self.rule1 = Rule.objects.create( - degree=self.degree, - parent=self.parent_rule, - q=repr(Q(full_code="CIS-1200")), - num=1, - ) - self.rule2 = Rule.objects.create( # .5 cus / 1 course CIS-19XX classes - degree=self.degree, - parent=self.parent_rule, - q=repr(Q(full_code__startswith="CIS-19")), - credits=0.5, - num=1, - ) - self.rule3 = Rule.objects.create( # 2 CIS classes - degree=self.degree, - parent=None, - q=repr(Q(full_code__startswith="CIS")), - num=2, - ) - - self.double_count_restriction = DoubleCountRestriction.objects.create( - rule=self.rule2, # CIS-19XX - other_rule=self.rule3, # CIS-XXXX - max_credits=1, - ) - self.degree_plan = DegreePlan.objects.create( - name="Good Degree Plan", - person=self.user, - degree=self.degree, - ) - self.other_degree = Degree.objects.create( - program="EU_BSE", degree="BSE", major="CMPE", year=2023 - ) - self.bad_degree_plan = DegreePlan.objects.create( - name="Bad Degree Plan", - person=self.user, - degree=self.other_degree, # empty degree - ) - - # TODO: this test feels mildly useless... - def test_creation(self): - fulfillment = Fulfillment( - degree_plan=self.degree_plan, - full_code=self.cis_1200.full_code, - semester=TEST_SEMESTER, - ) - fulfillment.save() - fulfillment.rules.add(self.rule1, self.rule3) - - def test_creation_without_semester(self): - fulfillment = Fulfillment( - degree_plan=self.degree_plan, - full_code=self.cis_1200.full_code, - rules=[self.rule1] - ) - fulfillment.save() - - def test_duplicate_full_code(self): - fulfillment = Fulfillment( - degree_plan=self.degree_plan, - full_code=self.cis_1200.full_code, - semester=TEST_SEMESTER, - rules=[self.rule1] - ) - fulfillment.save() - - other_fulfillment = Fulfillment( - degree_plan=self.degree_plan, - full_code=self.cis_1200.full_code, - semester=TEST_SEMESTER, - rules=[self.rule3] - ) - - try: - other_fulfillment.save() - self.fail("No exception raised.") - except IntegrityError: - pass - - def test_fulfill_rule_of_wrong_degree(self): - fulfillment = Fulfillment( - degree_plan=self.bad_degree_plan, # has no rules - full_code=self.cis_1200.full_code, - semester=TEST_SEMESTER, - rules=[self.rule1] - ) - try: - fulfillment.save() - self.fail("No exception raised.") - except serializers.ValidationError: - pass # OK - - - def test_fulfill_nested_rule_of_wrong_degree(self): - fulfillment = Fulfillment( - degree_plan=self.bad_degree_plan, # has no rules - full_code=self.cis_1910_001.full_code, - semester=TEST_SEMESTER, - rules=[self.parent_rule] - ) - try: - fulfillment.save() - self.fail("No exception raised.") - except serializers.ValidationError: - pass # OK - - def test_double_count_violation(self): - fulfillment = Fulfillment( - degree_plan=self.degree_plan, - full_code=self.cis_1910_001.full_code, - semester=TEST_SEMESTER, - rules=[self.rule2, self.rule3] - ) - fulfillment.save() - - other_fulfillment = Fulfillment( - degree_plan=self.degree_plan, - full_code=self.cis_1920_001.full_code, - semester=TEST_SEMESTER, - rules=[self.rule2, self.rule3] - ) - try: - other_fulfillment.save() - self.fail("No exception raised.") - except DoubleCountException as e: - self.assertEquals(e.detail, [self.double_count_restriction.id]) - pass # OK - - def test_multiple_double_count_violations(self): - self.fail("unimplemented") - - def test_rule_violation_on_credits(self): # rule_violation - fulfillment = Fulfillment( - degree_plan=self.degree_plan, - full_code=self.cis_1930.full_code, # 1 CU course - semester=TEST_SEMESTER, - rules=[self.rule2] # 0.5 CU rule - ) - try: - fulfillment.save() - self.fail("No exception raised.") - except RuleViolationException as e: - self.assertEquals(e.detail, [self.rule2.id]) - pass - - def test_rule_violation_on_courses(self): # rule_violation - fulfillment = Fulfillment( - degree_plan=self.degree_plan, - full_code=self.cis_1930.full_code, - semester=TEST_SEMESTER, - rules=[self.rule3] - ) - fulfillment.save() - - other_fulfillment = Fulfillment( - degree_plan=self.degree_plan, - full_code=self.cis_1200.full_code, - semester=TEST_SEMESTER, - rules=[self.rule3] - ) - try: - other_fulfillment.save() - self.fail("No exception raised.") - except RuleViolationException as e: - self.assertEquals(e.detail, [self.rule3.id]) - pass - - def test_multiple_rule_violations(self): # rule_violation - fulfillment = Fulfillment( - degree_plan=self.degree_plan, - full_code=self.cis_1930.full_code, - semester=TEST_SEMESTER, - rules=[self.rule3, self.rule2] - ) - fulfillment.save() - - other_fulfillment = Fulfillment( - degree_plan=self.degree_plan, - full_code=self.cis_1200.full_code, - semester=TEST_SEMESTER, - rules=[self.rule3] - ) - try: - other_fulfillment.save() - self.fail("No exception raised.") - except RuleViolationException as e: - self.assertEquals(e.detail, [self.rule3.id, self.rule2.id]) - pass - - def test_fulfill_non_leaf_rule(self): - fulfillment = Fulfillment( - degree_plan=self.degree_plan, - full_code=self.cis_1910.full_code, - semester=TEST_SEMESTER, - rules=[self.parent_rule] - ) - - fulfillment.save() - # TODO: figure out what the right exception is here and put in try-except - self.fail("No exception raised.") - - def test_update_fulfilled_rules_causes_rule_violation(self): - fulfillment = Fulfillment( - degree_plan=self.degree_plan, - full_code=self.cis_1200.full_code, - semester=TEST_SEMESTER, - rules=[self.rule1] - ) - fulfillment.save() - try: - fulfillment.rules.add(self.rule2) # CIS-1900! - # TODO: do we need an explicit save here? - self.fail("No exception raised.") - except RuleViolationException: - pass # OK - - def test_status_updates(self): - self.fail("unimplemented") # TODO: should this be tested here? - - def test_fulfill_with_old_code(self): - # TODO: is this the responsibility of the Rule or the Fulfillment? - self.fail("unimplemented") - - def test_fulfill_with_nonexistent_code(self): - # TODO: does nonexistent code mean the fullfillment is wrong? or that the courses we have in our DB are incomplete? - self.fail("unimplemented") + self.assertTrue(self.root_rule.evaluate([self.cis_1200.full_code, self.cis_1910.full_code])) class DoubleCountRestrictionTest(TestCase): @@ -451,4 +189,4 @@ def test_credits_violation(self): pass def test_num_courses_violation(self): - pass \ No newline at end of file + pass