Skip to content

[QTI] Serve legacy assessment items as QTI through the API endpoint (dual-read) #6030

Closed

Description

@rtibbles

❌ This issue is not open for contribution. Visit Contributing guidelines to learn about the contributing process and how to find suitable issues.

Overview

During a short transition window, the AssessmentItem API endpoint converts legacy items to QTI on read — via the reusable conversion (#6003) — so the frontend only ever receives QTI and needs no backward-compatibility handling. The one-time global backfill that makes this permanent and removes the dual-read is #6007.

Complexity: Medium
Target branch: unstable

Context

The Change

Acceptance Criteria

  • The API endpoint returns QTI for a still-legacy row (dual-read), so the frontend only ever receives QTI
  • perseus_question rows are returned unchanged
  • Conversion failures are surfaced, not silently dropped
  • Tests cover the API dual-read across the supported legacy types

References

AI usage

Architecture decided with the maintainer across an iterative session: blanket QTI type with the item XML in raw_data; XSD-authoritative validation across all sources; a legacy→QTI global migration with an API-layer dual-read; ricecooker upload delegating to the AssessmentItem serializer; and a Perseus custom-interaction contract confirmed against the QTI 3.0 specification. Claude mapped the existing publish/validation/ricecooker code, proposed the breakdown, and drafted each issue; the maintainer steered every decision and reviewed throughout.

Activity

  1. rtibblesbot commented on Aug 1, 2026

    @rtibblesbot
    Contributor

    Plan: Make the AssessmentItem API read path return QTI for still-legacy rows, so the frontend only ever receives type='QTI' with item XML in raw_data.

    • Represent an answerless legacy choice question in QTI
    • Dual-read legacy rows as QTI
    • Tag the converted item with the content node's language

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

    How was this generated?
    • Implemented code following the pre-approved plan using test-driven development (red-green-refactor)
    • Ran the project's full test suite and linter before committing
    • For UI changes: captured live screenshots to verify rendering
    • For backend changes: verified with example API/CLI output
  2. rtibblesbot commented on Aug 1, 2026

    @rtibblesbot
    Contributor

    Acceptance criteria — where each is covered (PR #6073):

    AC Covered by
    Endpoint returns QTI for a still-legacy row DualReadTestCase.test_supported_legacy_types_returned_as_qti (list route, all five convertible types) and test_detail_route_converts (serialize_object reaches consolidate by a different path)
    perseus_question rows returned unchanged test_perseus_question_returned_unchanged — type and raw_data byte-identical. test_native_qti_item_returned_unchanged pins the same for stored QTI.
    Conversion failures surfaced, not silently dropped consolidate re-raises as LegacyConversionError; test_unconvertible_type_raises_on_list_route and test_unconvertible_type_is_not_a_404_on_detail_route. The second is the load-bearing one — serialize_object catches (IndexError, ValueError, TypeError) and turns them into a 404, so a bare ValueError would report a corrupt row as a missing one.
    Tests cover the dual-read across supported legacy types single_selection, multiple_selection, true_false, input_question, free_response — one subTest each, every one asserting the returned raw_data is XSD-valid and carries the expected interaction element. The answerless choice variant (the shape the editor writes for every newly added question) is covered separately at both layers: test_answerless_choice_item_is_returned_as_valid_qti and test_single_selection_no_answers.

    pytest contentcuration/contentcuration/tests/viewsets/test_assessmentitem.py contentcuration/contentcuration/tests/utils/qti — 305 passed.

    Two things on the PR still need a maintainer decision rather than a code change; both are called out in the PR body under Reviewer guidance: the sequencing constraint against AssessmentTab.vue still mounting the legacy AssessmentEditor, and raising rather than log-and-skip on the read path, which is the opposite of what was agreed for publish on #6029.

  3. rtibblesbot commented on Aug 1, 2026

    @rtibblesbot
    Contributor

    The bot has been waiting for a response for 2 hour(s). It will resume automatically when someone replies.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions