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()); }
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]; }