Ingest QTI 3.0 packages as per-item QTI exercises - #753
Conversation
d7bd100 to
a50caed
Compare
a50caed to
3277d2b
Compare
rtibbles
left a comment
There was a problem hiding this comment.
Uploaded to unstable Studio: every QTI 3.0 package in 1EdTech's qti-examples, plus Citolab's biologie and examples. 24 exercises, 322 QTI items and 112 images committed, all complete, with practice-quiz defaults and chef overrides stored as intended. Two changes before merge.
|
|
||
| def _build_qti_question(self, member, package, images): | ||
| """``images`` maps media package paths to file dicts, shared across items.""" | ||
| text, root = read_qti3(package.directory, member, "qti-assessment-item") |
There was a problem hiding this comment.
blocking: One XSD-invalid item fails the whole channel upload. 1EdTech's data-attributes.xml has a qti-rubric-block without the required use attribute. It passes here; Studio's validate_qti_item rejects it, Studio fails the node and its 10-node add_nodes batch, and ricecooker refuses to commit.
Validate each item against the XSD Studio uses (schema/xsd, root imsqti_itemv3p0p1_v1p0.xsd) and reject failures like other unusable items, naming the first XSD error in the warning. Test: an XSD-invalid item is rejected and the package's valid items still ingest.
There was a problem hiding this comment.
Fixed in 5c4dcc8. Studio's XSD tree (studio@3bdac307) is vendored in ricecooker/utils/qti_xsd/. _build_qti_question validates each item against it and rejects failures, naming the first error (for data-attributes.xml: "fails the QTI 3.0 schema at line 15: … The attribute 'use' is required but missing."). New test: test_schema_invalid_items_are_rejected. I searched for other paths that send QTI to Studio. QTIQuestion is built only from this pipeline output (nodes.py:1170), so this is the one place. lxml is now a direct dependency (it was already installed via ebooklib/pycaption).
| if path is None or not os.path.isfile(path): | ||
| raise ValueError(f"references missing media {ref}") | ||
| if extract_path_ext(path) not in self.QTI_IMAGE_EXTENSIONS: | ||
| raise ValueError(f"references non-image media {ref}") |
There was a problem hiding this comment.
Strip <qti-stylesheet> elements from the item instead of rejecting it. Every item in Citolab's biologie package links ../css/cito_itemstyle.css, so 32 of its 40 items are rejected for the stylesheet alone. The qti-stylesheet case in test_items_with_unusable_media_are_rejected and the media line in docs/exercises.md change with it.
There was a problem hiding this comment.
Fixed in 71e675a. <qti-stylesheet> elements (both self-closing and paired) are now stripped before validation and media handling. 32llxx.xml now passes validation with its stylesheet removed. The stylesheet case is gone from test_items_with_unusable_media_are_rejected. New test: test_item_stylesheets_are_stripped. docs/exercises.md is updated. The class search found no other non-rendering QTI elements that cause rejection.
71e675a to
f3918b3
Compare
| def _build_qti_question(self, member, package, images): | ||
| """``images`` maps media package paths to file dicts, shared across items.""" | ||
| text, root = read_qti3(package.directory, member, "qti-assessment-item") | ||
| text = strip_stylesheets(text) |
There was a problem hiding this comment.
Strip every element and attribute outside the namespaces Studio's XSDs define, not just <qti-stylesheet>. 29 of biologie's 40 items are still rejected: 32cpek.xml puts a dep:dep-calculator in qti-companion-materials-info, and grpStrict.any needs a schema we don't have. Each of these items also has a standard <qti-calculator>, so nothing is lost.
- Parse with lxml, not regex. Which elements are foreign depends on each document's
xmlnsbindings. - Keep the QTI, MathML, SSML, XInclude and XML namespaces (the
targetNamespaces inqti_xsd/). - Do the
<qti-stylesheet>removal in the same pass and delete_STYLESHEET_RE. - Validate the stripped tree instead of re-parsing the text.
- Test: an item with a foreign-namespace element in
qti-companion-materials-infoingests without it.
There was a problem hiding this comment.
Done in 8c636d1 (parse_qti_item in ricecooker/utils/qti.py):
- One lxml pass drops
<qti-stylesheet>plus any element or attribute whose namespace isn't one of the 5 XSDtargetNamespaces;cleanup_namespacesthen drops the unusedxmlns:bindings _STYLESHEET_REis gone;validate_qti_itemvalidates the stripped tree, andraw_datais that tree serialized- Test:
test_foreign_namespace_markup_is_strippedputs adep:element inqti-companion-materials-infoand a foreign attribute onqti-item-body; both fail the XSD unstripped - Searched
qti.pyandconvert.pyfor other regex or text handling of item XML:_STYLESHEET_REwas the only one - History reshaped: stylesheet stripping now lives in the media commit, namespace stripping and the dropped identifier check in the schema commit
f3918b3 to
8c636d1
Compare
Carries a QTI 3.0 item as raw_data with its exercise images (studio#6032). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
8c636d1 to
0a348ed
Compare
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
0a348ed to
d97e727
Compare
| leaf["extra_fields"] = extra_fields | ||
| return leaf | ||
|
|
||
| def _build_qti_exercises(self, manifest, package): |
There was a problem hiding this comment.
blocking: Move QTI exercise building out of IMSCPConversionHandler into a per-package builder class in ricecooker/utils/qti.py, rather than growing convert.py further.
- Hold the package, the pipeline, and the question and image caches on the instance
- Drop the
build_questionandstore_imageclosures, and thebuild_questionparameter threaded through_build_qti_test_exerciseand_build_qti_exercise - The handler keeps one call, e.g.
QTIExerciseBuilder(package, self.get_pipeline()).exercises(manifest) - No behaviour change: the existing QTI tests in
tests/pipeline/test_convert.pyshould pass unchanged
There was a problem hiding this comment.
Done in e14364a: QTIExerciseBuilder in ricecooker/utils/qti.py holds the package, pipeline, and question/image caches; the handler makes the one call.
- Both closures and the threaded
build_questionparameter are gone; image refs map through a bound method. convert.pyimportsqtiinsidehandle_filebecauseqtiimports itsImageConversionHandler/SVGValidationHandler, which would otherwise be a cycle.- Searched
convert.pyfor other QTI code: only theis_qti_resourceguard in_build_leafstays. tests/pipeline/test_convert.pypasses unchanged.
parse_imscp_manifest returns every QTI resource as qti_resources, since QTI packages have no organization. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
e14364a to
f311cce
Compare
Each imsqti_test_* resource becomes a practice-quiz exercise of its items in test order; loose items with no test become one. Non-3.0 items are rejected. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- ContentNode builds QTIQuestions from decomposed exercise leaves - Chef's mastery_model/m/n/randomize/options.modality override the practice-quiz defaults - Exercise validation and mastery defaults lifted from ExerciseNode to ContentNode - Document QTI packages in docs/exercises.md Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Studio fails the whole channel commit on one schema-invalid item, so validate each item against Studio's vendored QTI 3.0 item XSD. - Strip elements and attributes outside the XSD's namespaces first; its strict wildcards reject them - Drop the missing-identifier check; the XSD requires identifier Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
f311cce to
671c43d
Compare
rtibbles
left a comment
There was a problem hiding this comment.
Changes make sense, manual testing checks out, and the code organization helps to keep it more readable.
Summary
ContentNode(uri=...)becomes one practice quiz per test, or one quiz of all loose items.type="QTI"item with its<qti-assessment-item>XML inraw_data.<checksum>.<ext>and uploaded asexercise_image.ricecooker/utils/qti_xsd/; validation adds anlxmldependency.<qti-stylesheet>elements, and elements and attributes outside the XSD's namespaces, are stripped from items.extra_fieldsoverride the quiz defaults.References
Fixes #337. Studio contract: learningequality/studio#6032. Follow-up: #752.
Reviewer guidance
node.to_dict()["questions"][i]["raw_data"]fromtest_single_test_package_becomes_a_practice_quizto Studio'scontentcuration.utils.assessment.qti.validation.validate_qti_item; both return valid.ExerciseNode'sm/ndefaults and mastery validation moved up toContentNodeso a one-test package can become the exercise itself; does anything outside exercises rely onContentNodeskipping them?Deviations from the issue spec
AI usage
Used Claude Code to plan and implement the ingestion from the maintainer-approved design. Verified with the full test suite, prek, and Studio's QTI XSD validator run on the produced payload.
🤖 Generated with Claude Code
@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly
How was this generated?
🟡 Waiting for feedback
Last updated: 2026-09-25 18:24 UTC