public function convert(BaseQuestionType $question, $interactionIdentifier, $interactionLabel)
 {
     /** @var tokenhighlight $question */
     // Grab those `template` and convert those highlights to <hottext>
     $html = new SimpleHtmlDom();
     $html->load($question->get_template());
     $tokens = $html->find('span.lrn_token');
     $indexIdentifierMap = [];
     foreach ($tokens as $key => &$span) {
         $span->outertext = '<hottext identifier="TOKEN_' . intval($key) . '">' . $span->innertext . '</hottext>';
         $indexIdentifierMap[$key] = 'TOKEN_' . intval($key);
     }
     $htmlContent = $html->save();
     $contentComponents = QtiMarshallerUtil::unmarshallElement($htmlContent);
     $contentCollection = ContentCollectionBuilder::buildBlockStaticCollectionContent($contentComponents);
     // Build the interaction
     $interaction = new HottextInteraction($interactionIdentifier, $contentCollection);
     $interaction->setLabel($interactionLabel);
     $interaction->setPrompt($this->convertStimulusForPrompt($question->get_stimulus()));
     // Learnosity does not enforce number of choices, thus using default such the min choice would be 1
     // and max would be the `max_selection` if set, otherwise use token count
     $interaction->setMinChoices(1);
     $interaction->setMaxChoices(is_int($question->get_max_selection()) ? $question->get_max_selection() : count($tokens));
     // Build validation
     $builder = new TokenhighlightValidationBuilder($indexIdentifierMap);
     list($responseDeclaration, $responseProcessing) = $builder->buildValidation($interaction->getResponseIdentifier(), $question->get_validation());
     return [$interaction, $responseDeclaration, $responseProcessing];
 }
 public function testSimpleCase()
 {
     /** @var AssessmentItem $assessmentItem */
     $question = json_decode($this->getFixtureFileContents('learnosityjsons/data_clozedropdown.json'), true);
     $assessmentItem = $this->convertToAssessmentItem($question);
     $interactions = $assessmentItem->getComponentsByClassName('inlineChoiceInteraction', true)->getArrayCopy();
     /** @var InlineChoiceInteraction $interactionOne */
     $interactionOne = $interactions[0];
     /** @var InlineChoiceInteraction $interactionTwo */
     $interactionTwo = $interactions[1];
     $this->assertTrue($interactionOne instanceof InlineChoiceInteraction);
     $this->assertTrue($interactionTwo instanceof InlineChoiceInteraction);
     $content = QtiMarshallerUtil::marshallCollection($assessmentItem->getItemBody()->getContent());
     $this->assertNotEmpty($content);
     // Assert response processing template
     $this->assertEquals(Constants::RESPONSE_PROCESSING_TEMPLATE_MATCH_CORRECT, $assessmentItem->getResponseProcessing()->getTemplate());
     // Assert response declarations
     $responseDeclarations = $assessmentItem->getResponseDeclarations()->getArrayCopy();
     /** @var ResponseDeclaration $responseDeclarationOne */
     $responseDeclarationOne = $responseDeclarations[0];
     /** @var ResponseDeclaration $responseDeclarationTwo */
     $responseDeclarationTwo = $responseDeclarations[1];
     // Check has the correct identifiers, also correct `correctResponse` values
     $this->assertEquals($responseDeclarationOne->getIdentifier(), $interactionOne->getResponseIdentifier());
     $this->assertNull($responseDeclarationOne->getMapping());
     $this->assertEquals('INLINECHOICE_2', $responseDeclarationOne->getCorrectResponse()->getValues()->getArrayCopy()[0]->getValue());
     $this->assertEquals('Choice C', QtiMarshallerUtil::marshallCollection($interactionOne->getComponentByIdentifier('INLINECHOICE_2')->getComponents()));
     $this->assertEquals($responseDeclarationTwo->getIdentifier(), $interactionTwo->getResponseIdentifier());
     $this->assertNull($responseDeclarationTwo->getMapping());
     $this->assertEquals('INLINECHOICE_1', $responseDeclarationTwo->getCorrectResponse()->getValues()->getArrayCopy()[0]->getValue());
     $this->assertEquals('Choice B', QtiMarshallerUtil::marshallCollection($interactionTwo->getComponentByIdentifier('INLINECHOICE_1')->getComponents()));
 }
 public function testSimpleCase()
 {
     $data = json_decode($this->getFixtureFileContents('learnosityjsons/tokenhighlight.json'), true);
     $assessmentItem = $this->convertToAssessmentItem($data);
     // Has <hottextInteraction> as the first and only interaction
     /** @var HottextInteraction $interaction */
     $interaction = $assessmentItem->getComponentsByClassName('hottextInteraction', true)->getArrayCopy()[0];
     $this->assertTrue($interaction instanceof HottextInteraction);
     // And its prompt is mapped correctly to item body
     $promptString = QtiMarshallerUtil::marshallCollection($interaction->getPrompt()->getComponents());
     $this->assertEquals('<p>[This is the stem.]</p>', $promptString);
     // Assert we have 3 hottext elements
     /** @var Hottext[] $hottexts */
     $hottexts = $interaction->getComponentsByClassName('hottext', true)->getArrayCopy(true);
     $this->assertEquals(3, $interaction->getComponentsByClassName('hottext', true)->count());
     $this->assertEquals('TOKEN_0', $hottexts[0]->getIdentifier());
     $this->assertEquals('Risus et tincidunt turpis facilisis.', QtiMarshallerUtil::marshallCollection($hottexts[0]->getComponents()));
     $this->assertEquals('TOKEN_1', $hottexts[1]->getIdentifier());
     $this->assertEquals('Curabitur eu nulla justo. Curabitur vulputate ut nisl et bibendum. ' . 'Nunc diam enim, porta sed eros vitae. dignissim, et tincidunt turpis facilisis.', QtiMarshallerUtil::marshallCollection($hottexts[1]->getComponents()));
     $this->assertEquals('TOKEN_2', $hottexts[2]->getIdentifier());
     $this->assertEquals('Curabitur eu nulla justo. Curabitur vulputate ut nisl et bibendum.', QtiMarshallerUtil::marshallCollection($hottexts[2]->getComponents()));
     // Assert we have the correct response processing template
     $responseProcessingTemplate = $assessmentItem->getResponseProcessing()->getTemplate();
     $this->assertEquals(Constants::RESPONSE_PROCESSING_TEMPLATE_MATCH_CORRECT, $responseProcessingTemplate);
     // Assert we have the correct response declaration values and map entries
     /** @var ResponseDeclaration $responseDeclaration */
     $responseDeclaration = $assessmentItem->getResponseDeclarations()->getArrayCopy()[0];
     $this->assertEquals(BaseType::IDENTIFIER, $responseDeclaration->getBaseType());
     $this->assertEquals(Cardinality::MULTIPLE, $responseDeclaration->getCardinality());
     /** @var Value[] $values */
     $values = $responseDeclaration->getCorrectResponse()->getValues()->getArrayCopy(true);
     $this->assertEquals('TOKEN_0', $values[0]->getValue());
     $this->assertEquals('TOKEN_2', $values[1]->getValue());
     $this->assertNull($responseDeclaration->getMapping());
 }
 public function getQuestionType()
 {
     /* @var QtiExtendedTextInteraction $interaction */
     $interaction = $this->interaction;
     $longtext = new longtext('longtext');
     LogService::log('No validation mapping supported for this interaction. Ignoring any ' . '<responseProcessing> and <responseDeclaration> if any');
     if (!empty($interaction->getPrompt())) {
         $promptContent = $interaction->getPrompt()->getContent();
         $longtext->set_stimulus(QtiMarshallerUtil::marshallCollection($promptContent));
     }
     if ($interaction->getPlaceholderText()) {
         $longtext->set_placeholder($interaction->getPlaceholderText());
     }
     /** As per QTI spec
      *  When multiple strings are accepted, expectedLength applies to each string.
      *  `expectedLength` works as a only as a 'hint' to student so we do not want to force a hard limit
      */
     if ($interaction->getExpectedLength() > 0) {
         $maxStrings = $interaction->getMaxStrings() > 0 ? $interaction->getMaxStrings() : 1;
         $expectedLength = $interaction->getExpectedLength() / 5;
         $longtext->set_max_length($maxStrings * $expectedLength);
         $longtext->set_submit_over_limit(true);
     }
     return $longtext;
 }
 public function convert(BaseQuestionType $questionType, $interactionIdentifier, $interactionLabel)
 {
     /** @var clozetext $question */
     $question = $questionType;
     // Extra text that can't be mapped since we are in textEntryInteraction which does not have prompt
     $this->extraContent = $question->get_stimulus();
     // Replace {{ response }} with `textEntryInteraction` elements
     $maxLength = !is_null($question->get_max_length()) ? intval($question->get_max_length()) : 15;
     // Set default to `15` if not set
     $index = 0;
     $template = preg_replace_callback('/{{response}}/', function ($match) use(&$index, $interactionIdentifier, $interactionLabel, $maxLength) {
         $interaction = new TextEntryInteraction($interactionIdentifier . '_' . $index);
         $interaction->setLabel($interactionLabel);
         $interaction->setExpectedLength($maxLength);
         $index++;
         $replacement = QtiMarshallerUtil::marshall($interaction);
         return $replacement;
     }, $question->get_template());
     // Wrap this interaction in a block since our `clozetext` `template` meant to be blocky and not inline
     $div = new Div();
     $div->setClass('lrn-template');
     $div->setContent(ContentCollectionBuilder::buildFlowCollectionContent(QtiMarshallerUtil::unmarshallElement($template)));
     // Build validation
     $isCaseSensitive = is_null($question->get_case_sensitive()) ? true : $question->get_case_sensitive();
     $validationBuilder = new ClozetextValidationBuilder($isCaseSensitive);
     list($responseDeclaration, $responseProcessing) = $validationBuilder->buildValidation($interactionIdentifier, $question->get_validation(), $isCaseSensitive);
     return [$div, $responseDeclaration, $responseProcessing];
 }
 public function testShorttextQuestionWithSimpleValidation()
 {
     $data = json_decode($this->getFixtureFileContents('learnosityjsons/shorttext.json'), true);
     $assessmentItem = $this->convertToAssessmentItem($data);
     // Has <textEntryInteraction> as the first and only interaction
     /** @var TextEntryInteraction $interaction */
     $interaction = $assessmentItem->getComponentsByClassName('textEntryInteraction', true)->getArrayCopy()[0];
     // Test basic attributes
     $this->assertTrue($interaction instanceof TextEntryInteraction);
     $this->assertEquals('placeholdertext', $interaction->getPlaceholderText());
     $this->assertEquals(15, $interaction->getExpectedLength());
     // Shorttext shall have one simple `map_response` <responseDeclaration> and <responseProcessing>
     /** @var ResponseDeclaration $responseDeclaration */
     $responseDeclaration = $assessmentItem->getResponseDeclarations()->getArrayCopy()[0];
     $this->assertEquals(Constants::RESPONSE_PROCESSING_TEMPLATE_MAP_RESPONSE, $assessmentItem->getResponseProcessing()->getTemplate());
     /** @var Value[] $values */
     $values = $responseDeclaration->getCorrectResponse()->getValues()->getArrayCopy(true);
     $this->assertEquals('hello', $values[0]->getValue());
     $this->assertEquals('anotherhello', $values[1]->getValue());
     /** @var MapEntry[] $mapEntries */
     $mapEntries = $responseDeclaration->getMapping()->getMapEntries()->getArrayCopy(true);
     $this->assertEquals('hello', $mapEntries[0]->getMapKey());
     $this->assertEquals(2, $mapEntries[0]->getMappedValue());
     $this->assertEquals('anotherhello', $mapEntries[1]->getMapKey());
     $this->assertEquals(1, $mapEntries[1]->getMappedValue());
     // Check itembody is correct that the stimulus is appended before
     $itemBodyContent = QtiMarshallerUtil::marshallCollection($assessmentItem->getItemBody()->getComponents());
     $expectedString = '<p>[This is the stem.]</p><div><textEntryInteraction responseIdentifier="shorttexttestreference" expectedLength="15" placeholderText="placeholdertext" label="shorttexttestreference"/></div>';
     $this->assertEquals($expectedString, $itemBodyContent);
 }
 public function testSingularResponsesWithNoValidation()
 {
     $question = $this->buildSimpleChoiceMatrixQuestion();
     /** @var MatchInteraction $interaction */
     $mapper = new ChoicematrixMapper();
     list($interaction, $responseDeclaration, $responseProcessing) = $mapper->convert($question, 'testIdentifier', 'testIdentifier');
     $this->assertEquals('My stimulus string', QtiMarshallerUtil::marshallCollection($interaction->getPrompt()->getComponents()));
     $this->assertFalse($interaction->mustShuffle());
     $this->assertEquals(2, $interaction->getMaxAssociations());
     $this->assertEquals(2, $interaction->getMinAssociations());
     // Assert its source choices (stems)
     /** @var SimpleAssociableChoice[] $stemAssociableChoices */
     $stemAssociableChoices = $interaction->getSourceChoices()->getSimpleAssociableChoices()->getArrayCopy(true);
     $this->assertEquals('Stem 1', QtiMarshallerUtil::marshallCollection($stemAssociableChoices[0]->getContent()));
     $this->assertEquals('STEM_0', $stemAssociableChoices[0]->getIdentifier());
     $this->assertEquals('Stem 2', QtiMarshallerUtil::marshallCollection($stemAssociableChoices[1]->getContent()));
     $this->assertEquals('STEM_1', $stemAssociableChoices[1]->getIdentifier());
     foreach ($stemAssociableChoices as $choice) {
         $this->assertEquals(1, $choice->getMatchMax());
         $this->assertEquals(1, $choice->getMatchMin());
     }
     // Assert its target choices (options)
     /** @var SimpleAssociableChoice[] $optionAssociableChoices */
     $optionAssociableChoices = $interaction->getTargetChoices()->getSimpleAssociableChoices()->getArrayCopy(true);
     $this->assertEquals('Option 1', QtiMarshallerUtil::marshallCollection($optionAssociableChoices[0]->getContent()));
     $this->assertEquals('OPTION_0', $optionAssociableChoices[0]->getIdentifier());
     $this->assertEquals('Option 2', QtiMarshallerUtil::marshallCollection($optionAssociableChoices[1]->getContent()));
     $this->assertEquals('OPTION_1', $optionAssociableChoices[1]->getIdentifier());
     $this->assertEquals('Option 3', QtiMarshallerUtil::marshallCollection($optionAssociableChoices[2]->getContent()));
     $this->assertEquals('OPTION_2', $optionAssociableChoices[2]->getIdentifier());
     foreach ($optionAssociableChoices as $choice) {
         $this->assertEquals(2, $choice->getMatchMax());
         $this->assertEquals(1, $choice->getMatchMin());
     }
 }
 public function testMappingSimpleQuestionWithNoValidation()
 {
     $placeholder = 'placeholdertest';
     $stimulus = '<strong>stimulushere</strong>';
     $questionReference = 'questionReferenceOne';
     $question = new plaintext('plaintext');
     $question->set_placeholder($placeholder);
     $question->set_stimulus($stimulus);
     $mapper = new PlaintextMapper();
     /** @var ExtendedTextInteraction $interaction */
     list($interaction, $responseDeclaration, $responseProcessing) = $mapper->convert($question, $questionReference, $questionReference);
     // No validation shall be mapped for longtext
     $this->assertNull($responseDeclaration);
     $this->assertNull($responseProcessing);
     // Assert question mapped correctly to ExtendedTextInteraction
     $this->assertTrue($interaction instanceof ExtendedTextInteraction);
     $this->assertEquals($questionReference, $interaction->getResponseIdentifier());
     $this->assertEquals($questionReference, $interaction->getLabel());
     $this->assertEquals($stimulus, QtiMarshallerUtil::marshallCollection($interaction->getPrompt()->getComponents()));
     $this->assertEquals($placeholder, $interaction->getPlaceholderText());
     // Assert question mapped correctly with default values
     $this->assertEquals(TextFormat::PLAIN, $interaction->getFormat());
     $this->assertEquals(1, $interaction->getMinStrings());
     $this->assertEquals(1, $interaction->getMaxStrings());
 }
 protected function convertStimulusForPrompt($stimulusString)
 {
     $stimulusComponents = QtiMarshallerUtil::unmarshallElement($stimulusString);
     $prompt = new Prompt();
     $prompt->setContent(ContentCollectionBuilder::buildFlowStaticCollectionContent($stimulusComponents));
     return $prompt;
 }
 public function convert(BaseQuestionType $questionType, $interactionIdentifier, $interactionLabel)
 {
     //TODO: Need validation a question shall have at least 1 {{response}} and 1 item in `possible_responses`
     /** @var clozeassociation $question */
     $question = $questionType;
     // Replace {{ response }} with `gap` elements
     $index = 0;
     $template = preg_replace_callback('/{{response}}/', function ($match) use(&$index) {
         $gapIdentifier = self::GAP_IDENTIFIER_PREFIX . $index;
         $replacement = '<gap identifier="' . $gapIdentifier . '"/>';
         $index++;
         return $replacement;
     }, $question->get_template());
     $content = ContentCollectionBuilder::buildBlockStaticCollectionContent(QtiMarshallerUtil::unmarshallElement($template));
     // Map `possible_responses` to gaps
     // TODO: Detect `img`
     $gapChoices = new GapChoiceCollection();
     $possibleResponses = $question->get_possible_responses();
     $matchMax = $question->get_duplicate_responses() ? count($possibleResponses) : 1;
     foreach ($possibleResponses as $index => $possibleResponse) {
         $gapChoice = new GapText(self::GAPCHOICE_IDENTIFIER_PREFIX . $index, $matchMax);
         $gapChoiceContent = new TextOrVariableCollection();
         $gapChoiceContent->attach(new TextRun($possibleResponse));
         $gapChoice->setContent($gapChoiceContent);
         $gapChoices->attach($gapChoice);
     }
     $interaction = new GapMatchInteraction($interactionIdentifier, $gapChoices, $content);
     $interaction->setLabel($interactionLabel);
     $interaction->setPrompt($this->convertStimulusForPrompt($question->get_stimulus()));
     $validationBuilder = new ClozeassociationValidationBuilder($possibleResponses);
     list($responseDeclaration, $responseProcessing) = $validationBuilder->buildValidation($interaction->getResponseIdentifier(), $question->get_validation());
     return [$interaction, $responseDeclaration, $responseProcessing];
 }
 public function testSimpleCaseWithNoValidation()
 {
     $stimulus = '<strong>Where is Learnosity office in Australia located?</strong>';
     $options = ['ChoiceA' => 'Melbourne', 'ChoiceB' => 'Sydney', 'ChoiceC' => 'Jakarta'];
     $question = $this->buildMcq($options);
     $question->set_stimulus($stimulus);
     $mcqMapper = new McqMapper();
     /** @var ChoiceInteraction $interaction */
     list($interaction, $responseDeclaration, $responseProcessing) = $mcqMapper->convert($question, 'testIdentifier', 'testIdentifierLabel');
     // Check usual
     $this->assertTrue($interaction instanceof ChoiceInteraction);
     $this->assertEquals('testIdentifier', $interaction->getResponseIdentifier());
     $this->assertEquals('testIdentifierLabel', $interaction->getLabel());
     $this->assertEquals($stimulus, QtiMarshallerUtil::marshallCollection($interaction->getPrompt()->getComponents()));
     $this->assertNull($responseDeclaration);
     $this->assertNull($responseProcessing);
     // Check `options` mapping are correct
     /** @var SimpleChoice[] $choices */
     $choices = $interaction->getSimpleChoices()->getArrayCopy(true);
     $this->assertEquals('ChoiceA', $choices[0]->getIdentifier());
     $this->assertEquals('Melbourne', QtiMarshallerUtil::marshallCollection($choices[0]->getContent()));
     $this->assertEquals('ChoiceB', $choices[1]->getIdentifier());
     $this->assertEquals('Sydney', QtiMarshallerUtil::marshallCollection($choices[1]->getContent()));
     $this->assertEquals('ChoiceC', $choices[2]->getIdentifier());
     $this->assertEquals('Jakarta', QtiMarshallerUtil::marshallCollection($choices[2]->getContent()));
     // Check the default values are correct
     $this->assertEquals(1, $interaction->getMaxChoices());
     $this->assertEquals(1, $interaction->getMinChoices());
     $this->assertEquals(Orientation::VERTICAL, $interaction->getOrientation());
 }
 public function convert(BaseQuestionType $questionType, $interactionIdentifier, $interactionLabel)
 {
     /** @var orderlist $question */
     $question = $questionType;
     $simpleChoiceCollection = new SimpleChoiceCollection();
     $indexIdentifiersMap = [];
     foreach ($question->get_list() as $key => $item) {
         $simpleChoice = new SimpleChoice('CHOICE_' . $key);
         $choiceContent = new FlowStaticCollection();
         foreach (QtiMarshallerUtil::unmarshallElement($item) as $component) {
             $choiceContent->attach($component);
         }
         $simpleChoice->setContent($choiceContent);
         $simpleChoiceCollection->attach($simpleChoice);
         $indexIdentifiersMap[$key] = $simpleChoice->getIdentifier();
     }
     $interaction = new OrderInteraction($interactionIdentifier, $simpleChoiceCollection);
     $interaction->setLabel($interactionLabel);
     $interaction->setPrompt($this->convertStimulusForPrompt($question->get_stimulus()));
     $interaction->setShuffle(false);
     $interaction->setOrientation(Orientation::VERTICAL);
     $builder = new OrderlistValidationBuilder($indexIdentifiersMap);
     list($responseDeclaration, $responseProcessing) = $builder->buildValidation($interactionIdentifier, $question->get_validation());
     return [$interaction, $responseDeclaration, $responseProcessing];
 }
 public function testSimpleCase()
 {
     $data = json_decode($this->getFixtureFileContents('learnosityjsons/orderlist.json'), true);
     $assessmentItem = $this->convertToAssessmentItem($data);
     /** @var OrderInteraction $interaction */
     $interaction = $assessmentItem->getComponentsByClassName('orderInteraction', true)->getArrayCopy()[0];
     $this->assertTrue($interaction instanceof OrderInteraction);
     // And its prompt is mapped correctly
     $promptString = QtiMarshallerUtil::marshallCollection($interaction->getPrompt()->getComponents());
     $this->assertEquals('<p>[This is the stem.]</p>', $promptString);
     // Assert its choices are correct
     /** @var SimpleChoice[] $simpleChoices */
     $simpleChoices = $interaction->getSimpleChoices()->getArrayCopy(true);
     $this->assertEquals('CHOICE_0', $simpleChoices[0]->getIdentifier());
     $this->assertEquals('[Choice A]', QtiMarshallerUtil::marshallCollection($simpleChoices[0]->getComponents()));
     $this->assertEquals('CHOICE_1', $simpleChoices[1]->getIdentifier());
     $this->assertEquals('[Choice B]', QtiMarshallerUtil::marshallCollection($simpleChoices[1]->getComponents()));
     $this->assertEquals('CHOICE_2', $simpleChoices[2]->getIdentifier());
     $this->assertEquals('[Choice C]', QtiMarshallerUtil::marshallCollection($simpleChoices[2]->getComponents()));
     $this->assertEquals('CHOICE_3', $simpleChoices[3]->getIdentifier());
     $this->assertEquals('[Choice D]', QtiMarshallerUtil::marshallCollection($simpleChoices[3]->getComponents()));
     // Also assert its validation is match_correct and correct
     /** @var ResponseDeclaration $responseDeclaration */
     $responseDeclaration = $assessmentItem->getResponseDeclarations()->getArrayCopy()[0];
     $this->assertEquals(Cardinality::ORDERED, $responseDeclaration->getCardinality());
     /** @var Value[] $correctResponseValues */
     $correctResponseValues = $responseDeclaration->getCorrectResponse()->getValues()->getArrayCopy(true);
     $this->assertEquals('CHOICE_3', $correctResponseValues[0]->getValue());
     $this->assertEquals('CHOICE_1', $correctResponseValues[1]->getValue());
     $this->assertEquals('CHOICE_0', $correctResponseValues[2]->getValue());
     $this->assertEquals('CHOICE_2', $correctResponseValues[3]->getValue());
     $this->assertEquals(Constants::RESPONSE_PROCESSING_TEMPLATE_MATCH_CORRECT, $assessmentItem->getResponseProcessing()->getTemplate());
 }
 private function getFeatureReplacementString($node)
 {
     // Process inline feature
     if (isset($node->attr['data-type']) && isset($node->attr['data-src'])) {
         $src = trim($node->attr['data-src']);
         $type = trim($node->attr['data-type']);
         if ($type === 'audioplayer' || $type === 'audioplayer') {
             return QtiMarshallerUtil::marshallValidQti(new Object($src, MimeUtil::guessMimeType(basename($src))));
         }
         // Process regular question feature
     } else {
         $nodeClassAttribute = $node->attr['class'];
         $featureReference = $this->getFeatureReferenceFromClassName($nodeClassAttribute);
         $feature = $this->widgets[$featureReference];
         $type = $feature['data']['type'];
         if ($type === 'audioplayer' || $type === 'audioplayer') {
             $src = $feature['data']['src'];
             $object = new Object($src, MimeUtil::guessMimeType(basename($src)));
             $object->setLabel($featureReference);
             return QtiMarshallerUtil::marshallValidQti($object);
         } else {
             if ($type === 'sharedpassage') {
                 $content = $feature['data']['content'];
                 $object = new Object('', 'text/html');
                 $object->setContent(ContentCollectionBuilder::buildObjectFlowCollectionContent(QtiMarshallerUtil::unmarshallElement($content)));
                 $object->setLabel($featureReference);
                 return QtiMarshallerUtil::marshallValidQti($object);
             }
         }
     }
     throw new MappingException($type . ' not supported');
 }
 public function getPrompt()
 {
     if ($this->interaction->getPrompt() instanceof Prompt) {
         $promptContent = $this->interaction->getPrompt()->getContent();
         return QtiMarshallerUtil::marshallCollection($promptContent);
     }
     return '';
 }
 public function testMappingItemWithSharedPassage()
 {
     $data = json_decode($this->getFixtureFileContents('learnosityjsons/item_mcq_sharedpassage.json'), true);
     $assessmentItem = $this->convertToAssessmentItem($data);
     /** @var Object $object */
     $object = $assessmentItem->getComponentsByClassName('object', true)->getArrayCopy()[0];
     $this->assertEquals('text/html', $object->getType());
     $this->assertEquals('<p>This is the content of my shared passage</p>', QtiMarshallerUtil::marshallCollection($object->getComponents()));
 }
 private function buildTemplate(ItemBody $itemBody, array $interactionXmls)
 {
     // Build item's HTML content
     $content = QtiMarshallerUtil::marshallCollection($itemBody->getComponents());
     foreach ($interactionXmls as $interactionXml) {
         $content = str_replace($interactionXml, '{{response}}', $content);
     }
     return $content;
 }
 private function buildOptions(SimpleChoiceCollection $simpleChoices)
 {
     /* @var $choice SimpleChoice */
     $options = [];
     foreach ($simpleChoices as $key => $choice) {
         // Store 'SimpleChoice' identifier to key for validation purposes
         $options[] = ['label' => QtiMarshallerUtil::marshallCollection($choice->getContent()), 'value' => $choice->getIdentifier()];
     }
     return $options;
 }
 protected function buildPossibleResponseMapping(QtiGraphicGapMatchInteraction $interaction)
 {
     $possibleResponseMapping = [];
     $gapChoices = $interaction->getGapImgs();
     /** @var GapChoice $gapChoice */
     foreach ($gapChoices as $gapChoice) {
         $gapChoiceContent = QtiMarshallerUtil::marshallCollection($gapChoice->getComponents());
         $possibleResponseMapping[$gapChoice->getIdentifier()] = $gapChoiceContent;
     }
     return $possibleResponseMapping;
 }
 private function buildOptions(SimpleMatchSet $simpleMatchSet, &$mapping)
 {
     $options = [];
     $choiceCollection = $simpleMatchSet->getSimpleAssociableChoices();
     /** @var SimpleAssociableChoice $choice */
     foreach ($choiceCollection as $choice) {
         $contentStr = QtiMarshallerUtil::marshallCollection($choice->getContent());
         $options[] = $contentStr;
         $mapping[$choice->getIdentifier()] = count($options) - 1;
     }
     return $options;
 }
 public function testSimpleCase()
 {
     $data = json_decode($this->getFixtureFileContents('learnosityjsons/choicematrix.json'), true);
     $assessmentItem = $this->convertToAssessmentItem($data);
     /** @var MatchInteraction $interaction */
     $interaction = $assessmentItem->getComponentsByClassName('matchInteraction', true)->getArrayCopy()[0];
     $this->assertTrue($interaction instanceof MatchInteraction);
     // And its prompt is mapped correctly
     $promptString = QtiMarshallerUtil::marshallCollection($interaction->getPrompt()->getComponents());
     $this->assertEquals('<p>[This is the stem.]</p>', $promptString);
     // Assert its source choices (stems)
     /** @var SimpleAssociableChoice[] $stemAssociableChoices */
     $stemAssociableChoices = $interaction->getSourceChoices()->getSimpleAssociableChoices()->getArrayCopy(true);
     $this->assertEquals('[Stem 1]', QtiMarshallerUtil::marshallCollection($stemAssociableChoices[0]->getContent()));
     $this->assertEquals('STEM_0', $stemAssociableChoices[0]->getIdentifier());
     $this->assertEquals('[Stem 2]', QtiMarshallerUtil::marshallCollection($stemAssociableChoices[1]->getContent()));
     $this->assertEquals('STEM_1', $stemAssociableChoices[1]->getIdentifier());
     $this->assertEquals('[Stem 3]', QtiMarshallerUtil::marshallCollection($stemAssociableChoices[2]->getContent()));
     $this->assertEquals('STEM_2', $stemAssociableChoices[2]->getIdentifier());
     $this->assertEquals('[Stem 4]', QtiMarshallerUtil::marshallCollection($stemAssociableChoices[3]->getContent()));
     $this->assertEquals('STEM_3', $stemAssociableChoices[3]->getIdentifier());
     foreach ($stemAssociableChoices as $choice) {
         $this->assertEquals(1, $choice->getMatchMax());
         $this->assertEquals(1, $choice->getMatchMin());
     }
     // Assert its target choices (options)
     /** @var SimpleAssociableChoice[] $optionAssociableChoices */
     $optionAssociableChoices = $interaction->getTargetChoices()->getSimpleAssociableChoices()->getArrayCopy(true);
     $this->assertEquals('True', QtiMarshallerUtil::marshallCollection($optionAssociableChoices[0]->getContent()));
     $this->assertEquals('OPTION_0', $optionAssociableChoices[0]->getIdentifier());
     $this->assertEquals('False', QtiMarshallerUtil::marshallCollection($optionAssociableChoices[1]->getContent()));
     $this->assertEquals('OPTION_1', $optionAssociableChoices[1]->getIdentifier());
     foreach ($optionAssociableChoices as $choice) {
         $this->assertEquals(4, $choice->getMatchMax());
         $this->assertEquals(1, $choice->getMatchMin());
     }
     // Assert its valdation, woo hooo!
     $this->assertEquals(Constants::RESPONSE_PROCESSING_TEMPLATE_MATCH_CORRECT, $assessmentItem->getResponseProcessing()->getTemplate());
     /** @var ResponseDeclaration $responseDeclaration */
     $responseDeclaration = $assessmentItem->getResponseDeclarations()->getArrayCopy()[0];
     $this->assertEquals(Cardinality::MULTIPLE, $responseDeclaration->getCardinality());
     $this->assertEquals(BaseType::DIRECTED_PAIR, $responseDeclaration->getBaseType());
     /** @var Value[] $values */
     $values = $responseDeclaration->getCorrectResponse()->getValues()->getArrayCopy(true);
     $this->assertDirectPair($values[0]->getValue(), 'STEM_0', 'OPTION_0');
     $this->assertDirectPair($values[1]->getValue(), 'STEM_1', 'OPTION_1');
     $this->assertDirectPair($values[2]->getValue(), 'STEM_2', 'OPTION_1');
     $this->assertDirectPair($values[3]->getValue(), 'STEM_3', 'OPTION_0');
     $this->assertNull($responseDeclaration->getMapping());
 }
 public function getQuestionType()
 {
     /** @var HotspotInteraction $interaction */
     $interaction = $this->interaction;
     $imageObject = $interaction->getObject();
     // Yes, width and height is necessary unfortunately
     if ($imageObject->getHeight() < 0 || $imageObject->getWidth() < 0) {
         throw new MappingException('Hotspot interaction image object need to specifiy both width and height for conversion');
     }
     // Slab the image object
     $hotspot = new hotspot('hotspot');
     $hotspot->set_image($this->buildHotspotImage($imageObject));
     // Support mapping for <prompt>
     if ($interaction->getPrompt() instanceof Prompt) {
         $promptContent = $interaction->getPrompt()->getContent();
         $hotspot->set_stimulus(QtiMarshallerUtil::marshallCollection($promptContent));
     }
     // Map the hotspot areas
     $areas = $this->buildAreas($interaction->getHotspotChoices(), $imageObject);
     $hotspot->set_areas($areas);
     // Setup the area attributes with assumption
     // TODO: Let's say the default fill is always clear, and the stroke would be blackish
     $globalAttributes = new hotspot_area_attributes_global();
     $globalAttributes->set_fill("rgba(0,0,0,0)");
     $globalAttributes->set_stroke("rgba(25, 90, 107, 0.5)");
     $areaAttributes = new hotspot_area_attributes();
     $areaAttributes->set_global($globalAttributes);
     $hotspot->set_area_attributes($areaAttributes);
     // Partial support for @maxChoices
     // @maxChoices of 0 or more than 1 would then would set `multiple_responses` to true
     $maxChoices = $interaction->getMaxChoices();
     if ($maxChoices !== 1) {
         if ($maxChoices !== 0 && $maxChoices !== count($areas)) {
             // We do not support specifying amount of areas
             LogService::log("Allowing multiple responses of max " . count($areas) . " options, however " . "maxChoices of {$maxChoices} would be ignored since we can't support exact number");
         }
         $hotspot->set_multiple_responses(true);
     }
     // Ignoring @minChoices
     if (!empty($interaction->getMinChoices())) {
         LogService::log('Attribute minChoices is not supported. Thus, ignored');
     }
     // Build validation
     $validationBuilder = new HotspotInteractionValidationBuilder($this->responseDeclaration, $areas, $maxChoices);
     $validation = $validationBuilder->buildValidation($this->responseProcessingTemplate);
     if (!empty($validation)) {
         $hotspot->set_validation($validation);
     }
     return $hotspot;
 }
 private function buildOptionCollection(choicematrix $question, $stemCount)
 {
     $optionIndexIdentifierMap = [];
     $optionCollection = new SimpleAssociableChoiceCollection();
     foreach ($question->get_options() as $key => $optionValue) {
         // Learnosity's `choicematrix` always have its options to have any number of associable choice, thus setting to stems count
         // Same as above, won't validate upon empty response, thus setting match min to 1
         $optionChoice = new SimpleAssociableChoice('OPTION_' . $key, $stemCount);
         $optionChoice->setMatchMin(1);
         $optionChoice->setContent(ContentCollectionBuilder::buildFlowStaticCollectionContent(QtiMarshallerUtil::unmarshallElement($optionValue)));
         $optionCollection->attach($optionChoice);
         $optionIndexIdentifierMap[$key] = $optionChoice->getIdentifier();
     }
     return [$optionCollection, $optionIndexIdentifierMap];
 }
 public function getQuestionType()
 {
     /* @var \qtism\data\content\interactions\InlineChoiceInteraction $interaction */
     $interaction = $this->validateInteraction($this->interaction);
     $template = '{{response}}';
     foreach ($interaction->getContent() as $inlineChoice) {
         $this->choicesMapping[$inlineChoice->getIdentifier()] = QtiMarshallerUtil::marshallCollection($inlineChoice->getContent());
     }
     $question = new clozedropdown('clozedropdown', $template, [array_values($this->choicesMapping)]);
     $validation = $this->buildValidation();
     if ($validation) {
         $question->set_validation($validation);
     }
     return $question;
 }
 public function testSimpleCase()
 {
     /** @var AssessmentItem $assessmentItem */
     $question = json_decode($this->getFixtureFileContents('learnosityjsons/data_clozetext.json'), true);
     $assessmentItem = $this->convertToAssessmentItem($question);
     $interactions = $assessmentItem->getComponentsByClassName('textEntryInteraction', true)->getArrayCopy();
     /** @var TextEntryInteraction $interactionOne */
     $interactionOne = $interactions[0];
     /** @var TextEntryInteraction $interactionTwo */
     $interactionTwo = $interactions[1];
     $this->assertTrue($interactionOne instanceof TextEntryInteraction);
     $this->assertTrue($interactionTwo instanceof TextEntryInteraction);
     $this->assertEquals(15, $interactionOne->getExpectedLength());
     $this->assertEquals(15, $interactionTwo->getExpectedLength());
     $content = QtiMarshallerUtil::marshallCollection($assessmentItem->getItemBody()->getContent());
     $this->assertNotEmpty($content);
     // Assert response declarations
     $responseDeclarations = $assessmentItem->getResponseDeclarations()->getArrayCopy();
     /** @var ResponseDeclaration $responseDeclarationOne */
     $responseDeclarationOne = $responseDeclarations[0];
     /** @var ResponseDeclaration $responseDeclarationTwo */
     $responseDeclarationTwo = $responseDeclarations[1];
     // Check has the correct identifiers
     $this->assertEquals($responseDeclarationOne->getIdentifier(), $interactionOne->getResponseIdentifier());
     $this->assertEquals($responseDeclarationTwo->getIdentifier(), $interactionTwo->getResponseIdentifier());
     // Also correct `correctResponse` values
     $this->assertEquals('responseone', $responseDeclarationOne->getCorrectResponse()->getValues()->getArrayCopy()[0]->getValue());
     $this->assertEquals('otherresponseone', $responseDeclarationOne->getCorrectResponse()->getValues()->getArrayCopy()[1]->getValue());
     $this->assertEquals('anotherresponseone', $responseDeclarationOne->getCorrectResponse()->getValues()->getArrayCopy()[2]->getValue());
     $this->assertEquals('responsetwo', $responseDeclarationTwo->getCorrectResponse()->getValues()->getArrayCopy()[0]->getValue());
     $this->assertEquals('otherresponsetwo', $responseDeclarationTwo->getCorrectResponse()->getValues()->getArrayCopy()[1]->getValue());
     $this->assertEquals('anotherresponsetwo', $responseDeclarationTwo->getCorrectResponse()->getValues()->getArrayCopy()[2]->getValue());
     // Also correct `mapping` entries
     $this->assertEquals('responseone', $responseDeclarationOne->getMapping()->getMapEntries()->getArrayCopy()[0]->getMapKey());
     $this->assertEquals(3, $responseDeclarationOne->getMapping()->getMapEntries()->getArrayCopy()[0]->getMappedValue());
     $this->assertEquals('otherresponseone', $responseDeclarationOne->getMapping()->getMapEntries()->getArrayCopy()[1]->getMapKey());
     $this->assertEquals(2, $responseDeclarationOne->getMapping()->getMapEntries()->getArrayCopy()[1]->getMappedValue());
     $this->assertEquals('anotherresponseone', $responseDeclarationOne->getMapping()->getMapEntries()->getArrayCopy()[2]->getMapKey());
     $this->assertEquals(1, $responseDeclarationOne->getMapping()->getMapEntries()->getArrayCopy()[2]->getMappedValue());
     $this->assertEquals('responsetwo', $responseDeclarationTwo->getMapping()->getMapEntries()->getArrayCopy()[0]->getMapKey());
     $this->assertEquals(3, $responseDeclarationTwo->getMapping()->getMapEntries()->getArrayCopy()[0]->getMappedValue());
     $this->assertEquals('otherresponsetwo', $responseDeclarationTwo->getMapping()->getMapEntries()->getArrayCopy()[1]->getMapKey());
     $this->assertEquals(2, $responseDeclarationTwo->getMapping()->getMapEntries()->getArrayCopy()[1]->getMappedValue());
     $this->assertEquals('anotherresponsetwo', $responseDeclarationTwo->getMapping()->getMapEntries()->getArrayCopy()[2]->getMapKey());
     $this->assertEquals(1, $responseDeclarationTwo->getMapping()->getMapEntries()->getArrayCopy()[2]->getMappedValue());
     // Assert response processing template
     $this->assertEquals(Constants::RESPONSE_PROCESSING_TEMPLATE_MAP_RESPONSE, $assessmentItem->getResponseProcessing()->getTemplate());
 }
 public function map($itemReference, ItemBody $itemBody, QtiComponentCollection $interactionComponents, QtiComponentCollection $responseDeclarations = null, ResponseProcessingTemplate $responseProcessingTemplate = null)
 {
     $this->itemReference = $itemReference;
     $questionsXmls = [];
     $responseDeclarationsMap = [];
     if ($responseDeclarations) {
         /** @var ResponseDeclaration $responseDeclaration */
         foreach ($responseDeclarations as $responseDeclaration) {
             $responseDeclarationsMap[$responseDeclaration->getIdentifier()] = $responseDeclaration;
         }
     }
     foreach ($interactionComponents as $component) {
         /* @var $component Interaction */
         $questionReference = $this->itemReference . '_' . $component->getResponseIdentifier();
         // Process <responseDeclaration>
         $responseDeclaration = isset($responseDeclarationsMap[$component->getResponseIdentifier()]) ? $responseDeclarationsMap[$component->getResponseIdentifier()] : null;
         $mapper = $this->getMapperInstance($component->getQtiClassName(), [$component, $responseDeclaration, $responseProcessingTemplate]);
         $question = $mapper->getQuestionType();
         $this->questions[$questionReference] = new Question($question->get_type(), $questionReference, $question);
         $questionsXmls[$questionReference] = ['qtiClassName' => $component->getQtiClassName(), 'responseIdentifier' => $component->getResponseIdentifier()];
     }
     // Build item's HTML content
     $extraContentHtml = new SimpleHtmlDom();
     if (!$extraContentHtml->load(QtiMarshallerUtil::marshallCollection($itemBody->getComponents()), false)) {
         throw new \Exception('Issues with the content for itemBody, it might not be valid');
     }
     foreach ($questionsXmls as $questionReference => $interactionData) {
         // Append this question span to our `item` content as it is
         $this->content .= '<span class="learnosity-response question-' . $questionReference . '"></span>';
         // Clean up interaction HTML content
         $qtiClassName = $interactionData['qtiClassName'];
         $responseIdentifier = $interactionData['responseIdentifier'];
         $toFind = $qtiClassName . '[responseIdentifier="' . $responseIdentifier . '"]';
         foreach ($extraContentHtml->find($toFind) as &$tag) {
             $tag->outertext = '';
         }
     }
     $extraContent = $extraContentHtml->save();
     // Making assumption question always has stimulus `right`?
     // So, prepend the extra content on the stimulus on the first question
     if (!empty(trim($extraContent))) {
         $firstQuestionReference = key($this->questions);
         $newStimulus = $extraContent . $this->questions[$firstQuestionReference]->get_data()->get_stimulus();
         $this->questions[$firstQuestionReference]->get_data()->set_stimulus($newStimulus);
         LogService::log('Extra <itemBody> content is prepended to question stimulus and please verify as this `might` break item content structure');
     }
     return true;
 }
 public function testSimpleCommonCase()
 {
     /** @var AssessmentItem $assessmentItem */
     $question = json_decode($this->getFixtureFileContents('learnosityjsons/data_imageclozeassociation.json'), true);
     $assessmentItem = $this->convertToAssessmentItem($question);
     /** @var GraphicGapMatchInteraction $interaction */
     $interaction = $assessmentItem->getComponentsByClassName('graphicGapMatchInteraction', true)->getArrayCopy()[0];
     $this->assertTrue($interaction instanceof GraphicGapMatchInteraction);
     // And its prompt is mapped correctly
     $promptString = QtiMarshallerUtil::marshallCollection($interaction->getPrompt()->getComponents());
     $this->assertEquals('<p>[This is the stem.]</p>', $promptString);
     // And its gapimages mapped well
     /** @var GapImg[] $gapImages */
     $gapImages = $interaction->getGapImgs()->getArrayCopy();
     $this->assertEquals(3, count($gapImages));
     $this->assertEquals('CHOICE_0', $gapImages[0]->getIdentifier());
     $object0 = $gapImages[0]->getComponents()->current();
     $this->assertEquals('image/png', $object0->getType());
     $this->assertEquals(56, $object0->getWidth());
     $this->assertEquals(13, $object0->getHeight());
     $this->assertEquals('CHOICE_1', $gapImages[1]->getIdentifier());
     $object1 = $gapImages[1]->getComponents()->current();
     $this->assertEquals('image/png', $object1->getType());
     $this->assertEquals(56, $object1->getWidth());
     $this->assertEquals(13, $object1->getHeight());
     $this->assertEquals('CHOICE_2', $gapImages[2]->getIdentifier());
     $object2 = $gapImages[2]->getComponents()->current();
     $this->assertEquals('image/png', $object2->getType());
     $this->assertEquals(100, $object2->getWidth());
     $this->assertEquals(100, $object2->getHeight());
     // And its associableHotspot
     // TODO: Do more through assert with coords and matchmax/matchmin check
     $this->assertEquals(3, $interaction->getAssociableHotspots()->count());
     // And its response processing and response declaration
     $this->assertEquals(Constants::RESPONSE_PROCESSING_TEMPLATE_MATCH_CORRECT, $assessmentItem->getResponseProcessing()->getTemplate());
     /** @var ResponseDeclaration $responseDeclaration */
     $responseDeclaration = $assessmentItem->getResponseDeclarations()->getArrayCopy()[0];
     $this->assertEquals(Cardinality::MULTIPLE, $responseDeclaration->getCardinality());
     $this->assertEquals(BaseType::DIRECTED_PAIR, $responseDeclaration->getBaseType());
     /** @var Value[] $values */
     $values = $responseDeclaration->getCorrectResponse()->getValues()->getArrayCopy(true);
     $this->assertDirectPair($values[0]->getValue(), 'ASSOCIABLEHOTSPOT_0', 'CHOICE_2');
     $this->assertDirectPair($values[1]->getValue(), 'ASSOCIABLEHOTSPOT_1', 'CHOICE_1');
     $this->assertDirectPair($values[2]->getValue(), 'ASSOCIABLEHOTSPOT_2', 'CHOICE_0');
     // And, we don't have mapping because we simply won't
     $this->assertEquals(null, $responseDeclaration->getMapping());
 }
Example #28
0
 public function convert(BaseQuestionType $questionType, $interactionIdentifier, $interactionLabel)
 {
     /** @var mcq $question */
     $question = $questionType;
     // Build <choiceInteraction>
     $valueIdentifierMap = [];
     $simpleChoiceCollection = new SimpleChoiceCollection();
     foreach ($question->get_options() as $index => $option) {
         /** @var mcq_options_item $option */
         $choiceContent = new FlowStaticCollection();
         foreach (QtiMarshallerUtil::unmarshallElement($option->get_label()) as $component) {
             $choiceContent->attach($component);
         }
         // Use option['value'] as choice `identifier` if it has the correct format,
         // Otherwise, generate a valid using index such `CHOICE_1`, `CHOICE_2`, etc
         $originalOptionValue = $option->get_value();
         $choiceIdentifier = Format::isIdentifier($originalOptionValue, false) ? $originalOptionValue : 'CHOICE_' . $index;
         // Store this reference in a map
         $valueIdentifierMap[$originalOptionValue] = $choiceIdentifier;
         $choice = new SimpleChoice($choiceIdentifier);
         $choice->setContent($choiceContent);
         $simpleChoiceCollection->attach($choice);
     }
     // Build final interaction and its corresponding <responseDeclaration>, and its <responseProcessingTemplate>
     $interaction = new ChoiceInteraction($interactionIdentifier, $simpleChoiceCollection);
     $interaction->setLabel($interactionLabel);
     $interaction->setMinChoices(1);
     $interaction->setMaxChoices($question->get_multiple_responses() ? $simpleChoiceCollection->count() : 1);
     // Build the prompt
     $interaction->setPrompt($this->convertStimulusForPrompt($question->get_stimulus()));
     // Set shuffle options
     $interaction->setShuffle($question->get_shuffle_options() ? true : false);
     // Set the layout
     if ($question->get_ui_style() instanceof mcq_ui_style && $question->get_ui_style()->get_type() === 'horizontal' && intval($question->get_ui_style()->get_columns()) === count($question->get_options())) {
         $interaction->setOrientation(Orientation::HORIZONTAL);
     } else {
         $interaction->setOrientation(Orientation::VERTICAL);
         LogService::log('ui_style` is ignored and `choiceInteraction` is assumed and set as `vertical`');
     }
     if (empty($question->get_validation())) {
         return [$interaction, null, null];
     }
     $builder = new McqValidationBuilder($question->get_multiple_responses(), $valueIdentifierMap);
     list($responseDeclaration, $responseProcessing) = $builder->buildValidation($interactionIdentifier, $question->get_validation());
     return [$interaction, $responseDeclaration, $responseProcessing];
 }
 public function testSimpleWithNoValidation()
 {
     $data = json_decode($this->getFixtureFileContents('learnosityjsons/item_plaintext.json'), true);
     $assessmentItem = $this->convertToAssessmentItem($data);
     // Plaintext shall have no <responseDeclaration> and <responseProcessing>
     $this->assertEquals(0, $assessmentItem->getResponseDeclarations()->count());
     $this->assertNull($assessmentItem->getResponseProcessing());
     // Has <extendedTextInteraction> as the first and only interaction
     /** @var ExtendedTextInteraction $interaction */
     $interaction = $assessmentItem->getComponentsByClassName('extendedTextInteraction', true)->getArrayCopy()[0];
     $this->assertTrue($interaction instanceof ExtendedTextInteraction);
     // And its prompt is mapped correctly
     $promptString = QtiMarshallerUtil::marshallCollection($interaction->getPrompt()->getComponents());
     $this->assertEquals('<p>Write an essay</p>', $promptString);
     // And it is a HTML text by default
     $this->assertEquals(TextFormat::PLAIN, $interaction->getFormat());
 }
 private function buildTemplate(QtiGapMatchInteraction $interaction)
 {
     $templateCollection = new QtiComponentCollection();
     foreach ($interaction->getComponents() as $component) {
         // Ignore `prompt` and the `gapChoice` since they are going to be mapped somewhere else :)
         if (!$component instanceof Prompt && !$component instanceof GapChoice) {
             $templateCollection->attach($component);
         }
     }
     $gapIdentifiers = [];
     $content = QtiMarshallerUtil::marshallCollection($templateCollection);
     foreach ($interaction->getComponentsByClassName('gap', true) as $gap) {
         /** @var Gap $gap */
         $gapIdentifiers[] = $gap->getIdentifier();
         $gapString = QtiMarshallerUtil::marshall($gap);
         $content = str_replace($gapString, '{{response}}', $content);
     }
     return [$content, $gapIdentifiers];
 }