Skip to content

feat: land ricecooker assessment items as QTI#6032

Merged
rtibbles merged 3 commits into
learningequality:unstablefrom
rtibblesbot:issue-6002-6c5913
Jul 8, 2026
Merged

feat: land ricecooker assessment items as QTI#6032
rtibbles merged 3 commits into
learningequality:unstablefrom
rtibblesbot:issue-6002-6c5913

Conversation

@rtibblesbot

@rtibblesbot rtibblesbot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Ricecooker's internal ingest endpoint built AssessmentItems directly and validated only assessment_id uniqueness, so malformed QTI, unconverted legacy question types, and Perseus content wrapped for republishing could all land in Studio unchecked. create_exercises now delegates item construction and schema validation to AssessmentItemSerializer: native QTI is validated and persisted as-is, legacy/structured questions are converted to schema-valid QTI on ingest, and raw Perseus passes through unchanged. A QTI item wrapping a Perseus custom interaction is rejected outright as a categorical upload mistake, rather than unwrapped. Referenced media is mapped for every resulting item, reusing the File row ricecooker's file_upload_url request already created instead of any extra bookkeeping.

References

Fixes #6002.

Reviewer guidance

  • views/internal.py:963-978 — DRF's ValidationError (bad QTI/legacy data) and OSError (a referenced checksum was never uploaded) are both caught and re-raised as NodeValidationError; confirm the wrapped message still gives ricecooker callers an actionable error in both cases.
  • views/internal.py:996-1005 — a QTI item wrapping a Perseus custom interaction is now rejected outright rather than unwrapped, per review feedback that this is a categorical ricecooker-side mistake to prevent, not handle.
  • viewsets/assessmentitem.py:147 — widened the raw_data validation to also accept PERSEUS_QUESTION, not just QTI. Confirm this can't be used to smuggle raw_data edits into a PERSEUS_QUESTION item through the browser-editor sync API in a way that wasn't intended.

AI usage

Used Claude Code to implement the refactor from an existing task plan and write the accompanying tests. Verified with the full affected test suite and the repo's pre-commit hooks.


@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly

How was this generated?
  • Ran pre-flight CI checks (lint, format, tests) and verified all pass
  • Rebased onto the target branch and resolved any conflicts
  • Reorganized commit history into clean, logical commits
  • Audited the diff to ensure only issue-relevant files are changed
  • Built PR body from the repository's PR template with evidence blocks
@rtibblesbot

🟡 Waiting for feedback

Last updated: 2026-07-08 18:53 UTC

Refactor create_exercises to delegate item construction and schema
validation to AssessmentItemSerializer instead of duplicating that
logic. Native QTI (editor-shaped or arbitrary) is persisted and
schema-validated directly; legacy/structured questions are converted
to QTI on ingest; raw Perseus passes through unchanged; a Perseus
custom interaction is unwrapped back to its raw perseus_question by
reading its data-perseus-path payload. Referenced media is mapped via
the existing map_files_to_assessment_item helper.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@rtibblesbot rtibblesbot marked this pull request as ready for review July 5, 2026 05:21

@rtibbles rtibbles left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is broadly looking sensible - I think the spec was overly generous, and we should tighten to just rejecting Perseus-in-QTI questions as an error, not trying to handle it.

Also, we should do a better attempt to reuse existing file objects, rather than do risky time based watermarking.


if question.get("type") == exercises.QTI:
raw_data = question.get("raw_data")
perseus_path = find_perseus_custom_interaction_path(raw_data)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually - I am thinking rather than trying to handle this, we should just reject it outright. Uploading this in this way is a categorical mistake. Ricecooker should be preventing this on its side.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, done — QTI items wrapping a Perseus custom interaction are now rejected outright (raises a ValueErrorNodeValidationError) rather than unwrapped. See 57f555e.

)
)

# AssessmentItemSerializer.set_files() auto-creates a File row per checksum

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels like a lot of extra bookkeeping - also, why wouldn't ricecooker create one? It would have to have done so in order to upload the file in the first place for it to exist in storage to be referenced here.

We should, wherever possible, defer to the file object created during the file_upload_url request. If the "auto extract" logic is failing at this, we need to improve that too, rather than having an essay of text explaining why we need to create three file objects then clean up 2 of them.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — dropped the create-then-cleanup dance. AssessmentItemSerializer.set_files() already reuses the pre-staged File row from ricecooker's file_upload_url request for a referenced checksum (that's exactly the reuse path it was built for), so serializer.save() now just attaches it directly. If a checksum has no matching upload, that surfaces as a storage-layer error (OSError), which we catch and turn into a NodeValidationError instead of a 500. Test now asserts the attached file is the pre-staged one (same id/original_filename), not a newly-created duplicate. See 57f555e.

rtibblesbot and others added 2 commits July 8, 2026 11:41
…ed File

Per review: uploading a QTI item that wraps a Perseus custom interaction
is a categorical mistake ricecooker should prevent on its side, so reject
it instead of unwrapping it. Also drop the create-three-files-then-clean-
up-two watermarking dance for QTI media - AssessmentItemSerializer.set_files()
already reuses the File row ricecooker's file_upload_url request created,
so just let it, and surface any storage-layer lookup failure as a
NodeValidationError.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…I XML builder

Merge the two try/except blocks around serializer validation and save
into one, and factor the custom-interaction test fixture to reuse
_item_xml instead of duplicating the QTI item XML template.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@rtibbles rtibbles left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All changes look correct - tests give confidence, and the tighter scoping reduces the potential for weirdness in here.

@rtibbles rtibbles merged commit 99c9787 into learningequality:unstable Jul 8, 2026
19 checks passed
@rtibblesbot rtibblesbot deleted the issue-6002-6c5913 branch July 8, 2026 19:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[QTI] Accept QTI items through the internal API for ricecooker import

2 participants