Skip to content

refactor: extract reusable legacy→QTI conversion into qti/convert.py#6031

Open
rtibblesbot wants to merge 4 commits into
learningequality:unstablefrom
rtibblesbot:issue-6003-b9d97e
Open

refactor: extract reusable legacy→QTI conversion into qti/convert.py#6031
rtibblesbot wants to merge 4 commits into
learningequality:unstablefrom
rtibblesbot:issue-6003-b9d97e

Conversation

@rtibblesbot

@rtibblesbot rtibblesbot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Legacy→QTI conversion logic lived only inside the archive-building QTIExerciseGenerator, so other consumers needing the same per-type conversion would have to duplicate it. Extract the choice/text-entry XML-building logic into a standalone, storage/Django-decoupled function, and have QTIExerciseGenerator delegate to it instead of building QTI XML itself.

References

Fixes #6003. Related: #6030, #6002, #6007, #6011.

Reviewer guidance

  • contentcuration/contentcuration/utils/assessment/qti/convert.py:169convert_legacy_assessment_item_to_qti and its per-type builders were moved from archive.py; check the extraction preserved output byte-for-byte rather than just structurally.
  • contentcuration/contentcuration/utils/assessment/qti/archive.py:139 — confirm the native-QTI / perseus-raise / delegate-to-convert dispatch order in create_assessment_item is unchanged from before the refactor.

AI usage

Used Claude Code to extract the conversion logic and write per-type tests. Verified with the full QTI test suite and flake8.


@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-05 01:41 UTC

Pulls the per-type XML-building logic out of QTIExerciseGenerator into
a standalone, storage/Django-decoupled convert_legacy_assessment_item_to_qti()
so the API dual-read, ricecooker ingest, and global backfill can each call
it directly instead of re-implementing conversion.

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

@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 all makes sense to me, and it's pretty much just a code move rather than a full refactor - I think it can be done slightly more completely (especially with this intermediary state where the old tests still pass, so we can be confident of cleaning them up now).

@@ -0,0 +1,251 @@
# flake8: noqa: E501

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.

Rather than do this, maybe put the expected output XML into separate fixture files?

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 — moved the expected QTI XML for each conversion test into fixture files under tests/utils/qti/fixtures/ (single_selection.xml, multiple_selection.xml, true_false.xml, input_question.xml, plus two new ones for the ported edge cases below) and load them via a small _load_fixture() helper instead of inline triple-quoted strings.

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.

OK, but you've left the noqa comment here? Remove it.

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.

Removed the stray # flake8: noqa: E501 — it was left over from when the expected XML lived inline as long strings. Now that it lives in fixture files the exception is unneeded; flake8 passes clean without it.

return "".join(x.strip() for x in xml_string.split("\n"))


class ChoiceInteractionConversionTests(unittest.TestCase):

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.

I agree that this is now the right place for this, but I think we should also clean up the conversion specific tests from the archive creation tests here: https://github.com/learningequality/studio/blob/unstable/contentcuration/contentcuration/tests/utils/test_exercise_creation.py#L1226

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.

Cleaned up TestQTIExerciseCreation in test_exercise_creation.py: removed test_multiple_selection_question, test_free_response_question, and test_input_question, which only re-asserted per-type conversion XML already covered by test_convert.py. Trimmed test_basic_qti_exercise_creation to drop its duplicate item-XML assertion (kept the zip/manifest checks, which are archive-specific). test_question_with_mathematical_content, test_free_response_question_no_answers, and test_free_response_question_with_maths tested conversion edge cases (MathML rendering, empty-answers response declaration) with no archive-specific behavior, so I ported them into test_convert.py as test_math_content_in_choice_interaction, test_free_response_no_answers, and test_free_response_with_maths (using fixture files per the other thread) rather than dropping that coverage. Left the tests that exercise genuinely archive-specific behavior (manifest structure, image bundling/resizing, native QTI passthrough, Perseus/unsupported-type dispatch) as-is. Full suite still green (29 passed in test_exercise_creation.py, 10 in test_convert.py).

…sion tests

Addresses PR learningequality#6031 review: expected XML for qti/convert.py's tests now
lives in fixture files instead of inline strings, and the per-type
conversion assertions duplicated from test_exercise_creation.py's
TestQTIExerciseCreation are removed there (archive-level packaging
behavior stays); the math-rendering and no-answers edge cases they
covered move to test_convert.py alongside the other conversion tests.

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.

This looks good - just one last piece of cleanup needed.

@@ -0,0 +1,251 @@
# flake8: noqa: E501

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.

OK, but you've left the noqa comment here? Remove it.

rtibblesbot and others added 2 commits July 4, 2026 18:30
The noqa was needed when long XML strings lived inline; now that
expected XML lives in fixture files the exception is unnecessary.

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

Consolidates the duplicated ResponseDeclaration construction in choice and
text-entry conversion into one helper, and factors repeated
LegacyAssessmentItem setup in tests into a _make_item helper.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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] Reusable legacy→QTI assessment-item conversion utility

2 participants