Skip to content

[QTI] Inline choice interaction: descriptor, parsing, XML assembly and validation #6180

Description

@AlexVelezLl

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

Overview

Add the inline choice interaction's headless layer: its descriptor, parsing, XML assembly and validation. This task has no UI. It fixes the XML shape and the in-memory state that the passage node (#6181) and the editor UI (#6182) build on, and it is fully testable without them.

Complexity: Medium
Target branch: unstable

Context

The interaction maps to <qti-inline-choice-interaction>. In the design, an inline choice question has two authored regions:

  • an optional Question (a rich-text prompt, like the other interactions' prompts)
  • a Passage of rich text holding one or more dropdowns ("chips"), each with its own plain-text options and one correct answer

Each dropdown is its own interaction with its own response declaration. Carrying several declarations through the editor core, and scoring across them, is #6179.

Text entry is the only other Placement.INLINE interaction and the nearest reference for recovering authored content from the item body (interactions/textEntry/parse.js). Choice is the nearest reference for choices with a correct answer (interactions/choice/).

Kolibri's QTI viewer already renders the interaction (kolibri/plugins/qti_viewer/frontend/components/interactions/InlineChoiceInteraction.vue), and the server's QTI 3.0 XSD validation accepts it.

QTI XML reference

<qti-assessment-item xmlns="http://www.imsglobal.org/xsd/imsqtiasi_v3p0"
  identifier="item_k2lm9qaz" title="Question" adaptive="false" time-dependent="false">

  <qti-response-declaration identifier="response_xq7tbn2c" cardinality="single" base-type="identifier">
    <qti-correct-response>
      <qti-value>choice_a1b2c3d4</qti-value>
    </qti-correct-response>
  </qti-response-declaration>
  <qti-response-declaration identifier="response_pw4rzk8d" cardinality="single" base-type="identifier">
    <qti-correct-response>
      <qti-value>choice_m0o0n0aa</qti-value>
    </qti-correct-response>
  </qti-response-declaration>

  <!-- Outcome declarations and response processing are generated by the core (#6179) -->

  <qti-item-body>
    <!-- Question (optional) -->
    <p>Select the correct words to complete the sentence:</p>

    <!-- Passage -->
    <p>
      The Earth
      <qti-inline-choice-interaction response-identifier="response_xq7tbn2c" shuffle="true">
        <qti-inline-choice identifier="choice_a1b2c3d4">revolves</qti-inline-choice>
        <qti-inline-choice identifier="choice_e5f6g7h8">stays</qti-inline-choice>
      </qti-inline-choice-interaction>
      around the Sun, and the
      <qti-inline-choice-interaction response-identifier="response_pw4rzk8d" shuffle="true">
        <qti-inline-choice identifier="choice_s0u0n0aa">Sun</qti-inline-choice>
        <qti-inline-choice identifier="choice_s0t0a0rs">Stars</qti-inline-choice>
        <qti-inline-choice identifier="choice_m0o0n0aa">Moon</qti-inline-choice>
      </qti-inline-choice-interaction>
      orbits the Earth.
    </p>
  </qti-item-body>
</qti-assessment-item>

Rules:

  1. Every dropdown is one <qti-inline-choice-interaction> with its own response-identifier, bound to its own <qti-response-declaration> with cardinality="single", base-type="identifier" and exactly one <qti-value> in <qti-correct-response>. A dropdown with no correct option yet has no <qti-correct-response>.
  2. Response identifiers are unique within the item and stable across edits: adding, removing or reordering a dropdown must not rename the others. Kolibri stores the learner's answer state keyed by response identifier (AssessmentItem.vue).
  3. Choice identifiers are unique within their dropdown and stable across edits; choices without one get generateRandomSlug('choice').
  4. <qti-inline-choice> content is plain text. The passage around the dropdowns is rich text, and a dropdown can sit inside formatted content (e.g. within bold text or a list item).
  5. shuffle is always "true".
  6. Response declarations appear in the same order as their dropdowns in the passage.
  7. The question and the passage must be distinguishable when the item is parsed back. An item saved with an empty question, or a question that itself contains several paragraphs, must reopen with the same question and passage it was saved with.

The Change

  • New QtiInteraction.INLINE_CHOICE and QuestionType.INLINE_CHOICE.
  • An inline choice descriptor under interactions/inlineChoice/ (Descriptor.js, parse.js, validation.js), with Placement.INLINE, registered in interactions/descriptors.js.
  • parse and buildXML round-trip the question, the passage, and every dropdown's options and correct answer, following the rules above. The state should keep dropdowns in passage order, addressable by response identifier, so the passage node ([QTI] Inline choice chip and Insert action in the passage editor #6181) and the options list ([QTI] Inline choice interaction editor #6182) can refer to the same dropdown.
  • Validation reuses the existing ValidationError codes. Each error is attributed to the dropdown it belongs to (by response identifier), and to the option where there is one:
    • a dropdown with fewer than two options → TOO_FEW_CHOICES
    • a dropdown with no correct option → NO_CORRECT_ANSWER
    • an empty option → EMPTY_CHOICE_CONTENT
    • two options with the same text in one dropdown → DUPLICATE_CHOICE_CONTENT
  • A passage with no dropdowns is already reported as NO_INTERACTION by validateItemShape. The question is optional, so an empty question is not an error.
  • The question type is not offered in the selector yet (getTypeOptions contributes nothing). [QTI] Inline choice interaction editor #6182 turns it on.

Out of Scope

Acceptance Criteria

General

  • Parsing and rebuilding an item with a question and several dropdowns returns the same question, passage text, dropdown order, options, correct answers and identifiers
  • An item with an empty question, and an item whose question has several paragraphs, both reopen with the question and passage they were saved with
  • Rebuilt XML follows every rule above and passes server-side QTI validation
  • A dropdown with no correct option is written without <qti-correct-response>, and parses back with no correct option
  • Removing or reordering a dropdown in the state leaves the other dropdowns' response identifiers unchanged in the rebuilt XML
  • Each validation case listed above is reported with the response identifier of its dropdown
  • resolveDescriptor resolves an item containing qti-inline-choice-interaction to the inline choice descriptor and QuestionType.INLINE_CHOICE
  • The question type selector does not offer Inline choice yet

Testing

  • parse.spec.js covers the round trips above, including missing identifiers and a dropdown with no correct response
  • validation.spec.js covers each validation case
  • Descriptor.spec.js covers matching and question type resolution

Testing

pnpm jest --config jest_config/jest.conf.js contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions

References

AI usage

I used Claude Code to draft this issue from the Figma design and the existing interaction plugins, which it read to ground the Context and rules. I decided how to split the work and reviewed and edited the text.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions