Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validates JSON fields for AssessmentItem #3901

Merged
merged 4 commits into from
Jan 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 112 additions & 4 deletions contentcuration/contentcuration/tests/viewsets/test_assessmentitem.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import absolute_import

import json
import uuid

from django.urls import reverse
Expand All @@ -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)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
31 changes: 27 additions & 4 deletions contentcuration/contentcuration/viewsets/assessmentitem.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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])
)
)
)
Expand All @@ -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
Expand All @@ -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 = {}
Expand All @@ -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.
Expand Down