public function parse($xmlString, $validate = true) { // TODO: Remove this, and move it higher up LogService::flush(); $xmlDocument = new XmlDocument(); if ($validate === false) { LogService::log('QTI pre-validation is turned off, some invalid attributes might be stripped from XML content upon conversion'); } $xmlDocument->loadFromString($xmlString, $validate); /** @var AssessmentItem $assessmentItem */ $assessmentTest = $xmlDocument->getDocumentComponent(); if (!$assessmentTest instanceof AssessmentTest) { throw new MappingException('XML is not a valid <assessmentItem> document'); } // Ignore `testPart` and `assessmentSection`. Grab every item references and merge in array $itemReferences = []; foreach ($assessmentTest->getComponentsByClassName('assessmentItemRef', true) as $assessmentItemRef) { $itemReferences[] = $assessmentItemRef->getIdentifier(); } LogService::log('Support for mapping is very limited. Elements such `testPart`, `assessmentSections`, `seclection`, `rubricBlock`, ' . 'etc are ignored. Please see developer docs for more details'); $data = new activity_data(); $data->set_items($itemReferences); $activity = new activity($assessmentTest->getIdentifier(), $data); // Flush out all the error messages stored in this static class, also ensure they are unique $messages = array_values(array_unique(LogService::flush())); return [$activity, $messages]; }
protected function buildResponseProcessing($validation, $isCaseSensitive = true) { // Guess question type $validationClazz = new \ReflectionClass($validation); $questionType = str_replace('_validation', '', $validationClazz->getShortName()); if (in_array($questionType, Constants::$questionTypesWithMappingSupport)) { $responseProcessing = new ResponseProcessing(); $responseProcessing->setTemplate(Constants::RESPONSE_PROCESSING_TEMPLATE_MAP_RESPONSE); return $responseProcessing; } if ($validation->get_valid_response()->get_score() != 1) { $validation->get_valid_response()->set_score(1); LogService::log('Only support mapping to `matchCorrect` template, thus validation score is changed to 1 and since mapped to QTI pre-defined `match_correct.xml` template'); } // Warn and remove `alt_responses` because couldn't support responseDeclaration with multiple valid answers if (!empty($validation->get_alt_responses())) { $validation->set_alt_responses([]); LogService::log('Does not support multiple validation responses for `responseDeclaration`, only use `valid_response`, ignoring `alt_responses`'); } // Warn since we only support match_correct, can't support `$isCaseSensitive` if ($isCaseSensitive == false) { LogService::log('Only support mapping to `matchCorrect` template, thus case sensitivity is ignored'); } $responseProcessing = new ResponseProcessing(); $responseProcessing->setTemplate(Constants::RESPONSE_PROCESSING_TEMPLATE_MATCH_CORRECT); return $responseProcessing; }
public function getQuestionType() { /* @var QtiMatchInteraction $interaction */ $interaction = $this->interaction; if ($interaction->mustShuffle()) { LogService::log('Shuffle attribute is not supported'); } $simpleMatchSetCollection = $interaction->getSimpleMatchSets(); $stems = $this->buildOptions($simpleMatchSetCollection[0], $this->stemMapping); $options = $this->buildOptions($simpleMatchSetCollection[1], $this->optionsMapping); // Build validation $validationBuilder = new MatchInteractionValidationBuilder($this->stemMapping, $this->optionsMapping, $this->responseDeclaration); $validation = $validationBuilder->buildValidation($this->responseProcessingTemplate); if ($interaction->getMaxAssociations() !== count($stems)) { LogService::log('Max Association number not equals to number of stems is not supported'); } $uiStyle = new choicematrix_ui_style(); $uiStyle->set_type('table'); $isMultipleResponse = $this->isMultipleResponse($interaction); $question = new choicematrix('choicematrix', $options, $stems); $question->set_multiple_responses($isMultipleResponse); $question->set_stimulus($this->getPrompt()); $question->set_ui_style($uiStyle); if ($validation) { $question->set_validation($validation); } return $question; }
public function buildValidation(ResponseProcessingTemplate $responseProcessingTemplate) { try { switch ($responseProcessingTemplate->getTemplate()) { case ResponseProcessingTemplate::MATCH_CORRECT: return $this->getMatchCorrectTemplateValidation(); case ResponseProcessingTemplate::MAP_RESPONSE: case ResponseProcessingTemplate::CC2_MAP_RESPONSE: return $this->getMapResponseTemplateValidation(); case ResponseProcessingTemplate::NONE: if (!empty($this->responseDeclaration)) { // If the response processing template is not set, simply check whether `mapping` or `correctResponse` exists and // simply use `em if (!empty($this->responseDeclaration->getMapping()) && $this->responseDeclaration->getMapping()->getMapEntries()->count() > 0) { LogService::log('Response processing is not set, the `validation` object is assumed to be mapped based on `mapping` map entries elements'); return $this->getMapResponseTemplateValidation(); } if (!empty($this->responseDeclaration->getCorrectResponse()) && $this->responseDeclaration->getCorrectResponse()->getValues()->count() > 0) { LogService::log('Response processing is not set, the `validation` object is assumed to be mapped based on `correctResponse` values elements'); return $this->getMatchCorrectTemplateValidation(); } } return $this->getNoTemplateResponsesValidation(); default: LogService::log('Unrecognised response processing template. Validation is not available'); } } catch (MappingException $e) { LogService::log('Validation is not available. Critical error: ' . $e->getMessage()); } return null; }
public function buildItemBody(array $interactions, $content = '') { // Try to build the <itemBody> according to items` content if exists if (empty($content)) { return $this->buildItemBodySimple($interactions); } try { return $this->buildItemBodyWithItemContent($interactions, $content); // If anything fails, <itemBody> can't be mapped due to whatever reasons // Probably simply due to its being wrapped in a tag which only accept inline content // Simply build it without considering items` content and put the content on the top } catch (\Exception $e) { $itemBody = $this->buildItemBodySimple($interactions); $itemBodyContent = new BlockCollection(); // Build the div bundle that contains all the item`s content // minus those questions and features `span` $html = new SimpleHtmlDom(); $html->load($content); foreach ($html->find('span.learnosity-response') as &$span) { $span->outertext = ''; } $div = new Div(); $contentCollection = QtiMarshallerUtil::unmarshallElement($html->save()); $div->setContent(ContentCollectionBuilder::buildFlowCollectionContent($contentCollection)); $itemBodyContent->attach($div); $itemBodyContent->merge($itemBody->getComponents()); $itemBody->setContent($itemBodyContent); LogService::log('Interactions are failed to be mapped with `item` content: ' . $e->getMessage() . '. Thus, interactions are separated from its actual `item` content and appended in the bottom'); return $itemBody; } }
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 static function build($questionType, $scoringType, array $responses) { // Validate the `responses` array foreach ($responses as $response) { if (!$response instanceof ValidResponse) { throw new \Exception('Invalid `responses` array. Fail to build validation'); } } // Filter out negative values $responses = array_filter($responses, function ($response) { /** @var ValidResponse $response */ $isPositiveValue = floatval($response->getScore()) >= 0; if (!$isPositiveValue) { LogService::log('Ignored validation mapping with negative score'); } return $isPositiveValue; }); // Sort by score value, as the one with highest score would be used for `valid_response` object self::susort($responses, function ($a, $b) { /** * @var ValidResponse $a * @var ValidResponse $b */ if ($a->getScore() == $b->getScore()) { return 0; } return $a->getScore() > $b->getScore() ? -1 : 1; }); // Build `valid_response` and its `alt_responses` $validResponse = null; $altResponses = []; foreach ($responses as $response) { /** @var ValidResponse $response */ if (!$validResponse) { $validResponseRef = new \ReflectionClass(self::BASE_NS . $questionType . '_validation_valid_response'); $validResponse = $validResponseRef->newInstance(); $validResponse->set_score($response->getScore()); $validResponse->set_value($response->getValue()); } else { $altResponseItemRef = new \ReflectionClass(self::BASE_NS . $questionType . '_validation_alt_responses_item'); $altResponseItem = $altResponseItemRef->newInstance(); $altResponseItem->set_score($response->getScore()); $altResponseItem->set_value($response->getValue()); $altResponses[] = $altResponseItem; } } // Build dah` validation object $validationRef = new \ReflectionClass(self::BASE_NS . $questionType . '_validation'); $validation = $validationRef->newInstance(); $validation->set_scoring_type($scoringType); if (!empty($validResponse)) { $validation->set_valid_response($validResponse); } if (!empty($altResponses)) { $validation->set_alt_responses($altResponses); } return $validation; }
private function validateInteraction(InlineChoiceInteraction $interaction) { if (!empty($interaction->mustShuffle())) { LogService::log('The attribute `shuffle` is not supported, thus is ignored'); } if (!empty($interaction->isRequired())) { LogService::log('The attribute `required` is not supported, thus is ignored'); } return $interaction; }
public function testShouldNotHandleMapResponseValidation() { $testOrderInteraction = OrderInteractionBuilder::buildOrderInteraction('testOrderInteraction', ['A' => 'Order A', 'B' => 'Order B', 'C' => 'Order C'], 'testPrompt'); $responseProcessingTemplate = ResponseProcessingTemplate::mapResponse(); $validResponseIdentifier = ['A' => [1, false], 'B' => [2, false], 'C' => [3, false]]; $responseDeclaration = ResponseDeclarationBuilder::buildWithMapping('testIdentifier', $validResponseIdentifier); $mapper = new OrderInteractionMapper($testOrderInteraction, $responseDeclaration, $responseProcessingTemplate); $mapper->getQuestionType(); $this->assertCount(1, LogService::read()); }
public function convert(Manifest $manifest, array $rules = []) { $activityReference = $manifest->getIdentifier(); $tagsWriter = new TagsWriter(); // Does not handle submanifest tyvm! if (!empty($manifest->getManifest())) { LogService::log('Does not handle sub-manifest element thus it is ignored'); } // Atm, we only have tags rules and this need to be validated and user shall be provided with a nice error message // TODO: Validation need to be done in future $tagRules = isset($rules['tags']) ? $rules['tags'] : []; // Let's map package metadatas as activity tags // We can write custom replacer or remover to fix the messy `identifier:catalog:` afterwards $activityTags = []; $metadatas = $manifest->getMetadata(); if (!empty($metadatas)) { $tags = $tagsWriter->convert($metadatas, $tagRules); if (!empty($tags)) { $activityTags = ['reference' => $activityReference, 'tags' => $tags]; } } $itemReferences = []; $itemsTags = []; // Build item reference and item tags JSON $organisations = $manifest->getOrganizations(); if (!empty($organisations)) { foreach ($organisations as $organisation) { foreach ($organisation->getItems() as $item) { $itemReferences[] = $item->getIdentifier(); } } } // Build item reference and item tags JSON $resources = $manifest->getResources(); if (!empty($resources)) { foreach ($resources as $resource) { // Just add `item` resource as items, and leave css and any other resources alone if (StringUtil::startsWith($resource->getType(), 'imsqti_item')) { /** @var Resource $resource */ $itemReference = $resource->getIdentifier(); $itemReferences[] = $itemReference; $tags = $tagsWriter->convert($resource->getMetadata(), $tagRules); if (!empty($tags)) { $itemsTags[] = ['reference' => $itemReference, 'tags' => $tags]; } } } } // Build activity JSON $activity = ['reference' => $activityReference, 'data' => ['items' => array_values(array_unique($itemReferences))], 'status' => 'published']; // Obvious here that these `items` hasn't and wouldn't be validated against // Should do it later by the function that calls this return [$activity, $activityTags, $itemsTags]; }
public function testSimpleCaseWithInvalidValidation() { $interaction = InlineChoiceInteractionBuilder::buildSimple('testIdentifier', ['doesntmatter' => 'Doesntmatter']); $mapper = new InlineChoiceInteractionMapper($interaction, null, ResponseProcessingTemplate::mapResponsePoint()); $question = $mapper->getQuestionType(); // Should map question correctly with no `validation` object $this->assertNotNull($question); $this->assertEquals('clozedropdown', $question->get_type()); $validation = $question->get_validation(); $this->assertNull($validation); $this->assertTrue(count(LogService::read()) === 1); }
private function validate(QtiOrderInteraction $interaction) { if ($interaction->mustShuffle()) { LogService::log('Attribute `shuffle` is not supported'); } foreach ($interaction->getSimpleChoices() as $simpleChoice) { /** @var SimpleChoice $simpleChoice */ if ($simpleChoice->isFixed()) { LogService::log('Attribute `fixed` for ' . $simpleChoice->getIdentifier() . ' is not supported'); } } return true; }
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 testMapWithMergableInteractionType() { $componentCollection = $this->buildComponentCollectionWithMergableInteractionTypes(); $result = $this->mergedItemBuilder->map('testAssessmentItemIdentifier', ItemBodyBuilder::buildItemBody($componentCollection), $componentCollection); $this->assertTrue($result); $this->assertCount(1, LogService::read()); $questions = $this->mergedItemBuilder->getQuestions(); $this->assertCount(1, $questions); $this->assertInstanceOf('LearnosityQti\\Entities\\Question', $questions[0]); $q = $questions[0]; $this->assertEquals('testAssessmentItemIdentifier_testTextEntryInteractionOne_testTextEntryInteractionTwo', $q->get_reference()); $this->assertEquals('clozetext', $q->get_type()); $qData = $q->get_data(); $this->assertEquals('clozetext', $qData->get_type()); $this->assertTrue(substr_count($qData->get_template(), '{{response}}') === 2); }
protected function getMatchCorrectTemplateValidation() { // TODO: Validate against mismatch possible responses and correct response // Build the `value` object on `valid_response` $values = []; foreach ($this->responseDeclaration->getCorrectResponse()->getValues() as $v) { /** @var Value $v */ $value = $v->getValue(); if (!isset($this->orderMapping[$value])) { LogService::log('Cannot locate ' . $value . ' in responseDeclaration'); continue; } $values[] = $this->orderMapping[$value]; } return ValidationBuilder::build('orderlist', 'exactMatch', [new ValidResponse(1, $values)]); }
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 getQuestionType() { /* @var QtiChoiceInteraction $interaction */ $interaction = $this->interaction; $options = $this->buildOptions($interaction->getSimpleChoices()); $mcq = new mcq('mcq', $options); // Support for @shuffle $mustShuffle = $interaction->mustShuffle(); if ($mustShuffle) { $mcq->set_shuffle_options($mustShuffle); LogService::log('Set shuffle choices as true, however `fixed` attribute would be ignored since we don\'t support partial shuffle'); } // Support for @orientation ('vertical' or 'horizontal') $uiStyle = new mcq_ui_style(); if ($interaction->getOrientation() === Orientation::HORIZONTAL) { $uiStyle->set_type('horizontal'); $uiStyle->set_columns(count($options)); $mcq->set_ui_style($uiStyle); } // Support mapping for <prompt> if ($interaction->getPrompt() instanceof Prompt) { $promptContent = $interaction->getPrompt()->getContent(); $mcq->set_stimulus(QtiMarshallerUtil::marshallCollection($promptContent)); } // Partial support for @maxChoices // @maxChoices of 0 or more than 1 would then map to choicematrix $maxChoices = $interaction->getMaxChoices(); if ($maxChoices !== 1) { if ($maxChoices !== 0 && $maxChoices !== count($options)) { // We do not support specifying amount of choices LogService::log("Allowing multiple responses of max " . count($options) . " options, however " . "maxChoices of {$maxChoices} would be ignored since we can't support exact number"); } $mcq->set_multiple_responses(true); } // Ignoring @minChoices if (!empty($interaction->getMinChoices())) { LogService::log('Attribute minChoices is not supported. Thus, ignored'); } // Build validation $validationBuilder = new ChoiceInteractionValidationBuilder($this->responseDeclaration, array_column($options, 'label', 'value'), $maxChoices); $validation = $validationBuilder->buildValidation($this->responseProcessingTemplate); if (!empty($validation)) { $mcq->set_validation($validation); } return $mcq; }
public function processAssessmentItem(AssessmentItem $assessmentItem) { // TODO: Yea, we ignore rubric but what happen if the rubric is deep inside nested $newCollection = new BlockCollection(); $itemBodyNew = new ItemBody(); /** @var QtiComponent $component */ foreach ($assessmentItem->getItemBody()->getContent() as $key => $component) { if (!$component instanceof RubricBlock) { $newCollection->attach($component); } else { LogService::log('Does not support <rubricBlock>. Ignoring <rubricBlock>'); } } $itemBodyNew->setContent($newCollection); $assessmentItem->setItemBody($itemBodyNew); return $assessmentItem; }
protected function getMatchCorrectTemplateValidation() { $interactionResponses = []; foreach ($this->responseDeclarations as $responseIdentifier => $responseDeclaration) { /** @var ResponseDeclaration $responseDeclaration */ if (!empty($responseDeclaration->getCorrectResponse())) { $correctResponses = $responseDeclaration->getCorrectResponse()->getValues()->getArrayCopy(true); $interactionResponses[] = array_map(function ($value) { /** @var Value $value */ return new ValidResponse(1, [$value->getValue()]); }, $correctResponses); } else { LogService::log('Response declaration has no correct response values. Thus, validation ignored'); } } $responses = ArrayUtil::cartesianProductForResponses($interactionResponses); return ValidationBuilder::build('clozetext', 'exactMatch', $responses); }
public function convert(item $item, array $questions) { // Make sure we clean up the log LogService::flush(); // Try to build the identifier using item `reference` // Otherwise, generate an alternative identifier and store the original reference as `label` $itemReference = $item->get_reference(); $itemIdentifier = Format::isIdentifier($itemReference, false) ? $itemReference : 'ITEM_' . StringUtil::generateRandomString(12); if ($itemReference !== $itemIdentifier) { LogService::log("The item `reference` ({$itemReference}) is not a valid identifier, thus can not be used for `assessmentItem` identifier. " . "Replaced it with randomly generated `{$itemIdentifier}` and stored the original `reference` as `label` attribute"); } $builder = new AssessmentItemBuilder(); $assessmentItem = $builder->build($itemIdentifier, $itemReference, $questions, $item->get_content()); $xml = new XmlDocument(); $xml->setDocumentComponent($assessmentItem); // Flush out all the error messages stored in this static class, also ensure they are unique $messages = array_values(array_unique(LogService::flush())); return [$xml->saveToString(true), $messages]; }
protected function getMapResponseTemplateValidation() { $interactionResponses = []; /** @var ResponseDeclaration $responseDeclaration */ foreach ($this->responseDeclarations as $responseIdentifier => $responseDeclaration) { $responses = []; foreach ($responseDeclaration->getMapping()->getMapEntries()->getArrayCopy(true) as $mapEntry) { /** @var MapEntry $mapEntry */ $responses[] = new ValidResponse($mapEntry->getMappedValue(), [$this->possibleResponses[$responseIdentifier][$mapEntry->getMapKey()]]); // Find out if one of them is case sensitive if (!$mapEntry->isCaseSensitive()) { LogService::log('Could not support `caseSensitive` attribute for this interaction type. This question validation is always case sensitive'); } } $interactionResponses[] = $responses; } $responses = ArrayUtil::cartesianProductForResponses($interactionResponses); return ValidationBuilder::build('clozedropdown', 'exactMatch', $responses); }
private static function populateClassFields($class, $values) { // And, set values magically using setter methods foreach ($values as $key => $value) { if (!method_exists($class, "set_{$key}")) { LogService::log("Ignoring attribute '{$key}'. Invalid key"); continue; } if ($value === null) { LogService::log("Ignoring attribute '{$key}'. Invalid key"); continue; } $setter = new \ReflectionMethod($class, "set_{$key}"); $parameters = []; foreach ($setter->getParameters() as $parameter) { $parameterName = $parameter->getName(); $parameters[$parameterName] = self::buildField($parameter, $values[$parameterName]); } $setter->invokeArgs($class, $parameters); } return $class; }
private function processHtml($content) { $html = new SimpleHtmlDom(); $html->load($content); // Replace <br> with <br />, <img ....> with <img />, etc /** @var array $selfClosingTags ie. `img, br, input, meta, link, hr, base, embed, spacer` */ $selfClosingTags = implode(array_keys($html->getSelfClosingTags()), ', '); foreach ($html->find($selfClosingTags) as &$node) { $node->outertext = rtrim($node->outertext, '>') . '/>'; } // Replace these audioplayer and videoplayer feature with <object> nodes foreach ($html->find('span.learnosity-feature') as &$node) { try { // Replace <span..> with <object..> $replacement = $this->getFeatureReplacementString($node); $node->outertext = $replacement; } catch (MappingException $e) { LogService::log($e->getMessage() . '. Ignoring mapping feature ' . $node->outertext . '`'); } } return $html->save(); }
public function testWithMapResponseValidationMissingAssociableIdentifier() { $bgObject = new Object('http://img.png', 'image/png'); $bgObject->setWidth(100); $bgObject->setHeight(200); $testInteraction = GraphicGapInteractionBuilder::build('testInteraction', $bgObject, ['A' => 'img_A.png', 'B' => 'img_B.png', 'C' => 'img_C.png'], ['G1' => [0, 0, 10, 10], 'G2' => [0, 0, 10, 10]]); $responseProcessingTemplate = ResponseProcessingTemplate::mapResponse(); $responseDeclaration = ResponseDeclarationBuilder::buildWithMapping('testIdentifier', ['A G1' => [1, false]], 'QtiDirectedPair'); $mapper = new GraphicGapMatchInteractionMapper($testInteraction, $responseDeclaration, $responseProcessingTemplate); /** @var imageclozeassociation $q */ $q = $mapper->getQuestionType(); $this->assertEquals('imageclozeassociation', $q->get_type()); $this->assertEquals(['<img src="img_A.png"/>', '<img src="img_B.png"/>', '<img src="img_C.png"/>'], $q->get_possible_responses()); $this->assertFalse($q->get_duplicate_responses()); $this->assertNull($q->get_validation()); $containsWarning = false; foreach (LogService::read() as $message) { if (StringUtil::contains($message, 'Amount of gap identifiers 2 does not match the amount 1 for <responseDeclaration>')) { return true; } } $this->assertTrue($containsWarning); }
protected function getMapResponseTemplateValidation() { $validResponses = []; foreach ($this->responseDeclaration->getMapping()->getMapEntries() as $mapEntry) { /** @var MapEntry $mapEntry */ if (!isset($this->options[$mapEntry->getMapKey()])) { LogService::log('Invalid choice `' . $mapEntry->getMapKey() . '`'); continue; } if ($mapEntry->getMappedValue() < 0) { LogService::log('Invalid score ` ' . $mapEntry->getMappedValue() . ' `. Negative score is ignored'); continue; } $validResponses[] = new ValidResponse($mapEntry->getMappedValue(), [$mapEntry->getMapKey()]); } // Handle `multiple` cardinality if ($this->responseDeclaration->getCardinality() === Cardinality::MULTIPLE) { $combinationChoicesCount = $this->maxChoices === 0 ? count($validResponses) : $this->maxChoices; $combinationResponses = ArrayUtil::combinations($validResponses, $combinationChoicesCount); $validResponses = ArrayUtil::combineValidResponsesWithSummedScore($combinationResponses); } return ValidationBuilder::build('mcq', 'exactMatch', $validResponses); }
public static function convertLearnosityToQtiItem(array $data) { $jsonType = self::guessLearnosityJsonDataType($data); // Handle `item` which contains both a single item and one or more questions/features if ($jsonType === self::LEARNOSITY_DATA_ITEM) { list($xmlString, $messages) = self::convertLearnosityItem($data); // Handle if just question } else { if ($jsonType === self::LEARNOSITY_DATA_QUESTION) { list($xmlString, $messages) = self::convertLearnosityQuestion($data); // Handle if just question data } else { if ($jsonType === self::LEARNOSITY_DATA_QUESTION_DATA) { list($xmlString, $messages) = self::convertLearnosityQuestionData($data); } else { throw new \Exception('Unknown JSON format'); } } } // Validate them before proceeding by feeding it back try { $document = new XmlDocument(); $document->loadFromString($xmlString); } catch (\Exception $e) { LogService::log('Unknown error occurred. The QTI XML produced may not be valid'); } return [$xmlString, $messages]; }
public function testInvalidResponseProcessingTemplate() { $interaction = new \qtism\data\content\interactions\TextEntryInteraction('testIdentifier'); $mapper = new TextEntryInteractionMapper($interaction, null, ResponseProcessingTemplate::getFromTemplateUrl('')); $question = $mapper->getQuestionType(); $this->assertCount(1, LogService::read()); }
private function checkObjectComponents(Object $object, $conversionTo) { if (!empty($object->getComponents())) { LogService::log('Converting <object> element to ' . $conversionTo . '. Any contents within it are removed'); } }
public function testInvalidResponseProcessingTemplate() { $itemBody = $this->buildItemBodyWithSingleInteraction(); $responseDeclarations = new QtiComponentCollection(); $responseDeclarations->attach(ResponseDeclarationBuilder::buildWithMapping('testIdentifierOne', ['Sydney' => [2, false], 'sydney' => [1, false]])); $mapper = new MergedTextEntryInteractionMapper('dummyReference', $itemBody, $responseDeclarations, ResponseProcessingTemplate::getFromTemplateUrl('')); $question = $mapper->getQuestionType(); $this->assertNotNull($question); $this->assertNull($question->get_validation()); $this->assertCount(1, LogService::read()); }
public function tearDown() { LogService::flush(); }