private function generateLearnosityToQtiDocumentation() { $questionTypeDocumentation = []; $responsesSchemas = $this->schemasService->getResponsesSchemas(); foreach ($responsesSchemas as $questionType => $data) { if (in_array($questionType, QtiExportConstant::$supportedQuestionTypes)) { /** @var QuestionTypeDocumentationInterface $mapperClass */ $mapperClass = 'LearnosityQti\\Processors\\QtiV2\\Out\\Documentation\\QuestionTypes\\' . ucfirst($questionType) . 'Documentation'; $documentation = $mapperClass::getDocumentation(); foreach (array_keys($this->generateAtributeTable($data['attributes'])) as $flattenedAttributeName) { // TODO: Need to check new or non-existing attribute name in case our schemas change if (!in_array($flattenedAttributeName, array_keys($documentation))) { $documentation[$flattenedAttributeName] = LearnosityDoc::none(); } } // TODO: Hack here, hide all the `validation` attributes $documentationToDisplay = []; foreach ($documentation as $attributeName => $doc) { if (!StringUtil::startsWith($attributeName, 'validation')) { $documentationToDisplay[$attributeName] = $doc; } } $questionTypeDocumentation[$questionType] = ['mapping' => $documentationToDisplay, 'introduction' => $mapperClass::getIntroductionNotes()]; } } return ['questionTypes' => $questionTypeDocumentation, 'unsupportedQuestionTypes' => array_keys(array_diff_key($responsesSchemas, $questionTypeDocumentation))]; }
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 matchAndRemoveByKey($pathPattern) { $matches = []; foreach ($this->flattenedMetadatas as $key => $value) { list($valueMatched, $valueMatches) = StringUtil::matchString($pathPattern, $key); if ($valueMatched === true) { $matches[$key] = ['value' => $value, 'valueMatched' => $valueMatched, 'valueMatches' => $valueMatches]; } } $this->removeKeys(array_keys($matches)); return $matches; }
public function testMappingMcqQuestion() { $questionJson = $this->getFixtureFileContents('learnosityjsons/item_mcq.json'); $question = json_decode($questionJson, true); list($xmlString, $messages) = Converter::convertLearnosityToQtiItem($question); $this->assertNotNull($xmlString); $this->assertTrue(StringUtil::startsWith($xmlString, '<?xml version="1.0" encoding="UTF-8"?> <assessmentItem xmlns="http://www.imsglobal.org/xsd/imsqti_v2p1"')); $document = new XmlDocument(); $document->loadFromString($xmlString); $this->assertNotNull($document); }
private function matchParameters(array $parameters, array $matchedGroup) { $matchedParameters = []; foreach ($parameters as $param => $paramPath) { foreach ($matchedGroup as $group) { $endString = $group['endString']; $value = $group['value']; // Only grab the first match! list($matched, $parammatches) = StringUtil::matchString($paramPath, $endString); if ($matched === true) { $matchedParameters[$param] = $value; } } } return $matchedParameters; }
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]; }
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); }
private function buildItemBodyWithItemContent(array $interactions, $content) { // Map <itemBody> // TODO: Wrap these `content` stuff in a div // TODO: to avoid QtiComponentIterator bug ignoring 2nd element with empty content $contentCollection = QtiMarshallerUtil::unmarshallElement($content); $wrapperCollection = new FlowCollection(); foreach ($contentCollection as $component) { $wrapperCollection->attach($component); } $divWrapper = new Div(); $divWrapper->setContent($wrapperCollection); // Iterate through these elements and try to replace every single question `span` with its interaction equivalent $iterator = $divWrapper->getIterator(); foreach ($iterator as $component) { if ($component instanceof Span && StringUtil::contains($component->getClass(), 'learnosity-response')) { $currentContainer = $iterator->getCurrentContainer(); $questionReference = trim(str_replace('learnosity-response', '', $component->getClass())); $questionReference = trim(str_replace('question-', '', $questionReference)); // Build the actual interaction $interaction = $interactions[$questionReference]['interaction']; $content = new FlowCollection(); if (isset($interactions[$questionReference]['extraContent'])) { $content->attach($interactions[$questionReference]['extraContent']); } $content->attach($interaction); $replacement = ContentCollectionBuilder::buildContent($currentContainer, $content)->current(); $currentContainer->getComponents()->replace($component, $replacement); } } // Extract the actual content from the div wrapper and add that to our <itemBody> $componentsWithinDiv = $divWrapper->getComponents(); $itemBody = new ItemBody(); $itemBody->setContent(ContentCollectionBuilder::buildBlockCollectionContent($componentsWithinDiv)); return $itemBody; }
public static function convertQtiItemToLearnosity($xmlString, $baseAssetsUrl = '', $validate = true) { $itemMapper = AppContainer::getApplicationContainer()->get('qtiv2_item_mapper'); $itemWriter = AppContainer::getApplicationContainer()->get('learnosity_item_writer'); $questionWriter = AppContainer::getApplicationContainer()->get('learnosity_question_writer'); // Parse `em try { list($item, $questions, $exceptions) = $itemMapper->parse($xmlString, $validate); } catch (XmlStorageException $e) { // Check invalid schema error message and intercept to rethrow as known `InvalidQtiException` exception $exceptionMessage = $e->getMessage(); if (StringUtil::startsWith($exceptionMessage, 'The document could not be validated with XML Schema')) { $exceptionMessage = preg_replace('/The document could not be validated with schema(.*)/', 'The document could not be validated with standard QTI schema: ', $exceptionMessage); throw new InvalidQtiException($exceptionMessage); } else { throw $e; } } // Conversion to JSON $itemData = []; if ($item instanceof item) { $itemData = $itemWriter->convert($item); } $questionsData = []; if (is_array($questions)) { foreach ($questions as $question) { $questionsData[] = $questionWriter->convert($question); } } return [$itemData, $questionsData, $exceptions]; }
private function map(Question $question) { $type = $question->get_type(); if (!in_array($type, Constants::$supportedQuestionTypes)) { throw new MappingException("Question type `{$type}` not yet supported to be mapped to QTI"); } $clazz = new \ReflectionClass(self::MAPPER_CLASS_BASE . ucfirst($type . 'Mapper')); $questionTypeMapper = $clazz->newInstance(); // Try to use question `reference` as identifier // Otherwise, generate an alternative identifier and store the original reference as `label` to be passed in $questionReference = $question->get_reference(); $interactionIdentifier = Format::isIdentifier($questionReference, false) ? $questionReference : strtoupper($type) . '_' . StringUtil::generateRandomString(12); if ($interactionIdentifier !== $questionReference) { LogService::log("The question `reference` ({$questionReference}) is not a valid identifier. " . "Replaced it with randomly generated `{$interactionIdentifier}` and stored the original `reference` as `label` attribute"); } $result = $questionTypeMapper->convert($question->get_data(), $interactionIdentifier, $questionReference); $result[] = $questionTypeMapper->getExtraContent(); return $result; }
private function getFeatureReferenceFromClassName($classname) { // Parse classname, ie `learnosity-feature feature-DEMOFEATURE123` // Then, return `DEMOFEATURE123` $parts = preg_split('/\\s+/', $classname); foreach ($parts as $part) { if (StringUtil::startsWith(strtolower($part), 'feature-')) { return explode('-', $part)[1]; } } // TODO: throw exception return null; }