diff --git a/contentcuration/contentcuration/tests/viewsets/test_contentnode.py b/contentcuration/contentcuration/tests/viewsets/test_contentnode.py index ef2fb9ed4b..37d2ea4ec2 100644 --- a/contentcuration/contentcuration/tests/viewsets/test_contentnode.py +++ b/contentcuration/contentcuration/tests/viewsets/test_contentnode.py @@ -15,6 +15,7 @@ from django_concurrent_tests.helpers import make_concurrent_calls from le_utils.constants import completion_criteria from le_utils.constants import content_kinds +from le_utils.constants import exercises from le_utils.constants import roles from le_utils.constants.labels.accessibility_categories import ACCESSIBILITYCATEGORIESLIST from le_utils.constants.labels.subjects import SUBJECTSLIST @@ -366,6 +367,65 @@ def test_public_get_contentnode__unauthenticated(self): ) self.assertEqual(response.status_code, 403, response.content) + def test_consolidate_extra_fields(self): + + user = testdata.user() + channel = testdata.channel() + channel.public = True + channel.save() + contentnode = models.ContentNode.objects.create( + title="Ozer's cool contentnode", + id=uuid.uuid4().hex, + kind_id=content_kinds.EXERCISE, + description="coolest contentnode this side of the Pacific", + parent_id=channel.main_tree_id, + extra_fields={ + "m": 3, + "n": 6, + "mastery_model": exercises.M_OF_N, + } + ) + + self.client.force_authenticate(user=user) + with self.settings(TEST_ENV=False): + response = self.client.get( + self.viewset_url(pk=contentnode.id), format="json", + ) + self.assertEqual(response.status_code, 200, response.content) + print(response.data["extra_fields"]) + self.assertEqual(response.data["extra_fields"]["options"]["completion_criteria"]["threshold"]["m"], 3) + self.assertEqual(response.data["extra_fields"]["options"]["completion_criteria"]["threshold"]["n"], 6) + self.assertEqual(response.data["extra_fields"]["options"]["completion_criteria"]["threshold"]["mastery_model"], exercises.M_OF_N) + self.assertEqual(response.data["extra_fields"]["options"]["completion_criteria"]["model"], completion_criteria.MASTERY) + + def test_consolidate_extra_fields_with_mastrey_model_none(self): + + user = testdata.user() + channel = testdata.channel() + channel.public = True + channel.save() + contentnode = models.ContentNode.objects.create( + title="Aron's cool contentnode", + id=uuid.uuid4().hex, + kind_id=content_kinds.EXERCISE, + description="India is the hottest country in the world", + parent_id=channel.main_tree_id, + extra_fields={ + + "m": None, + "n": None, + "mastery_model": None, + } + ) + + self.client.force_authenticate(user=user) + with self.settings(TEST_ENV=False): + response = self.client.get( + self.viewset_url(pk=contentnode.id), format="json", + ) + self.assertEqual(response.status_code, 200, response.content) + self.assertEqual(response.data["extra_fields"], {}) + class SyncTestCase(StudioAPITestCase): @property @@ -591,32 +651,28 @@ def test_update_contentnode_extra_fields(self): user = testdata.user() contentnode = models.ContentNode.objects.create(**self.contentnode_db_metadata) - # Update extra_fields.m + # Update m and n fields m = 5 + n = 10 self.client.force_authenticate(user=user) - response = self.client.post( - self.sync_url, - [generate_update_event(contentnode.id, CONTENTNODE, {"extra_fields.m": m})], - format="json", - ) - self.assertEqual(response.status_code, 200, response.content) - self.assertEqual( - models.ContentNode.objects.get(id=contentnode.id).extra_fields["m"], m - ) - # Update extra_fields.m - n = 10 response = self.client.post( self.sync_url, - [generate_update_event(contentnode.id, CONTENTNODE, {"extra_fields.n": n})], + [generate_update_event(contentnode.id, CONTENTNODE, { + "extra_fields.options.completion_criteria.threshold.m": m, + "extra_fields.options.completion_criteria.threshold.n": n, + "extra_fields.options.completion_criteria.threshold.mastery_model": exercises.M_OF_N, + "extra_fields.options.completion_criteria.model": completion_criteria.MASTERY} + )], format="json", ) + self.assertEqual(response.status_code, 200, response.content) self.assertEqual( - models.ContentNode.objects.get(id=contentnode.id).extra_fields["m"], m + models.ContentNode.objects.get(id=contentnode.id).extra_fields["options"]["completion_criteria"]["threshold"]["m"], m ) self.assertEqual( - models.ContentNode.objects.get(id=contentnode.id).extra_fields["n"], n + models.ContentNode.objects.get(id=contentnode.id).extra_fields["options"]["completion_criteria"]["threshold"]["n"], n ) # Update extra_fields.randomize @@ -628,10 +684,10 @@ def test_update_contentnode_extra_fields(self): ) self.assertEqual(response.status_code, 200, response.content) self.assertEqual( - models.ContentNode.objects.get(id=contentnode.id).extra_fields["m"], m + models.ContentNode.objects.get(id=contentnode.id).extra_fields["options"]["completion_criteria"]["threshold"]["m"], m ) self.assertEqual( - models.ContentNode.objects.get(id=contentnode.id).extra_fields["n"], n + models.ContentNode.objects.get(id=contentnode.id).extra_fields["options"]["completion_criteria"]["threshold"]["n"], n ) self.assertEqual( models.ContentNode.objects.get(id=contentnode.id).extra_fields["randomize"], randomize @@ -641,19 +697,30 @@ def test_update_contentnode_remove_from_extra_fields(self): user = testdata.user() metadata = self.contentnode_db_metadata metadata["extra_fields"] = { - "m": 5, + "options": { + "threshold": { + "m": 5, + "n": None, + "mastery_model": exercises.M_OF_N, + }, + "model": completion_criteria.MASTERY, + } } contentnode = models.ContentNode.objects.create(**metadata) self.client.force_authenticate(user=user) - # Remove extra_fields.m + # Remove m from extra_fields response = self.client.post( self.sync_url, - [generate_update_event(contentnode.id, CONTENTNODE, {"extra_fields.m": None})], + [generate_update_event(contentnode.id, CONTENTNODE, { + "extra_fields.options.completion_criteria.threshold.m": None, + "extra_fields.options.completion_criteria.threshold.n": None, + "extra_fields.options.completion_criteria.threshold.mastery_model": exercises.M_OF_N, + "extra_fields.options.completion_criteria.model": completion_criteria.MASTERY} + )], format="json", ) self.assertEqual(response.status_code, 200, response.content) - with self.assertRaises(KeyError): - models.ContentNode.objects.get(id=contentnode.id).extra_fields["m"] + self.assertEqual(models.ContentNode.objects.get(id=contentnode.id).extra_fields["options"]["completion_criteria"]["threshold"]["m"], None) def test_update_contentnode_add_to_extra_fields_nested(self): user = testdata.user() diff --git a/contentcuration/contentcuration/utils/db_tools.py b/contentcuration/contentcuration/utils/db_tools.py index 347f98ee00..74ddeab693 100644 --- a/contentcuration/contentcuration/utils/db_tools.py +++ b/contentcuration/contentcuration/utils/db_tools.py @@ -158,6 +158,7 @@ def create_exercise(title, parent, license_id, description="", user=None, empty= "m": 3, "n": 5, } + exercise = ContentNode.objects.create( title=title, description=description, diff --git a/contentcuration/contentcuration/viewsets/contentnode.py b/contentcuration/contentcuration/viewsets/contentnode.py index c7bb2e222a..063212590d 100644 --- a/contentcuration/contentcuration/viewsets/contentnode.py +++ b/contentcuration/contentcuration/viewsets/contentnode.py @@ -19,8 +19,8 @@ from django_cte import CTEQuerySet from django_filters.rest_framework import CharFilter from django_filters.rest_framework import UUIDFilter +from le_utils.constants import completion_criteria from le_utils.constants import content_kinds -from le_utils.constants import exercises from le_utils.constants import roles from le_utils.constants.labels import accessibility_categories from le_utils.constants.labels import learning_activities @@ -35,7 +35,7 @@ from rest_framework.serializers import CharField from rest_framework.serializers import ChoiceField from rest_framework.serializers import DictField -from rest_framework.serializers import IntegerField +from rest_framework.serializers import Field from rest_framework.serializers import ValidationError from rest_framework.viewsets import ViewSet @@ -254,15 +254,14 @@ def update(self, queryset, all_validated_data): return all_objects -class ThresholdField(CharField): +class ThresholdField(Field): def to_representation(self, value): return value def to_internal_value(self, data): - data = super(ThresholdField, self).to_internal_value(data) try: data = int(data) - except ValueError: + except(ValueError, TypeError): pass return data @@ -287,12 +286,7 @@ class ExtraFieldsOptionsSerializer(JSONFieldDictSerializer): class ExtraFieldsSerializer(JSONFieldDictSerializer): - mastery_model = ChoiceField( - choices=exercises.MASTERY_MODELS, allow_null=True, required=False - ) randomize = BooleanField() - m = IntegerField(allow_null=True, required=False) - n = IntegerField(allow_null=True, required=False) options = ExtraFieldsOptionsSerializer(required=False) @@ -455,6 +449,26 @@ def get_title(item): return item["title"] if item["parent_id"] else item["original_channel_name"] +def consolidate_extra_fields(item): + extra_fields = item.get("extra_fields") + if item["kind"] == content_kinds.EXERCISE: + m = extra_fields.pop("m", None) + n = extra_fields.pop("n", None) + mastery_model = extra_fields.pop("mastery_model", None) + if not extra_fields.get("options", {}).get("completion_criteria", {}) and mastery_model is not None: + extra_fields["options"] = extra_fields.get("options", {}) + extra_fields["options"]["completion_criteria"] = { + "threshold": { + "m": m, + "n": n, + "mastery_model": mastery_model, + }, + "model": completion_criteria.MASTERY, + } + + return extra_fields + + class PrerequisitesUpdateHandler(ViewSet): """ Dummy viewset for handling create and delete changes for prerequisites @@ -683,6 +697,7 @@ class ContentNodeViewSet(BulkUpdateMixin, ChangeEventMixin, ValuesViewset): "accessibility_labels": partial(dict_if_none, field_name="accessibility_labels"), "categories": partial(dict_if_none, field_name="categories"), "learner_needs": partial(dict_if_none, field_name="learner_needs"), + "extra_fields": consolidate_extra_fields, } def _annotate_channel_id(self, queryset):