Skip to content
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
16 changes: 12 additions & 4 deletions contentcuration/contentcuration/tests/testdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,13 +288,16 @@ def create_temp_file(filebytes, preset="document", ext="pdf", original_filename=
)


def create_studio_file(filebytes, preset="document", ext="pdf", original_filename=None):
def create_studio_file(
filebytes, preset="document", ext="pdf", original_filename=None, uploaded_by=None
):
"""
Create a file with contents of `filebytes` and the associated cc.File object for it.
:param filebytes: The data to be stored in the file (as bytes)
:param preset: String identifying the format preset (defaults to ``document``)
:param ext: File extension, omitting the initial period
:param original_filename: Original filename (needed for exercise_images)
:param uploaded_by: User the file should appear to have been uploaded by
Returns a dict containing the following:
- name (str): the filename within the content storage system (= md5 hash of the contents + .ext )
- data (bytes): file content (echo of `filebytes`)
Expand Down Expand Up @@ -335,6 +338,7 @@ def create_studio_file(filebytes, preset="document", ext="pdf", original_filenam
preset=preset,
original_filename=original_filename,
file_on_disk=storage_file_path,
uploaded_by=uploaded_by,
)

return {
Expand Down Expand Up @@ -414,20 +418,23 @@ def create_test_file(filebytes, ext="pdf"):
]


def fileobj_exercise_image(size=(100, 100), color="red"):
def fileobj_exercise_image(size=(100, 100), color="red", uploaded_by=None):
"""
Create a generic exercise image file in storage and return a File model pointing to it.
"""
image = Image.new("RGB", size, color=color)
buffer = BytesIO()
image.save(buffer, "JPEG")
temp_file_dict = create_studio_file(
buffer.getvalue(), preset=format_presets.EXERCISE_IMAGE, ext="jpg"
buffer.getvalue(),
preset=format_presets.EXERCISE_IMAGE,
ext="jpg",
uploaded_by=uploaded_by,
)
return temp_file_dict["db_file"]


def fileobj_exercise_graphie(original_filename=None):
def fileobj_exercise_graphie(original_filename=None, uploaded_by=None):
"""
Create an graphi exercise image file in storage and return a File model pointing to it.
"""
Expand All @@ -439,6 +446,7 @@ def fileobj_exercise_graphie(original_filename=None):
preset=format_presets.EXERCISE_GRAPHIE,
ext="graphie",
original_filename=original_filename or "theoriginalfilename",
uploaded_by=uploaded_by,
)
return temp_file_dict["db_file"]

Expand Down
74 changes: 74 additions & 0 deletions contentcuration/contentcuration/tests/utils/qti/test_ingest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import unittest

from contentcuration.tests.utils.qti.test_validation import _item_xml
from contentcuration.tests.utils.qti.test_validation import VALID_CHOICE_ITEM
from contentcuration.utils.assessment.qti.ingest import convert_legacy_question_to_qti
from contentcuration.utils.assessment.qti.ingest import (
find_perseus_custom_interaction_path,
)
from contentcuration.utils.assessment.qti.ingest import (
strip_content_storage_placeholder,
)
from contentcuration.utils.assessment.qti.media import get_qti_media_references
from contentcuration.utils.assessment.qti.validation import validate_qti_item


class StripContentStoragePlaceholderTests(unittest.TestCase):
def test_strips_placeholder_leaving_bare_filename(self):
text = "Look: ![](${☣ CONTENTSTORAGE}/abc123.png)"
self.assertEqual(
strip_content_storage_placeholder(text), "Look: ![](abc123.png)"
)


class ConvertLegacyQuestionToQTITests(unittest.TestCase):
def test_convert_legacy_question_to_qti_strips_placeholder_from_question_and_answers(
self,
):
question_data = {
"type": "multiple_selection",
"assessment_id": "abf45e8fd7f151adb1b3df2d751e945e",
"question": "Which is red? ![](${☣ CONTENTSTORAGE}/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.png)", # noqa
"answers": '[{"answer": "Apple ![](${☣ CONTENTSTORAGE}/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.png)", "correct": true, "order": 0}, {"answer": "Sky", "correct": false, "order": 1}]', # noqa
"randomize": False,
}
result = convert_legacy_question_to_qti(question_data)
validation_result = validate_qti_item(result.xml)
self.assertTrue(validation_result.is_valid, validation_result.errors)
self.assertEqual(
get_qti_media_references(result.xml),
{
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.png",
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.png",
},
)


def _custom_interaction_item_xml(data_type, path_attr, path_value):
return _item_xml(
"item1",
"t",
"",
'<qti-custom-interaction response-identifier="RESPONSE" data-type="%s" %s="%s"/>'
% (data_type, path_attr, path_value),
)


class FindPerseusCustomInteractionPathTests(unittest.TestCase):
def test_find_perseus_custom_interaction_path_detects_wrapper(self):
raw_data = _custom_interaction_item_xml(
"perseus", "data-perseus-path", "cccccccccccccccccccccccccccccccc.json"
)
self.assertEqual(
find_perseus_custom_interaction_path(raw_data),
"cccccccccccccccccccccccccccccccc.json",
)

def test_find_perseus_custom_interaction_path_returns_none_when_absent(self):
self.assertIsNone(find_perseus_custom_interaction_path(VALID_CHOICE_ITEM))

def test_find_perseus_custom_interaction_path_ignores_other_vendor_types(self):
raw_data = _custom_interaction_item_xml(
"other-vendor", "data-other-path", "cccccccccccccccccccccccccccccccc.json"
)
self.assertIsNone(find_perseus_custom_interaction_path(raw_data))
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from django.core.exceptions import ValidationError as DjangoValidationError
from le_utils.constants import exercises

from contentcuration.tests.base import StudioTestCase
from contentcuration.tests.testdata import channel
from contentcuration.tests.utils.qti.test_validation import _item_xml
from contentcuration.views.internal import create_exercises


class CreateExercisesInvalidQTITestCase(StudioTestCase):
def test_invalid_qti_raw_data_raises_node_validation_error(self):
node = channel().main_tree
questions = [
{
"assessment_id": "dddddddddddddddddddddddddddddddd",
"type": exercises.QTI,
"raw_data": "<not-qti>this is not valid QTI</not-qti>",
"files": [],
"source_url": None,
"randomize": False,
}
]
with self.assertRaises(DjangoValidationError):
create_exercises(self.admin_user, node, questions)

def test_unsupported_legacy_question_type_raises_node_validation_error(self):
node = channel().main_tree
questions = [
{
"assessment_id": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
"type": "not_a_real_question_type",
"question": "What?",
"answers": "[]",
"files": [],
"source_url": None,
"randomize": False,
}
]
with self.assertRaises(DjangoValidationError):
create_exercises(self.admin_user, node, questions)

def test_qti_raw_data_referencing_missing_file_raises_node_validation_error(self):
node = channel().main_tree
raw_data = _item_xml(
"item_1",
"Sample Item",
'<qti-response-declaration identifier="RESPONSE" cardinality="single" base-type="identifier">'
"<qti-correct-response><qti-value>choice_0</qti-value></qti-correct-response>"
"</qti-response-declaration>",
'<qti-choice-interaction response-identifier="RESPONSE" max-choices="1" '
'min-choices="0" orientation="vertical"><qti-prompt>Pick. '
'<img src="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.png" alt="diagram" /></qti-prompt>'
'<qti-simple-choice identifier="choice_0" show-hide="show" fixed="false">A</qti-simple-choice>'
'<qti-simple-choice identifier="choice_1" show-hide="show" fixed="false">B</qti-simple-choice>'
"</qti-choice-interaction>",
)
questions = [
{
"assessment_id": "ffffffffffffffffffffffffffffffff",
"type": exercises.QTI,
"raw_data": raw_data,
"files": [],
"source_url": None,
"randomize": False,
}
]
with self.assertRaises(DjangoValidationError):
create_exercises(self.admin_user, node, questions)

def test_qti_wrapping_perseus_custom_interaction_rejected(self):
node = channel().main_tree
raw_data = _item_xml(
"item_2",
"Sample Item",
"",
'<qti-custom-interaction response-identifier="RESPONSE" data-type="perseus" '
'data-perseus-path="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.json"/>',
)
questions = [
{
"assessment_id": "cccccccccccccccccccccccccccccccc",
"type": exercises.QTI,
"raw_data": raw_data,
"files": [],
"source_url": None,
"randomize": False,
}
]
with self.assertRaises(DjangoValidationError):
create_exercises(self.admin_user, node, questions)
Loading
Loading