diff --git a/contentcuration/contentcuration/tests/viewsets/test_assessmentitem.py b/contentcuration/contentcuration/tests/viewsets/test_assessmentitem.py index e1ffa0f9d3..dd80e09291 100644 --- a/contentcuration/contentcuration/tests/viewsets/test_assessmentitem.py +++ b/contentcuration/contentcuration/tests/viewsets/test_assessmentitem.py @@ -1,5 +1,6 @@ from __future__ import absolute_import +import json import uuid from django.urls import reverse @@ -21,6 +22,7 @@ class SyncTestCase(SyncTestMixin, StudioAPITestCase): @property def assessmentitem_metadata(self): return { + "assessment_id": uuid.uuid4().hex, "contentnode": self.channel.main_tree.get_descendants() .filter(kind_id=content_kinds.EXERCISE) @@ -105,11 +107,14 @@ def test_create_assessmentitem_with_file_answers(self): image_file = testdata.fileobj_exercise_image() image_file.uploaded_by = self.user image_file.save() - answers = "![alt_text](${}/{}.{})".format( + answer = "![alt_text](${}/{}.{})".format( exercises.IMG_PLACEHOLDER, image_file.checksum, image_file.file_format_id ) - assessmentitem["answers"] = answers + answers = [{'answer': answer, 'correct': False, 'order': 1}] + + assessmentitem["answers"] = json.dumps(answers) + response = self.sync_changes( [ generate_create_event( @@ -139,11 +144,16 @@ def test_create_assessmentitem_with_file_hints(self): image_file = testdata.fileobj_exercise_image() image_file.uploaded_by = self.user image_file.save() - hints = "![alt_text](${}/{}.{})".format( + hint = "![alt_text](${}/{}.{})".format( exercises.IMG_PLACEHOLDER, image_file.checksum, image_file.file_format_id ) + hints = [ + {"hint": hint, "order": 1}, + ] + hints = json.dumps(hints) assessmentitem["hints"] = hints + response = self.sync_changes( [ generate_create_event( @@ -154,6 +164,7 @@ def test_create_assessmentitem_with_file_hints(self): ) ], ) + self.assertEqual(response.status_code, 200, response.content) try: ai = models.AssessmentItem.objects.get( @@ -182,7 +193,7 @@ def test_create_assessmentitem_with_file_no_permission(self): ASSESSMENTITEM, assessmentitem, channel_id=self.channel.id, - ) + ), ], ) self.assertEqual(response.status_code, 200, response.content) @@ -498,6 +509,103 @@ def test_delete_assessmentitems(self): except models.AssessmentItem.DoesNotExist: pass + def test_valid_hints_assessmentitem(self): + self.client.force_authenticate(user=self.user) + assessmentitem = self.assessmentitem_metadata + assessmentitem["hints"] = json.dumps([{'hint': 'asdasdwdqasd', 'order': 1}, {'hint': 'testing the hint', 'order': 2}]) + response = self.sync_changes( + [ + generate_create_event( + [assessmentitem["contentnode"], assessmentitem["assessment_id"]], + ASSESSMENTITEM, + assessmentitem, + channel_id=self.channel.id, + ), + ], + ) + self.assertEqual(response.status_code, 200, response.content) + try: + models.AssessmentItem.objects.get( + assessment_id=assessmentitem["assessment_id"] + ) + except models.AssessmentItem.DoesNotExist: + self.fail("AssessmentItem was not created") + + def test_invalid_hints_assessmentitem(self): + + self.client.force_authenticate(user=self.user) + assessmentitem = self.assessmentitem_metadata + assessmentitem["hints"] = json.dumps("test invalid string for hints") + response = self.sync_changes( + [ + generate_create_event( + [assessmentitem["contentnode"], assessmentitem["assessment_id"]], + ASSESSMENTITEM, + assessmentitem, + channel_id=self.channel.id, + ), + ], + ) + + self.assertEqual(response.json()["errors"][0]["table"], "assessmentitem") + self.assertEqual(response.json()["errors"][0]["errors"]["hints"][0], "JSON Data Invalid for hints") + self.assertEqual(len(response.json()["errors"]), 1) + + with self.assertRaises(models.AssessmentItem.DoesNotExist, msg="AssessmentItem was created"): + models.AssessmentItem.objects.get( + assessment_id=assessmentitem["assessment_id"] + ) + + def test_valid_answers_assessmentitem(self): + self.client.force_authenticate(user=self.user) + assessmentitem = self.assessmentitem_metadata + assessmentitem["answers"] = json.dumps([{'answer': 'test answer 1 :)', 'correct': False, 'order': 1}, + {'answer': 'test answer 2 :)', 'correct': False, 'order': 2}, + {'answer': 'test answer 3 :)', 'correct': True, 'order': 3} + ]) + response = self.sync_changes( + [ + generate_create_event( + [assessmentitem["contentnode"], assessmentitem["assessment_id"]], + ASSESSMENTITEM, + assessmentitem, + channel_id=self.channel.id, + ), + ], + ) + self.assertEqual(response.status_code, 200, response.content) + try: + models.AssessmentItem.objects.get( + assessment_id=assessmentitem["assessment_id"] + ) + except models.AssessmentItem.DoesNotExist: + self.fail("AssessmentItem was not created") + + def test_invalid_answers_assessmentitem(self): + + self.client.force_authenticate(user=self.user) + assessmentitem = self.assessmentitem_metadata + assessmentitem["answers"] = json.dumps("test invalid string for answers") + response = self.sync_changes( + [ + generate_create_event( + [assessmentitem["contentnode"], assessmentitem["assessment_id"]], + ASSESSMENTITEM, + assessmentitem, + channel_id=self.channel.id, + ), + ], + ) + + self.assertEqual(response.json()["errors"][0]["table"], "assessmentitem") + self.assertEqual(response.json()["errors"][0]["errors"]["answers"][0], "JSON Data Invalid for answers") + self.assertEqual(len(response.json()["errors"]), 1) + + with self.assertRaises(models.AssessmentItem.DoesNotExist, msg="AssessmentItem was created"): + models.AssessmentItem.objects.get( + assessment_id=assessmentitem["assessment_id"] + ) + class CRUDTestCase(StudioAPITestCase): @property diff --git a/contentcuration/contentcuration/viewsets/assessmentitem.py b/contentcuration/contentcuration/viewsets/assessmentitem.py index 978208b5f1..c78cbace72 100644 --- a/contentcuration/contentcuration/viewsets/assessmentitem.py +++ b/contentcuration/contentcuration/viewsets/assessmentitem.py @@ -1,8 +1,10 @@ +import json import re from django.db import transaction from django_bulk_update.helper import bulk_update from le_utils.constants import exercises +from rest_framework import serializers from rest_framework.permissions import IsAuthenticated from rest_framework.serializers import ValidationError @@ -42,12 +44,14 @@ def get_filenames_from_assessment(assessment_item): # Get unique checksums in the assessment item text fields markdown # Coerce to a string, for Python 2, as the stored data is in unicode, and otherwise # the unicode char in the placeholder will not match + answers = json.loads(assessment_item.answers) + hints = json.loads(assessment_item.hints) return set( exercise_image_filename_regex.findall( str( assessment_item.question - + assessment_item.answers - + assessment_item.hints + + str([a["answer"] for a in answers]) + + str([h["hint"] for h in hints]) ) ) ) @@ -74,6 +78,8 @@ def update(self, queryset, all_validated_data): class AssessmentItemSerializer(BulkModelSerializer): # This is set as editable=False on the model so by default DRF does not allow us # to set it. + hints = serializers.CharField(required=False) + answers = serializers.CharField(required=False) assessment_id = UUIDRegexField() contentnode = UserFilteredPrimaryKeyRelatedField( queryset=ContentNode.objects.all(), required=False @@ -98,6 +104,24 @@ class Meta: # Use the contentnode and assessment_id as the lookup field for updates update_lookup_field = ("contentnode", "assessment_id") + def validate_answers(self, value): + answers = json.loads(value) + for answer in answers: + if not type(answer) is dict: + raise ValidationError('JSON Data Invalid for answers') + if not all(k in answer for k in ('answer', 'correct', 'order')): + raise ValidationError('Incorrect field in answers') + return value + + def validate_hints(self, value): + hints = json.loads(value) + for hint in hints: + if not type(hint) is dict: + raise ValidationError('JSON Data Invalid for hints') + if not all(k in hint for k in ('hint', 'order')): + raise ValidationError('Incorrect field in hints') + return value + def set_files(self, all_objects, all_validated_data=None): # noqa C901 files_to_delete = [] files_to_update = {} @@ -108,8 +132,7 @@ def set_files(self, all_objects, all_validated_data=None): # noqa C901 # If this is an update operation, check the validated data for which items # have had these fields modified. md_fields_modified = { - self.id_value_lookup(ai) for ai in all_validated_data - if "question" in ai or "hints" in ai or "answers" in ai + self.id_value_lookup(ai) for ai in all_validated_data if "question" in ai or "hints" in ai or "answers" in ai } else: # If this is a create operation, just check if these fields are not null.