diff --git a/data/.gitignore b/data/.gitignore new file mode 100644 index 00000000..e46504a6 --- /dev/null +++ b/data/.gitignore @@ -0,0 +1,3 @@ +input/* +output/* +!.gitignore diff --git a/data/input/.gitignore b/data/input/.gitignore deleted file mode 100644 index d6b7ef32..00000000 --- a/data/input/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/data/output/.gitignore b/data/output/.gitignore deleted file mode 100644 index d6b7ef32..00000000 --- a/data/output/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/src/Commands/ConvertToLearnosityCommand.php b/src/Commands/ConvertToLearnosityCommand.php index fb09841b..335558d5 100644 --- a/src/Commands/ConvertToLearnosityCommand.php +++ b/src/Commands/ConvertToLearnosityCommand.php @@ -38,6 +38,18 @@ protected function configure() 'The identifier of the item bank you want to import content into', '' ) + ->addOption( + 'item-reference-source', + '', + InputOption::VALUE_OPTIONAL, + 'The source to use to extract the reference for the item. ' . + 'Valid values are the following: ' . PHP_EOL . + ' item - uses the identifier attribute on the element' . PHP_EOL . + ' metadata - uses the element from the LOM metadata in the manifest, if available. If no is found, then this parameter operates in "item" mode' . PHP_EOL . + ' resource - uses the identifier attribute on the element in the manifest' . PHP_EOL . + ' filename - uses the basename of the XML file' . PHP_EOL, + 'metadata' + ) ; } @@ -47,6 +59,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $inputPath = $input->getOption('input'); $outputPath = $input->getOption('output'); $organisationId = $input->getOption('organisation_id'); + $itemReferenceSource = $input->getOption('item-reference-source'); // Validate the required options if (empty($inputPath) || empty($outputPath)) { @@ -73,6 +86,14 @@ protected function execute(InputInterface $input, OutputInterface $output) array_push($validationErrors, "The organisation_id option is required for asset uploads."); } + $validItemReferenceSources = ['item', 'metadata', 'filename', 'resource']; + if (isset($itemReferenceSource) && !in_array($itemReferenceSource, $validItemReferenceSources)) { + array_push( + $validationErrors, + "The item-reference-source must be one of the following values: " . join(', ', $validItemReferenceSources) + ); + } + if (!empty($validationErrors)) { $output->writeln([ '', @@ -87,7 +108,26 @@ protected function execute(InputInterface $input, OutputInterface $output) " mo convert:to:learnosity --input /path/to/qti --output /path/to/save/folder --organisation_id [integer]" ]); } else { + $Convert = new ConvertToLearnosityService($inputPath, $outputPath, $output, $organisationId); + + $Convert->useMetadataIdentifier = true; + $Convert->useResourceIdentifier = false; + $Convert->useFileNameAsIdentifier = false; + if ($itemReferenceSource === 'item') { + $Convert->useMetadataIdentifier = false; + $Convert->useResourceIdentifier = false; + $Convert->useFileNameAsIdentifier = false; + } elseif ($itemReferenceSource === 'filename') { + $Convert->useMetadataIdentifier = false; + $Convert->useResourceIdentifier = false; + $Convert->useFileNameAsIdentifier = true; + } elseif ($itemReferenceSource === 'resource') { + $Convert->useMetadataIdentifier = false; + $Convert->useResourceIdentifier = true; + $Convert->useFileNameAsIdentifier = false; + } + $result = $Convert->process(); if ($result['status'] === false) { $output->writeln('Error running job'); diff --git a/src/Converter.php b/src/Converter.php index 662fcf02..3e6be41b 100644 --- a/src/Converter.php +++ b/src/Converter.php @@ -192,7 +192,7 @@ public static function convertQtiItemToLearnosity($xmlString, $baseAssetsUrl = ' $sourceDirectoryPath = dirname($filePath); } // TODO: Handle additional (related) items being passed back - $result = $itemMapper->parse($xmlString, $validate, $sourceDirectoryPath, $metadata); + $result = $itemMapper->parse($xmlString, $validate, $sourceDirectoryPath, $metadata, $customItemReference); $item = $result['item']; $questions = $result['questions']; $features = $result['features']; @@ -247,6 +247,9 @@ public static function convertQtiItemToLearnosity($xmlString, $baseAssetsUrl = ' } } + $layoutService = new \LearnosityQti\Services\ItemLayoutService(); + $itemData = $layoutService->migrateItem($itemData, array_merge($questionsData, $featuresData)); + return [ 'item' => $itemData, 'questions' => $questionsData, diff --git a/src/Entities/QuestionTypes/imageclozeassociation.php b/src/Entities/QuestionTypes/imageclozeassociation.php index ecf8c59e..a2ac16e9 100644 --- a/src/Entities/QuestionTypes/imageclozeassociation.php +++ b/src/Entities/QuestionTypes/imageclozeassociation.php @@ -25,19 +25,22 @@ class imageclozeassociation extends BaseQuestionType { protected $response_positions; protected $aria_labels; protected $group_possible_responses; + protected $possible_responses; protected $img_src; protected $duplicate_responses; protected $shuffle_options; - + public function __construct( $type, imageclozeassociation_image $image, - array $response_positions + array $response_positions, + array $possible_responses ) { $this->type = $type; $this->image = $image; $this->response_positions = $response_positions; + $this->possible_responses = $possible_responses; } /** @@ -76,6 +79,24 @@ public function set_metadata (imageclozeassociation_metadata $metadata) { $this->metadata = $metadata; } + /** + * Get Possible Responses \ + * The question possible_responses. Can include text, tables, images. \ + * @return string $possible_responses \ + */ + public function get_possible_responses() { + return $this->possible_responses; + } + + /** + * Set Possible Responses \ + * The question possible_responses. Can include text, tables, images. \ + * @param string $possible_responses \ + */ + public function set_possible_responses (array $possible_responses) { + $this->possible_responses = $possible_responses; + } + /** * Get Stimulus \ * The question stimulus. Can include text, tables, images. \ @@ -396,9 +417,8 @@ public function set_shuffle_options ($shuffle_options) { $this->shuffle_options = $shuffle_options; } - + public function get_widget_type() { return 'response'; } } - diff --git a/src/Processors/IMSCP/In/ManifestMapper.php b/src/Processors/IMSCP/In/ManifestMapper.php index 2734cf47..492478d4 100644 --- a/src/Processors/IMSCP/In/ManifestMapper.php +++ b/src/Processors/IMSCP/In/ManifestMapper.php @@ -4,7 +4,7 @@ use LearnosityQti\Exceptions\MappingException; use LearnosityQti\Processors\IMSCP\Entities\Manifest; -use qtism\data\storage\xml\marshalling\Marshaller; +use qtism\data\storage\xml\Utils as XmlUtils; class ManifestMapper { @@ -23,34 +23,34 @@ public function parseManifestElement(\DOMElement $rootElement) // Manifest mapping start! $manifest = new Manifest(); - $manifest->setIdentifier(Marshaller::getDOMElementAttributeAs($rootElement, 'identifier')); + $manifest->setIdentifier(XmlUtils::getDOMElementAttributeAs($rootElement, 'identifier')); // Mapping (s) to Resource model - $resourcesElement = Marshaller::getChildElementsByTagName($rootElement, 'resources'); + $resourcesElement = XmlUtils::getChildElementsByTagName($rootElement, 'resources'); if (!empty($resourcesElement)) { if (count($resourcesElement) !== 1) { throw new MappingException('Resources tag must occur once'); } $resourceMapper = new ResourcesMapper(); - $resourcesListElements = Marshaller::getChildElementsByTagName($resourcesElement[0], 'resource'); + $resourcesListElements = XmlUtils::getChildElementsByTagName($resourcesElement[0], 'resource'); $resources = $resourceMapper->map($resourcesListElements); $manifest->setResources($resources); } // Mapping (s) to Organisation model - $organizationElements = Marshaller::getChildElementsByTagName($rootElement, 'organizations'); + $organizationElements = XmlUtils::getChildElementsByTagName($rootElement, 'organizations'); if (!empty($organizationElements)) { if (count($organizationElements) !== 1) { throw new MappingException('Organisations tag must occur once'); } $organisationsMapper = new OrganizationsMapper(); - $organisationListElements = Marshaller::getChildElementsByTagName($organizationElements[0], 'organization'); + $organisationListElements = XmlUtils::getChildElementsByTagName($organizationElements[0], 'organization'); $organisations = $organisationsMapper->map($organisationListElements); $manifest->setOrganizations($organisations); } // Mapping package Metadata - $metadataElement = Marshaller::getChildElementsByTagName($rootElement, 'metadata'); + $metadataElement = XmlUtils::getChildElementsByTagName($rootElement, 'metadata'); if (!empty($metadataElement)) { if (count($metadataElement) !== 1) { throw new MappingException('Metadata tag must occur once'); @@ -60,7 +60,7 @@ public function parseManifestElement(\DOMElement $rootElement) } // Mapping sub-manifest - $subManifestElement = Marshaller::getChildElementsByTagName($rootElement, 'manifest'); + $subManifestElement = XmlUtils::getChildElementsByTagName($rootElement, 'manifest'); if (!empty($subManifestElement)) { if (count($subManifestElement) !== 1) { throw new MappingException('Manifest tag must occur once'); diff --git a/src/Processors/IMSCP/In/OrganizationsMapper.php b/src/Processors/IMSCP/In/OrganizationsMapper.php index 1d7ac51b..80457c0d 100644 --- a/src/Processors/IMSCP/In/OrganizationsMapper.php +++ b/src/Processors/IMSCP/In/OrganizationsMapper.php @@ -4,7 +4,7 @@ use LearnosityQti\Processors\IMSCP\Entities\Item; use LearnosityQti\Processors\IMSCP\Entities\Organization; -use qtism\data\storage\xml\marshalling\Marshaller; +use qtism\data\storage\xml\Utils as XmlUtils; class OrganizationsMapper { @@ -13,21 +13,21 @@ public function map(array $organisationElements) $organisations = []; foreach ($organisationElements as $organisationElement) { $organisation = new Organization(); - $organisation->setIdentifier(Marshaller::getDOMElementAttributeAs($organisationElement, 'identifier')); - $organisation->setStructure(Marshaller::getDOMElementAttributeAs($organisationElement, 'structure')); + $organisation->setIdentifier(XmlUtils::getDOMElementAttributeAs($organisationElement, 'identifier')); + $organisation->setStructure(XmlUtils::getDOMElementAttributeAs($organisationElement, 'structure')); - $titleElements = Marshaller::getChildElementsByTagName($organisationElement, 'title'); + $titleElements = XmlUtils::getChildElementsByTagName($organisationElement, 'title'); if (!empty($titleElements)) { $organisation->setTitle($titleElements[0]->nodeValue); } - $itemElements = Marshaller::getChildElementsByTagName($organisationElement, 'item'); + $itemElements = XmlUtils::getChildElementsByTagName($organisationElement, 'item'); $items = []; foreach ($itemElements as $itemElement) { $item = new Item(); - $item->setTitle(Marshaller::getChildElementsByTagName($itemElement, 'title')); - $item->setIdentifier(Marshaller::getDOMElementAttributeAs($itemElement, 'identifier')); - $item->setIdentifierref(Marshaller::getDOMElementAttributeAs($itemElement, 'identifierref')); - $item->setIsvisible(Marshaller::getDOMElementAttributeAs($itemElement, 'isvisible')); + $item->setTitle(XmlUtils::getChildElementsByTagName($itemElement, 'title')); + $item->setIdentifier(XmlUtils::getDOMElementAttributeAs($itemElement, 'identifier')); + $item->setIdentifierref(XmlUtils::getDOMElementAttributeAs($itemElement, 'identifierref')); + $item->setIsvisible(XmlUtils::getDOMElementAttributeAs($itemElement, 'isvisible')); $items[] = $item; } $organisation->setItems($items); diff --git a/src/Processors/IMSCP/In/ResourcesMapper.php b/src/Processors/IMSCP/In/ResourcesMapper.php index a597dea9..5b543565 100644 --- a/src/Processors/IMSCP/In/ResourcesMapper.php +++ b/src/Processors/IMSCP/In/ResourcesMapper.php @@ -5,7 +5,7 @@ use LearnosityQti\Processors\IMSCP\Entities\Dependency; use LearnosityQti\Processors\IMSCP\Entities\File; use LearnosityQti\Processors\IMSCP\Entities\Resource; -use qtism\data\storage\xml\marshalling\Marshaller; +use qtism\data\storage\xml\Utils as XmlUtils; class ResourcesMapper { @@ -20,20 +20,20 @@ public function map(array $resourcesListElements) $resources = []; foreach ($resourcesListElements as $resourceElement) { $resource = new Resource(); - $resource->setHref(Marshaller::getDOMElementAttributeAs($resourceElement, 'href')); - $resource->setIdentifier(Marshaller::getDOMElementAttributeAs($resourceElement, 'identifier')); - $resource->setType(Marshaller::getDOMElementAttributeAs($resourceElement, 'type')); + $resource->setHref(XmlUtils::getDOMElementAttributeAs($resourceElement, 'href')); + $resource->setIdentifier(XmlUtils::getDOMElementAttributeAs($resourceElement, 'identifier')); + $resource->setType(XmlUtils::getDOMElementAttributeAs($resourceElement, 'type')); // Mapping to File models - $fileListElements = Marshaller::getChildElementsByTagName($resourceElement, 'file'); + $fileListElements = XmlUtils::getChildElementsByTagName($resourceElement, 'file'); $resource->setFiles($this->mapFileElements($fileListElements)); // Mapping to Dependency models - $dependencyListElements = Marshaller::getChildElementsByTagName($resourceElement, 'dependency'); + $dependencyListElements = XmlUtils::getChildElementsByTagName($resourceElement, 'dependency'); $resource->setDependencies($this->mapDependencyElements($dependencyListElements)); // Mapping its metadata - $metadataListElements = Marshaller::getChildElementsByTagName($resourceElement, 'metadata'); + $metadataListElements = XmlUtils::getChildElementsByTagName($resourceElement, 'metadata'); if (!empty($metadataListElements)) { $metadataMapper = new MetadataMapper(); $flattenedMetadatas = $metadataMapper->map($metadataListElements[0]); @@ -58,7 +58,7 @@ public function mapFileElements(array $fileListElements) if (is_array($fileListElements)) { foreach ($fileListElements as $fileElement) { $file = new File(); - $file->setHref(Marshaller::getDOMElementAttributeAs($fileElement, 'href')); + $file->setHref(XmlUtils::getDOMElementAttributeAs($fileElement, 'href')); $files[] = $file; } } @@ -78,7 +78,7 @@ public function mapDependencyElements(array $dependencyListElements) foreach ($dependencyListElements as $dependencyElement) { $dependency = new Dependency(); $dependency->setIdentifierref( - Marshaller::getDOMElementAttributeAs($dependencyElement, 'identifierref') + XmlUtils::getDOMElementAttributeAs($dependencyElement, 'identifierref') ); $dependencies[] = $dependency; } diff --git a/src/Processors/QtiV2/In/Interactions/GraphicGapMatchInteractionMapper.php b/src/Processors/QtiV2/In/Interactions/GraphicGapMatchInteractionMapper.php index fb1ccbf9..83f5c957 100644 --- a/src/Processors/QtiV2/In/Interactions/GraphicGapMatchInteractionMapper.php +++ b/src/Processors/QtiV2/In/Interactions/GraphicGapMatchInteractionMapper.php @@ -20,7 +20,7 @@ public function getQuestionType() { /** @var QtiGraphicGapMatchInteraction $interaction */ $interaction = $this->interaction; - /** @var Object $imageObject */ + /** @var ObjectElement $imageObject */ $imageObject = $interaction->getObject(); // Yes, width and height is necessary unfortunately @@ -48,9 +48,9 @@ public function getQuestionType() } $question = new imageclozeassociation( + 'imageclozeassociation', $image, $responsePosition, - 'imageclozeassociation', array_values($possibleResponseMapping) ); $question->set_response_containers($responseContainers); @@ -84,7 +84,7 @@ protected function buildPossibleResponseMapping(QtiGraphicGapMatchInteraction $i return $possibleResponseMapping; } - protected function buildTemplate(QtiGraphicGapMatchInteraction $interaction, Object $object) + protected function buildTemplate(QtiGraphicGapMatchInteraction $interaction, ObjectElement $object) { $associableHotspots = []; foreach ($interaction->getAssociableHotspots() as $associableHotspot) { diff --git a/src/Processors/QtiV2/In/Interactions/HotspotInteractionMapper.php b/src/Processors/QtiV2/In/Interactions/HotspotInteractionMapper.php index dcc245ea..69f02f86 100644 --- a/src/Processors/QtiV2/In/Interactions/HotspotInteractionMapper.php +++ b/src/Processors/QtiV2/In/Interactions/HotspotInteractionMapper.php @@ -86,7 +86,7 @@ public function getQuestionType() return $hotspot; } - private function buildHotspotImage(Object $imageObject) + private function buildHotspotImage(ObjectElement $imageObject) { $image = new hotspot_image(); $image->set_source($imageObject->getData()); @@ -95,7 +95,7 @@ private function buildHotspotImage(Object $imageObject) return $image; } - private function buildAreas(HotspotChoiceCollection $hotspotChoices, Object $imageObject) + private function buildAreas(HotspotChoiceCollection $hotspotChoices, ObjectElement $imageObject) { /* @var $choice HotspotChoice */ $areas = []; @@ -114,7 +114,7 @@ private function buildAreas(HotspotChoiceCollection $hotspotChoices, Object $ima return $areas; } - private function transformCoordinates(QtiCoords $coords, $shape, Object $imageObject) + private function transformCoordinates(QtiCoords $coords, $shape, ObjectElement $imageObject) { $width = $imageObject->getWidth(); $height = $imageObject->getHeight(); diff --git a/src/Processors/QtiV2/In/ItemBuilders/RegularItemBuilder.php b/src/Processors/QtiV2/In/ItemBuilders/RegularItemBuilder.php index b3139777..f66400e6 100644 --- a/src/Processors/QtiV2/In/ItemBuilders/RegularItemBuilder.php +++ b/src/Processors/QtiV2/In/ItemBuilders/RegularItemBuilder.php @@ -65,6 +65,10 @@ public function map( ]; } + if (empty($this->questions)) { + LogService::log('Item contains no valid, supported questions'); + } + // Build item's HTML content $extraContentHtml = new SimpleHtmlDom(); if (!$extraContentHtml->load(QtiMarshallerUtil::marshallCollection($itemBody->getComponents()), false)) { @@ -147,24 +151,26 @@ public function map( } // Inject item content into stimulus per question - foreach ($questionHtmlContents as $questionReference => $content) { - $existingStimulus = $this->questions[$questionReference]->get_data()->get_stimulus(); - // HACK: Replace placeholders in item content with stimulus, and inject the whole thing - $newStimulus = str_replace($questionReference, $existingStimulus, $content); - $this->questions[$questionReference]->get_data()->set_stimulus($newStimulus); - - LogService::log('Extra content is prepended to question stimulus and please verify as this `might` break item content structure'); - } + if (!empty($this->questions)) { + foreach ($questionHtmlContents as $questionReference => $content) { + $existingStimulus = $this->questions[$questionReference]->get_data()->get_stimulus(); + // HACK: Replace placeholders in item content with stimulus, and inject the whole thing + $newStimulus = str_replace($questionReference, $existingStimulus, $content); + $this->questions[$questionReference]->get_data()->set_stimulus($newStimulus); + + LogService::log('Extra content is prepended to question stimulus and please verify as this `might` break item content structure'); + } - // 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); - $existingStimulus = $this->questions[$firstQuestionReference]->get_data()->get_stimulus(); - $newStimulus = $extraContent . $existingStimulus; - $this->questions[$firstQuestionReference]->get_data()->set_stimulus($newStimulus); + // 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); + $existingStimulus = $this->questions[$firstQuestionReference]->get_data()->get_stimulus(); + $newStimulus = $extraContent . $existingStimulus; + $this->questions[$firstQuestionReference]->get_data()->set_stimulus($newStimulus); - LogService::log('Extra content is prepended to question stimulus and please verify as this `might` break item content structure'); + LogService::log('Extra content is prepended to question stimulus and please verify as this `might` break item content structure'); + } } // TODO: Confirm that calling processRubricBlock after generating the item content won't break anything diff --git a/src/Processors/QtiV2/In/ItemMapper.php b/src/Processors/QtiV2/In/ItemMapper.php index a81bcf8a..0ee393b8 100644 --- a/src/Processors/QtiV2/In/ItemMapper.php +++ b/src/Processors/QtiV2/In/ItemMapper.php @@ -44,7 +44,7 @@ public function __construct(ItemBuilderFactory $itemBuilderFactory) * first element, associated questions as the second, and * log messages resulting from the operation as the last element */ - public function parse($xmlString, $validateXml = true, $sourceDirectoryPath = null, $metadata = []) + public function parse($xmlString, $validateXml = true, $sourceDirectoryPath = null, $metadata = [], $customItemReference = null) { // TODO: Remove this, and move it higher up LogService::flush(); @@ -59,7 +59,7 @@ public function parse($xmlString, $validateXml = true, $sourceDirectoryPath = nu $assessmentItem = $this->getAssessmentItemFromXmlDocument($xmlDocument); // Convert the QTI assessment item into Learnosity output - return $this->parseWithAssessmentItemComponent($assessmentItem, $sourceDirectoryPath, $metadata); + return $this->parseWithAssessmentItemComponent($assessmentItem, $sourceDirectoryPath, $metadata, $customItemReference); } /** @@ -71,7 +71,7 @@ public function parse($xmlString, $validateXml = true, $sourceDirectoryPath = nu * first element, associated questions as the second, and * log messages resulting from the operation as the last element */ - public function parseWithAssessmentItemComponent(AssessmentItem $assessmentItem, $sourceDirectoryPath = null, $metadata = []) + public function parseWithAssessmentItemComponent(AssessmentItem $assessmentItem, $sourceDirectoryPath = null, $metadata = [], $customItemReference = null) { // TODO: Move this logging service upper to converter class level // Make sure we clean up the log @@ -85,7 +85,12 @@ public function parseWithAssessmentItemComponent(AssessmentItem $assessmentItem, // Conversion from QTI item to Learnosity item and questions // TODO: Handle additional (related) items being passed back - list($item, $questions, $features, $rubric) = $this->buildLearnosityItemFromQtiAssessmentItem($assessmentItem, $sourceDirectoryPath, $metadata); + list($item, $questions, $features, $rubric) = $this->buildLearnosityItemFromQtiAssessmentItem( + $assessmentItem, + $sourceDirectoryPath, + $metadata, + $customItemReference + ); // TODO: Check whether this needs to handle mapping questions to relevant items list($item, $questions) = $this->processLearnosityItem($item, $questions, $processings); @@ -114,10 +119,14 @@ public function parseWithAssessmentItemComponent(AssessmentItem $assessmentItem, * * @throws \LearnosityQti\Exceptions\MappingException */ - protected function buildLearnosityItemFromQtiAssessmentItem(AssessmentItem $assessmentItem, $sourceDirectoryPath = null, $metadata = []) + protected function buildLearnosityItemFromQtiAssessmentItem(AssessmentItem $assessmentItem, $sourceDirectoryPath = null, $metadata = [], $itemReference = null) { $responseProcessingTemplate = $this->getResponseProcessingTemplate($assessmentItem->getResponseProcessing()); + if (is_null($itemReference)) { + $itemReference = $assessmentItem->getIdentifier(); + } + /** @var ItemBody $itemBody */ $itemBody = $assessmentItem->getItemBody(); @@ -143,7 +152,7 @@ protected function buildLearnosityItemFromQtiAssessmentItem(AssessmentItem $asse } $itemBuilder->map( - $assessmentItem->getIdentifier(), + $itemReference, $itemBody, $interactionComponents, $responseDeclarations, diff --git a/src/Processors/QtiV2/In/Processings/AssetsProcessing.php b/src/Processors/QtiV2/In/Processings/AssetsProcessing.php index b4464bca..48a9e7b4 100644 --- a/src/Processors/QtiV2/In/Processings/AssetsProcessing.php +++ b/src/Processors/QtiV2/In/Processings/AssetsProcessing.php @@ -19,8 +19,8 @@ public function setBaseAssetUrl($baseAssetUrl) public function processAssessmentItem(AssessmentItem $assessmentItem) { foreach ($assessmentItem->getIterator() as $component) { - if ($component instanceof Object) { - /** @var Object $component */ + if ($component instanceof ObjectElement) { + /** @var ObjectElement $component */ if ($this->isInternalUrl($component->getData())) { $component->setData($this->baseAssetUrl . $component->getData()); } diff --git a/src/Processors/QtiV2/In/Validation/BaseInteractionValidationBuilder.php b/src/Processors/QtiV2/In/Validation/BaseInteractionValidationBuilder.php index 3fa10e8b..bff5f431 100644 --- a/src/Processors/QtiV2/In/Validation/BaseInteractionValidationBuilder.php +++ b/src/Processors/QtiV2/In/Validation/BaseInteractionValidationBuilder.php @@ -13,7 +13,9 @@ use \qtism\data\rules\ResponseRuleCollection; use \qtism\data\expressions\operators\Match; use \qtism\data\expressions\operators\Equal; +use \qtism\data\expressions\operators\Sum; use \qtism\data\expressions\BaseValue; +use \qtism\data\expressions\Expression; use \qtism\data\expressions\ExpressionCollection; use \qtism\data\expressions\Variable; use \qtism\data\state\OutcomeDeclarationCollection; @@ -336,6 +338,69 @@ protected function processConditionBranch(QtiComponent $conditionBranch) return [$responseId, $results]; } + /** + * @param \qtism\data\expressions\Expression + * @return mixed + */ + protected function evaluateExpression(Expression $expression) + { + switch (true) { + case $expression instanceof Sum: + return $this->evaluateSum($expression); + + case $expression instanceof BaseValue: + return $expression->getValue(); + + case $expression instanceof Variable: + return $this->evaluateVariable($expression); + + default: + LogService::log('ResponseProcessing: Unsupported expression: ' . get_class($expression)); + return null; + } + } + + /** + * @param \qtism\data\expressions\operators\Sum + * @return integer|float + */ + protected function evaluateSum(Sum $sum) + { + $values = []; + foreach ($sum->getExpressions() as $sumOperand) { + $values[] = $this->evaluateExpression($sumOperand); + } + + return array_sum($values); + } + + /** + * @param \qtism\data\expressions\Variable + * @return integer|float + */ + protected function evaluateVariable(Variable $variable) + { + $id = $variable->getIdentifier(); + + // look up the response declaration to get the base value + if (empty($this->outcomeDeclarations[$id])) { + // no mapping found for the specified identifier + LogService::log("ResponseProcessing: No variable mapping found in outcomeDeclaration block for: $id"); + return null; + } + + $value = 0; + if (!empty($this->outcomeDeclarations[$id]->getDefaultValue())) { + $defaultValues = $this->outcomeDeclarations[$id]->getDefaultValue(); + + // we only want the first object as as the variable should only map to another, not multiple + $values = $defaultValues->getValues(); + $value = $values[0]->getValue(); + } + + return $value; + } + /** * Get the outcome values from the ResponseRuleCollection * @@ -363,25 +428,16 @@ protected function getOutcomeValuesFromResponseRules(ResponseRuleCollection $res // the expression here can either be a BaseValue or a Variable object switch (true) { + case $expression instanceof Sum: + $results[] = $this->evaluateSum($expression); + break; + case $expression instanceof BaseValue: $results[] = $expression->getValue(); break; case $expression instanceof Variable: - $id = $expression->getIdentifier(); - - // look up the response declaration to get the base value - if (empty($this->outcomeDeclarations[$id])) { - // no mapping found for the specified identifier - LogService::log("ResponseProcessing: No variable mapping found in outcomeDeclaration block for: $id"); - break; - } - - $defaultValues = $this->outcomeDeclarations[$id]->getDefaultValue(); - - // we only want the first object as as the variable should only map to another, not multiple - $values = $defaultValues->getValues(); - $results[] = $values[0]->getValue(); + $results[] = $this->evaluateVariable($expression); break; case $expression instanceof MapResponse: diff --git a/src/Processors/QtiV2/In/Validation/GapMatchInteractionValidationBuilder.php b/src/Processors/QtiV2/In/Validation/GapMatchInteractionValidationBuilder.php index e58acaa3..623131a9 100644 --- a/src/Processors/QtiV2/In/Validation/GapMatchInteractionValidationBuilder.php +++ b/src/Processors/QtiV2/In/Validation/GapMatchInteractionValidationBuilder.php @@ -96,6 +96,10 @@ protected function getMapResponseTemplateValidation() $responseIndexSet = []; $responses = []; + // FIXME: Remove this hard-coded variable and implement handling of mode + // for map response template validation + $mode = 'exactMatch'; + foreach ($this->responseDeclaration->getMapping()->getMapEntries() as $mapEntry) { /** @var MapEntry $mapEntry */ /** @var QtiDirectedPair $mapKey */ diff --git a/src/Processors/QtiV2/Marshallers/LearnosityObjectMarshaller.php b/src/Processors/QtiV2/Marshallers/LearnosityObjectMarshaller.php index af93ec59..7f69cb00 100644 --- a/src/Processors/QtiV2/Marshallers/LearnosityObjectMarshaller.php +++ b/src/Processors/QtiV2/Marshallers/LearnosityObjectMarshaller.php @@ -19,7 +19,7 @@ class LearnosityObjectMarshaller extends ObjectMarshaller protected function marshallChildrenKnown(QtiComponent $component, array $elements) { - /** @var Object $component */ + /** @var ObjectElement $component */ switch ($this->getMIMEType($component->getType())) { case self::MIME_IMAGE: $this->checkObjectComponents($component, ' tag'); @@ -59,7 +59,7 @@ protected function marshallChildrenKnown(QtiComponent $component, array $element } } - private function checkObjectComponents(Object $object, $conversionTo) + private function checkObjectComponents(ObjectElement $object, $conversionTo) { if (!empty($object->getComponents())) { LogService::log('Converting element to ' . $conversionTo . '. Any contents within it are removed'); diff --git a/src/Processors/QtiV2/Out/QuestionTypes/HotspotMapper.php b/src/Processors/QtiV2/Out/QuestionTypes/HotspotMapper.php index 821c7859..65959054 100644 --- a/src/Processors/QtiV2/Out/QuestionTypes/HotspotMapper.php +++ b/src/Processors/QtiV2/Out/QuestionTypes/HotspotMapper.php @@ -67,7 +67,7 @@ public function convert(BaseQuestionType $questionType, $interactionIdentifier, private function buildMainImageObject(hotspot_image $image) { $imageSrc = $image->get_source(); - $imageObject = new Object($imageSrc, MimeUtil::guessMimeType($imageSrc)); + $imageObject = new ObjectElement($imageSrc, MimeUtil::guessMimeType($imageSrc)); $imageWidth = null; $imageHeight = null; diff --git a/src/Processors/QtiV2/Out/QuestionTypes/ImageclozeassociationMapper.php b/src/Processors/QtiV2/Out/QuestionTypes/ImageclozeassociationMapper.php index 49681ee3..2b6f1de6 100644 --- a/src/Processors/QtiV2/Out/QuestionTypes/ImageclozeassociationMapper.php +++ b/src/Processors/QtiV2/Out/QuestionTypes/ImageclozeassociationMapper.php @@ -86,7 +86,7 @@ private function buildGapImgCollection(array $possibleResponses, $matchMax) // TODO: Validation these attributes exists $src = $img[0]->src; $imagesize = getimagesize(CurlUtil::prepareUrlForCurl($src)); - $gapImageObject = new Object($src, $imagesize['mime']); + $gapImageObject = new ObjectElement($src, $imagesize['mime']); $gapImageObject->setWidth($imagesize[0]); $gapImageObject->setHeight($imagesize[1]); // No `img` assuming its all text @@ -104,7 +104,7 @@ private function buildMainImageObject(imageclozeassociation_image $image) { $imageSrc = $image->get_src(); list($imageWidth, $imageHeight) = CurlUtil::getImageSize(CurlUtil::prepareUrlForCurl($imageSrc)); - $imageObject = new Object($imageSrc, MimeUtil::guessMimeType($imageSrc)); + $imageObject = new ObjectElement($imageSrc, MimeUtil::guessMimeType($imageSrc)); $imageObject->setWidth($imageWidth); $imageObject->setHeight($imageHeight); @@ -135,7 +135,7 @@ private function convertTextToObjectWithBase64ImageString($text) $imagedata = 'data:image/png;base64,' . base64_encode($imagedata); - $gapImageObject = new Object($imagedata, 'image/png'); + $gapImageObject = new ObjectElement($imagedata, 'image/png'); $gapImageObject->setWidth($width); $gapImageObject->setHeight($height); return $gapImageObject; diff --git a/src/Services/ConvertToLearnosityService.php b/src/Services/ConvertToLearnosityService.php index 3cfbad1e..9c6a25a0 100644 --- a/src/Services/ConvertToLearnosityService.php +++ b/src/Services/ConvertToLearnosityService.php @@ -41,11 +41,11 @@ class ConvertToLearnosityService /* Job-specific configurations */ // Overrides identifiers to be the same as the filename - protected $useFileNameAsIdentifier = false; + public $useFileNameAsIdentifier = false; // Uses the identifier found in learning object metadata if available - protected $useMetadataIdentifier = true; + public $useMetadataIdentifier = true; // Resource identifiers sometimes (but not always) match the assessmentItem identifier, so this can be useful - protected $useResourceIdentifier = false; + public $useResourceIdentifier = false; private $assetsFixer; @@ -185,7 +185,11 @@ private function convertQtiContentPackagesInDirectory($sourceDirectory, $relativ $metadata['point_value'] = $itemPointValue; } - $this->output->writeln("Converting assessment item {$itemReference}: $relativeDir/$resourceHref"); + if (isset($itemReference)) { + $this->output->writeln("Converting assessment item {$itemReference}: $relativeDir/$resourceHref"); + } else { + $this->output->writeln("Converting assessment item {$itemCount}: $relativeDir/$resourceHref"); + } $convertedContent = $this->convertAssessmentItemInFile($assessmentItemContents, $itemReference, $metadata, $currentDir, $resourceHref); if (!empty($convertedContent)) { $results['qtiitems'][basename($relativeDir).'/'.$resourceHref] = $convertedContent; @@ -395,7 +399,7 @@ private function getItemReferenceFromResource( } if ($useResourceIdentifier) { - $itemReference = $this->getAttribute('identifier'); + $itemReference = $resource->getAttribute('identifier'); } if ($useFileNameAsIdentifier) { @@ -407,6 +411,18 @@ private function getItemReferenceFromResource( return $itemReference; } + /** + * Takes the resource href and extracts the file name out of it. + * @example items/My-File.xml will return My-File + * + * @param string $resourceHref + * @return string + */ + private function getIdentifierFromResourceHref($resourceHref, $suffix = '.xml') + { + return basename($resourceHref, $suffix); + } + /** * Tries to determine item scoring information based on response * processing rules in the given XML string. diff --git a/src/Services/ItemLayoutService.php b/src/Services/ItemLayoutService.php index 36871847..c561a07b 100644 --- a/src/Services/ItemLayoutService.php +++ b/src/Services/ItemLayoutService.php @@ -124,7 +124,7 @@ protected function migrateItemJson($itemJson) * * @return array - migrated item result */ - protected function migrateItem(array $item, array $widgetsJson = []) + public function migrateItem(array $item, array $widgetsJson = []) { if (isset($item['definition'])) { return $item; diff --git a/src/Services/LearnosityToQtiPreProcessingService.php b/src/Services/LearnosityToQtiPreProcessingService.php index 52f6522c..8398cfbf 100644 --- a/src/Services/LearnosityToQtiPreProcessingService.php +++ b/src/Services/LearnosityToQtiPreProcessingService.php @@ -64,7 +64,7 @@ private function getFeatureReplacementString($node) $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)))); + return QtiMarshallerUtil::marshallValidQti(new ObjectElement($src, MimeUtil::guessMimeType(basename($src)))); } // Process regular question feature } else { @@ -75,13 +75,13 @@ private function getFeatureReplacementString($node) if ($type === 'audioplayer' || $type === 'audioplayer') { $src = $feature['data']['src']; - $object = new Object($src, MimeUtil::guessMimeType(basename($src))); + $object = new ObjectElement($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 = new ObjectElement('', 'text/html'); $object->setContent(ContentCollectionBuilder::buildObjectFlowCollectionContent(QtiMarshallerUtil::unmarshallElement($content))); $object->setLabel($featureReference); return QtiMarshallerUtil::marshallValidQti($object);