tao-test/app/taoQtiItem/model/qti/ImportService.php

880 lines
36 KiB
PHP

<?php
/**
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; under version 2
* of the License (non-upgradable).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* Copyright (c) 2016 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
*
*/
namespace oat\taoQtiItem\model\qti;
use common_exception_Error;
use common_exception_UserReadableException;
use common_Logger;
use common_report_Report;
use core_kernel_classes_Class;
use core_kernel_classes_Resource;
use DOMDocument;
use Exception;
use helpers_File;
use oat\generis\model\OntologyAwareTrait;
use oat\tao\model\TaoOntology;
use oat\oatbox\mutex\LockTrait;
use oat\taoItems\model\media\ItemMediaResolver;
use oat\taoItems\model\media\LocalItemSource;
use oat\taoQtiItem\helpers\Authoring;
use oat\taoQtiItem\model\ItemModel;
use oat\taoQtiItem\model\portableElement\exception\PortableElementException;
use oat\taoQtiItem\model\portableElement\exception\PortableElementInvalidModelException;
use oat\taoQtiItem\model\qti\asset\AssetManager;
use oat\taoQtiItem\model\qti\asset\handler\LocalAssetHandler;
use oat\taoQtiItem\model\qti\asset\handler\PortableAssetHandler;
use oat\taoQtiItem\model\qti\asset\handler\SharedStimulusAssetHandler;
use oat\taoQtiItem\model\qti\asset\handler\StimulusHandler;
use oat\taoQtiItem\model\qti\event\UpdatedItemEventDispatcher;
use oat\taoQtiItem\model\qti\exception\ExtractException;
use oat\taoQtiItem\model\qti\exception\ParsingException;
use oat\taoQtiItem\model\qti\exception\TemplateException;
use oat\taoQtiItem\model\qti\metadata\importer\MetadataImporter;
use oat\taoQtiItem\model\qti\metadata\MetadataGuardianResource;
use oat\taoQtiItem\model\qti\metadata\MetadataService;
use oat\taoQtiItem\model\qti\parser\ValidationException;
use oat\taoQtiItem\model\event\ItemImported;
use qtism\data\QtiComponentCollection;
use qtism\data\rules\SetOutcomeValue;
use qtism\data\storage\xml\XmlDocument;
use qtism\data\storage\xml\XmlStorageException;
use qtism\runtime\processing\ResponseProcessingEngine;
use qtism\runtime\tests\AssessmentItemSession;
use qtism\runtime\tests\SessionManager;
use tao_helpers_File;
use taoItems_models_classes_ItemsService;
use oat\oatbox\event\EventManager;
use oat\oatbox\service\ServiceManager;
use oat\oatbox\service\ConfigurableService;
/**
* Short description of class oat\taoQtiItem\model\qti\ImportService
*
* @access public
* @author Joel Bout, <joel.bout@tudor.lu>
* @package taoQTI
*/
class ImportService extends ConfigurableService
{
use OntologyAwareTrait;
use LockTrait;
const SERVICE_ID = 'taoQtiItem/ImportService';
/**
* Checks that setOutcomeValue declared in the outcomeDeclaration
*/
const CONFIG_VALIDATE_RESPONSE_PROCESSING = 'validateResponseProcessing';
/**
* TTL of the item importing process
* How long item will be locked while lock service automatically release the lock
*/
const OPTION_IMPORT_LOCK_TTL = 'importLockTtl';
const PROPERTY_QTI_ITEM_IDENTIFIER = 'http://www.tao.lu/Ontologies/TAOItem.rdf#QtiItemIdentifier';
/**
* @return ImportService
*/
public static function singleton()
{
return ServiceManager::getServiceManager()->get(self::SERVICE_ID);
}
/**
* @var MetadataImporter Service to manage Lom metadata during package import
*/
protected $metadataImporter;
/**
* Short description of method importQTIFile
*
* @access public
* @param $qtiFile
* @param core_kernel_classes_Class $itemClass
* @param bool $validate
* @return common_report_Report
* @throws \common_ext_ExtensionException
* @throws common_exception_Error
* @throws \common_Exception
* @author Joel Bout, <joel.bout@tudor.lu>
*/
public function importQTIFile($qtiFile, core_kernel_classes_Class $itemClass, $validate = true)
{
$report = null;
try {
$qtiModel = $this->createQtiItemModel($qtiFile, $validate);
$rdfItem = $this->createRdfItem($itemClass, $qtiModel);
$report = \common_report_Report::createSuccess(__('The IMS QTI Item was successfully imported.'), $rdfItem);
} catch (ValidationException $ve) {
$report = \common_report_Report::createFailure(__('The IMS QTI Item could not be imported.'));
$report->add($ve->getReport());
}
return $report;
}
/**
*
* @param core_kernel_classes_Class $itemClass
* @param Item $qtiModel
* @param Resource $qtiItemResource
* @return core_kernel_classes_Resource
* @throws \common_Exception
* @throws common_exception_Error
*/
protected function createRdfItem(core_kernel_classes_Class $itemClass, Item $qtiModel)
{
$itemService = taoItems_models_classes_ItemsService::singleton();
$qtiService = Service::singleton();
if (!$itemService->isItemClass($itemClass)) {
throw new common_exception_Error('provided non Itemclass for ' . __FUNCTION__);
}
$rdfItem = $itemService->createInstance($itemClass);
//set the QTI type
$itemService->setItemModel($rdfItem, new core_kernel_classes_Resource(ItemModel::MODEL_URI));
//set the label
$label = '';
if ($qtiModel->hasAttribute('label')) {
$label = $qtiModel->getAttributeValue('label');
}
if (empty($label)) {
$label = $qtiModel->getAttributeValue('title');
}
$rdfItem->setLabel($label);
//save itemcontent
if (!$qtiService->saveDataItemToRdfItem($qtiModel, $rdfItem)) {
throw new \common_Exception('Unable to save item');
}
return $rdfItem;
}
protected function createQtiItemModel($qtiFile, $validate = true)
{
$qtiXml = Authoring::sanitizeQtiXml($qtiFile);
//validate the file to import
$qtiParser = new Parser($qtiXml);
if ($validate) {
$qtiParser->validate();
if (!$qtiParser->isValid()) {
$eStrs = [];
foreach ($qtiParser->getErrors() as $libXmlError) {
$eStrs[] = __('QTI-XML error at line %1$d "%2$s".', $libXmlError['line'], str_replace('[LibXMLError] ', '', trim($libXmlError['message'])));
}
// Make sure there are no duplicate...
$eStrs = array_unique($eStrs);
// Add sub-report.
throw new ValidationException($qtiFile, $eStrs);
}
}
$qtiItem = $qtiParser->load();
if (!$qtiItem && count($qtiParser->getErrors())) {
$errors = [];
foreach ($qtiParser->getErrors() as $error) {
$errors[] = $error['message'];
}
throw new ValidationException($qtiFile, $errors);
}
return $qtiItem;
}
protected function createQtiManifest($manifestFile, $validate = true)
{
//load and validate the manifest
$qtiManifestParser = new ManifestParser($manifestFile);
if ($validate) {
$qtiManifestParser->validate();
if (!$qtiManifestParser->isValid()) {
$eStrs = [];
foreach ($qtiManifestParser->getErrors() as $libXmlError) {
if (isset($libXmlError['line'])) {
$error = __('XML error at line %1$d "%2$s".', $libXmlError['line'], str_replace('[LibXMLError] ', '', trim($libXmlError['message'])));
} else {
$error = __('XML error "%1$s".', str_replace('[LibXMLError] ', '', trim($libXmlError['message'])));
}
$eStrs[] = $error;
}
// Add sub-report.
throw new ValidationException($manifestFile, $eStrs);
}
}
return $qtiManifestParser->load();
}
/**
* imports a qti package and
* returns the number of items imported
*
* @access public
* @param $file
* @param core_kernel_classes_Class $itemClass
* @param bool $validate
* @param bool $rollbackOnError
* @param bool $rollbackOnWarning
* @param bool $enableMetadataGuardians
* @param bool $enableMetadataValidators
* @param bool $itemMustExist
* @param bool $itemMustBeOverwritten
* @return common_report_Report
* @throws Exception
* @throws ExtractException
* @throws ParsingException
* @throws \common_Exception
* @throws \common_ext_ExtensionException
* @throws common_exception_Error
* @author Joel Bout, <joel.bout@tudor.lu>
*/
public function importQTIPACKFile(
$file,
core_kernel_classes_Class $itemClass,
$validate = true,
$rollbackOnError = false,
$rollbackOnWarning = false,
$enableMetadataGuardians = true,
$enableMetadataValidators = true,
$itemMustExist = false,
$itemMustBeOverwritten = false
) {
$initialLogMsg = "Importing QTI Package with the following options:\n";
$initialLogMsg .= '- Rollback On Warning: ' . json_encode($rollbackOnWarning) . "\n";
$initialLogMsg .= '- Rollback On Error: ' . json_encode($rollbackOnError) . "\n";
$initialLogMsg .= '- Enable Metadata Guardians: ' . json_encode($enableMetadataGuardians) . "\n";
$initialLogMsg .= '- Enable Metadata Validators: ' . json_encode($enableMetadataValidators) . "\n";
$initialLogMsg .= '- Item Must Exist: ' . json_encode($itemMustExist) . "\n";
$initialLogMsg .= '- Item Must Be Overwritten: ' . json_encode($itemMustBeOverwritten) . "\n";
\common_Logger::d($initialLogMsg);
//load and validate the package
$qtiPackageParser = new PackageParser($file);
if ($validate) {
$qtiPackageParser->validate();
if (!$qtiPackageParser->isValid()) {
throw new ParsingException('Invalid QTI package format');
}
}
//extract the package
$folder = $qtiPackageParser->extract();
if (!is_dir($folder)) {
throw new ExtractException();
}
$report = new common_report_Report(common_report_Report::TYPE_SUCCESS, '');
$successItems = [];
$allCreatedClasses = [];
$overwrittenItems = [];
$itemCount = 0;
try {
// The metadata import feature needs a DOM representation of the manifest.
$domManifest = new DOMDocument('1.0', 'UTF-8');
$domManifest->load($folder . 'imsmanifest.xml');
/** @var Resource[] $qtiItemResources */
$qtiItemResources = $this->createQtiManifest($folder . 'imsmanifest.xml');
$metadataValues = $this->getMetadataImporter()->extract($domManifest);
$sharedFiles = [];
$createdClasses = [];
foreach ($qtiItemResources as $qtiItemResource) {
$itemCount++;
$itemReport = $this->importQtiItem(
$folder,
$qtiItemResource,
$itemClass,
$sharedFiles,
[],
$metadataValues,
$createdClasses,
$enableMetadataGuardians,
$enableMetadataValidators,
$itemMustExist,
$itemMustBeOverwritten,
$overwrittenItems
);
$allCreatedClasses = array_merge($allCreatedClasses, $createdClasses);
$rdfItem = $itemReport->getData();
if ($rdfItem) {
$successItems[$qtiItemResource->getIdentifier()] = $rdfItem;
}
$report->add($itemReport);
}
} catch (ValidationException $ve) {
$validationReport = \common_report_Report::createFailure("The IMS Manifest file could not be validated");
$validationReport->add($ve->getReport());
$report->setMessage(__("No Items could be imported from the given IMS QTI package."));
$report->setType(common_report_Report::TYPE_ERROR);
$report->add($validationReport);
} catch (common_exception_UserReadableException $e) {
$report = new common_report_Report(common_report_Report::TYPE_ERROR, $e->getUserMessage());
$report->add($e);
}
if (!empty($successItems)) {
// Some items were imported from the package.
$report->setMessage(
__('%d Item(s) of %d imported from the given IMS QTI Package.', count($successItems), $itemCount)
);
if (count($successItems) !== $itemCount) {
$report->setType(common_report_Report::TYPE_WARNING);
}
} else {
$report->setMessage(__('No Items could be imported from the given IMS QTI package.'));
$report->setType(common_report_Report::TYPE_ERROR);
}
if ($rollbackOnError === true) {
if (
$report->getType() === common_report_Report::TYPE_ERROR || $report->contains(
common_report_Report::TYPE_ERROR
)
) {
$this->rollback($successItems, $report, $allCreatedClasses, $overwrittenItems);
}
} elseif ($rollbackOnWarning === true) {
if ($report->contains(common_report_Report::TYPE_WARNING)) {
$this->rollback($successItems, $report, $allCreatedClasses, $overwrittenItems);
}
}
// cleanup
tao_helpers_File::delTree($folder);
return $report;
}
/**
* Log events when items lock released after the configured item import ttl
* It is possible that somehow 2 item import processes were run at once,
* in this case we can get a situation when process 1 started import of the
* item with ID item1, then process 2 started import of the same item item1,
* process 2 saw that item with this ID already exists and pass information that
* item exists, but as we know item import is in progress and if someone try to
* work with items files he will see an error that files (any resources of the item)
* are not found and show error
* @param float $startImportTime
* @param string $itemId
*/
private function checkImportLockTime(float $startImportTime, string $itemId = ''): void
{
$timeElapsedSecs = microtime(true) - $startImportTime;
if ($timeElapsedSecs > $this->getOption(self::OPTION_IMPORT_LOCK_TTL)) {
common_Logger::w('Items lock was released before item '.$itemId.' import finished.');
}
}
/**
* @param $tmpFolder
* @param \oat\taoQtiItem\model\qti\Resource $qtiItemResource
* @param $itemClass
* @param array $sharedFiles
* @param array $dependencies
* @param array $metadataValues
* @param array $createdClasses
* @param boolean $enableMetadataGuardians
* @param boolean $enableMetadataValidators
* @param bool $itemMustExist
* @param bool $itemMustBeOverwritten
* @param array $overwrittenItems
* @return common_report_Report
* @throws common_exception_Error
*/
public function importQtiItem(
$tmpFolder,
Resource $qtiItemResource,
$itemClass,
array &$sharedFiles,
array $dependencies = [],
array $metadataValues = [],
&$createdClasses = [],
$enableMetadataGuardians = true,
$enableMetadataValidators = true,
$itemMustExist = false,
$itemMustBeOverwritten = false,
&$overwrittenItems = []
) {
// if report can't be finished
$report = common_report_Report::createFailure(
__('IMS QTI Item referenced as "%s" cannot be imported.', $qtiItemResource->getIdentifier())
);
$startImportTime = microtime(true);
$lock = $this->createLock(
__CLASS__ .'/'. __METHOD__.'/'.$qtiItemResource->getIdentifier(),
$this->getOption(self::OPTION_IMPORT_LOCK_TTL)
);
$lock->acquire(true);
try {
$qtiService = Service::singleton();
$overWriting = false;
//load the information about resources in the manifest
try {
$resourceIdentifier = $qtiItemResource->getIdentifier();
$this->getMetadataImporter()->setMetadataValues($metadataValues);
$guardian = false;
if ($enableMetadataGuardians === true) {
$guardian = $this->getMetadataImporter()->guard($resourceIdentifier);
if ($guardian !== false) {
// Item found by guardians.
if ($itemMustBeOverwritten === true) {
\common_Logger::i(
'Resource "' . $resourceIdentifier . '" is already stored in the database and will be overwritten.'
);
$overWriting = true;
} else {
\common_Logger::i(
'Resource "' . $resourceIdentifier . '" is already stored in the database and will not be imported.'
);
return common_report_Report::createInfo(
__('The IMS QTI Item referenced as "%s" in the IMS Manifest file was already stored in the Item Bank.', $resourceIdentifier),
new MetadataGuardianResource($guardian)
);
}
}
// Item not found by guardians.
elseif ($itemMustExist === true) {
\common_Logger::i(
'Resource "' . $resourceIdentifier . '" must be already stored in the database in order to proceed.'
);
return new common_report_Report(
common_report_Report::TYPE_ERROR,
__('The IMS QTI Item referenced as "%s" in the IMS Manifest file should have been found the Item Bank. Item not found.', $resourceIdentifier)
);
}
}
if ($enableMetadataValidators === true) {
$validationReport = $this->getMetadataImporter()->validate($resourceIdentifier);
if ($validationReport->getType() !== \common_report_Report::TYPE_SUCCESS) {
$validationReport->setMessage(
__('Item metadata with identifier "%s" is not valid: ', $resourceIdentifier) . $validationReport->getMessage()
);
\common_Logger::i('Item metadata is not valid: ' . $validationReport->getMessage());
return $validationReport;
}
}
$targetClass = $this->getMetadataImporter()->classLookUp($resourceIdentifier, $createdClasses);
$tmpQtiFile = $tmpFolder . helpers_File::urlToPath($qtiItemResource->getFile());
common_Logger::i('file :: ' . $qtiItemResource->getFile());
$qtiModel = $this->createQtiItemModel($tmpQtiFile);
if (
$this->getOption(self::CONFIG_VALIDATE_RESPONSE_PROCESSING) && !$this->validResponseProcessing(
$qtiModel
)
) {
return common_report_Report::createFailure(
__('The IMS QTI Item referenced as "%s" in the IMS Manifest file has incorrect Response Processing and outcomeDeclaration definitions.', $resourceIdentifier)
);
}
if ($guardian !== false && $itemMustBeOverwritten) {
\common_Logger::d(
'Resource "' . $resourceIdentifier . '" will overwrite item with URI ' . $guardian->getUri()
);
$rdfItem = $guardian;
$overwrittenItems[$guardian->getUri()] = $qtiService->backupContentByRdfItem($rdfItem);
$qtiService->saveDataItemToRdfItem($qtiModel, $rdfItem);
} else {
$rdfItem = $this->createRdfItem((($targetClass !== false) ? $targetClass : $itemClass), $qtiModel);
}
// Setting qtiIdentifier property
$qtiIdentifierProperty = new \core_kernel_classes_Property(self::PROPERTY_QTI_ITEM_IDENTIFIER);
$rdfItem->editPropertyValues($qtiIdentifierProperty, $resourceIdentifier);
$itemAssetManager = new AssetManager();
$itemAssetManager->setItemContent($qtiModel->toXML());
$itemAssetManager->setSource($tmpFolder);
/**
* Load asset handler following priority handler defined by you
* The first applicable will be used to import assets
*/
/** Portable element handler */
$peHandler = new PortableAssetHandler($qtiModel, $tmpFolder, dirname($tmpQtiFile));
$itemAssetManager->loadAssetHandler($peHandler);
if ($this->getServiceLocator()->get(\common_ext_ExtensionsManager::SERVICE_ID)->isInstalled('taoMediaManager')) {
// Media manager path where to store the stimulus.
$path = $this->retrieveFullPathLabels($itemClass);
// Uses the first created item's label as the leaf class.
if (is_array($path)) {
$path[] = $rdfItem->getLabel();
}
/** Shared stimulus handler */
$sharedStimulusHandler = new SharedStimulusAssetHandler();
$sharedStimulusHandler
->setQtiModel($qtiModel)
->setItemSource(new ItemMediaResolver($rdfItem, ''))
->setSharedFiles($sharedFiles)
->setParentPath(json_encode($path));
$itemAssetManager->loadAssetHandler($sharedStimulusHandler);
} else {
$handler = new StimulusHandler();
$handler->setQtiItem($qtiModel);
$handler->setItemSource(new LocalItemSource(['item' => $rdfItem]));
$itemAssetManager->loadAssetHandler($handler);
}
/** Local storage handler */
$localHandler = new LocalAssetHandler();
$localHandler->setItemSource(new LocalItemSource(['item' => $rdfItem]));
$itemAssetManager->loadAssetHandler($localHandler);
/** Copy external files to the item directory (preparation before import) */
$itemAssetManager->copyDependencyFiles($qtiItemResource, $dependencies);
$itemAssetManager
->importAuxiliaryFiles($qtiItemResource)
->importDependencyFiles($qtiItemResource, $dependencies);
$itemAssetManager->finalize();
if (isset($sharedStimulusHandler) && $sharedStimulusHandler instanceof SharedStimulusAssetHandler) {
$sharedFiles = $sharedStimulusHandler->getSharedFiles();
}
$qtiModel = $this->createQtiItemModel($itemAssetManager->getItemContent(), false);
$qtiService->saveDataItemToRdfItem($qtiModel, $rdfItem);
$this->getMetadataImporter()->inject($resourceIdentifier, $rdfItem);
$eventManager = ServiceManager::getServiceManager()->get(EventManager::CONFIG_ID);
$eventManager->trigger(new ItemImported($rdfItem, $qtiModel));
// Build report message.
if ($guardian !== false) {
$msg = __('The IMS QTI Item referenced as "%s" in the IMS Manifest file was successfully overwritten.', $qtiItemResource->getIdentifier());
} else {
$msg = __('The IMS QTI Item referenced as "%s" in the IMS Manifest file was successfully imported.', $qtiItemResource->getIdentifier());
}
$this->getItemEventDispatcher()->dispatch($qtiModel, $rdfItem);
$report = common_report_Report::createSuccess($msg, $rdfItem);
} catch (ParsingException $e) {
$message = __('Resource "' . $resourceIdentifier . 'has an error. ') . $e->getUserMessage();
$report = new common_report_Report(common_report_Report::TYPE_ERROR, $message);
} catch (ValidationException $ve) {
$report = common_report_Report::createFailure(
__('IMS QTI Item referenced as "%s" in the IMS Manifest file could not be imported.', $resourceIdentifier)
);
$report->add($ve->getReport());
} catch (XmlStorageException $e) {
$files = [];
$message = __('There are errors in the following shared stimulus : ') . PHP_EOL;
/** @var \LibXMLError $error */
foreach ($e->getErrors() as $error) {
if (!in_array($error->file, $files)) {
$files[] = $error->file;
$message .= '- ' . basename($error->file) . ' :' . PHP_EOL;
}
$message .= $error->message . ' at line : ' . $error->line . PHP_EOL;
}
$message .= __(' For Resource "' . $resourceIdentifier);
$report = new common_report_Report(
common_report_Report::TYPE_ERROR,
$message
);
} catch (PortableElementInvalidModelException $pe) {
$report = common_report_Report::createFailure(
__('IMS QTI Item referenced as "%s" contains a portable element and cannot be imported.', $resourceIdentifier)
);
$report->add($pe->getReport());
if (isset($rdfItem) && !is_null($rdfItem) && $rdfItem->exists() && !$overWriting) {
$rdfItem->delete();
}
} catch (PortableElementException $e) {
// an error occurred during a specific item
if ($e instanceof common_exception_UserReadableException) {
$msg = __('Error on item %1$s : %2$s', $resourceIdentifier, $e->getUserMessage());
} else {
$msg = __('Error on item %s', $resourceIdentifier);
common_Logger::d($e->getMessage());
}
$report = new common_report_Report(common_report_Report::TYPE_ERROR, $msg);
if (isset($rdfItem) && !is_null($rdfItem) && $rdfItem->exists() && !$overWriting) {
$rdfItem->delete();
}
} catch (TemplateException $e) {
$report = new common_report_Report(
common_report_Report::TYPE_ERROR,
__('The IMS QTI Item referenced as "%s" in the IMS Manifest file failed: %s', $resourceIdentifier, $e->getMessage())
);
if (isset($rdfItem) && !is_null($rdfItem) && $rdfItem->exists() && !$overWriting) {
$rdfItem->delete();
}
} catch (Exception $e) {
// an error occurred during a specific item
$report = new common_report_Report(
common_report_Report::TYPE_ERROR,
__("An unknown error occured while importing the IMS QTI Package with identifier: " . $resourceIdentifier)
);
if (isset($rdfItem) && !is_null($rdfItem) && $rdfItem->exists() && !$overWriting) {
$rdfItem->delete();
}
common_Logger::e($e->getMessage());
}
} catch (ValidationException $ve) {
$validationReport = common_report_Report::createFailure("The IMS Manifest file could not be validated");
$validationReport->add($ve->getReport());
$report->setMessage(__("No Items could be imported from the given IMS QTI package."));
$report->setType(common_report_Report::TYPE_ERROR);
$report->add($validationReport);
} catch (common_exception_UserReadableException $e) {
$report = new common_report_Report(common_report_Report::TYPE_ERROR, __($e->getUserMessage()));
$report->add($e);
} finally {
$this->checkImportLockTime($startImportTime, $qtiItemResource->getIdentifier());
$lock->release();
}
return $report;
}
protected function validResponseProcessing(Item $qtiModel)
{
// <outcomeDeclaration> from the items qti
$outcomes = $this->getOutcomesIds($qtiModel);
// <setOutcomeValue> from the responseProcessing (template or body) also items qti
$rules = $this->getSetOutcomeValueIds($qtiModel);
return count(array_diff($rules, $outcomes)) === 0;
}
protected function getOutcomesIds(Item $qtiModel)
{
$declaredIds = [];
/** @var OutcomeDeclaration $outcomeDeclaration */
foreach ($qtiModel->getOutcomes() as $outcomeDeclaration) {
$declaredIds[] = $outcomeDeclaration->getIdentifier();
}
return $declaredIds;
}
protected function getSetOutcomeValueIds($qtiModel)
{
$rules = $this->getResponseProcessingRules($qtiModel);
$ids = [];
foreach ($rules as $rule) {
/** @var QtiComponentCollection $collection */
$collection = $rule->getComponentsByClassName(SetOutcomeValue::CLASS_NAME, true);
while ($collection->valid()) {
/** @var SetOutcomeValue $setOutcomeValue */
$setOutcomeValue = $collection->current();
$ids[] = $setOutcomeValue->getIdentifier();
$collection->next();
}
}
return array_unique($ids);
}
protected function getResponseProcessingRules(Item $qtiModel)
{
$rules = [];
$qti = $qtiModel->toQTI();
$qtiXmlDoc = new XmlDocument();
$qtiXmlDoc->loadFromString($qti);
$itemSession = new AssessmentItemSession($qtiXmlDoc->getDocumentComponent(), new SessionManager());
$responseProcessing = $itemSession->getAssessmentItem()->getResponseProcessing();
// Some items (especially to collect information) have no response processing!
if (
$responseProcessing !== null && ($responseProcessing->hasTemplate(
) === true || $responseProcessing->hasTemplateLocation() === true || count(
$responseProcessing->getResponseRules()
) > 0)
) {
$engine = new ResponseProcessingEngine($responseProcessing, $itemSession);
$rules = $engine->getResponseProcessingRules();
}
return $rules;
}
/**
* Import metadata to a given QTI Item.
*
* @param MetadataValue[] $metadataValues An array of MetadataValue objects.
* @param Resource $qtiResource The object representing the QTI Resource, from an IMS Manifest perspective.
* @param core_kernel_classes_Resource $resource The object representing the target QTI Item in the Ontology.
* @param MetadataInjector[] $ontologyInjectors Implementations of MetadataInjector that will take care to inject the metadata values in the appropriate Ontology Resource Properties.
* @throws MetadataInjectionException If an error occurs while importing the metadata.
* @deprecated use MetadataService::getImporter::inject()
*
*/
public function importResourceMetadata(
array $metadataValues,
Resource $qtiResource,
core_kernel_classes_Resource $resource,
array $ontologyInjectors = []
) {
// Filter metadata values for this given item.
$identifier = $qtiResource->getIdentifier();
if (isset($metadataValues[$identifier]) === true) {
\common_Logger::i("Preparing Metadata Values for resource '${identifier}'...");
$values = $metadataValues[$identifier];
foreach ($ontologyInjectors as $injector) {
$valuesCount = count($values);
$injectorClass = get_class($injector);
\common_Logger::i(
"Attempting to inject ${valuesCount} Metadata Values in database for resource '${identifier}' with Metadata Injector '${injectorClass}'."
);
$injector->inject($resource, [$identifier => $values]);
}
}
}
/**
* @param array $items
* @param common_report_Report $report
* @param array $createdClasses (optional)
* @throws common_exception_Error
*/
protected function rollback(
array $items,
common_report_Report $report,
array $createdClasses = [],
array $overwrittenItems = []
) {
$overwrittenItemsIds = array_keys($overwrittenItems);
$qtiService = Service::singleton();
// 1. Simply delete items that were not involved in overwriting.
foreach ($items as $id => $item) {
if (!$item instanceof MetadataGuardianResource && !in_array($item->getUri(), $overwrittenItemsIds)) {
\common_Logger::d("Deleting item '${id}'...");
@taoItems_models_classes_ItemsService::singleton()->deleteResource($item);
$report->add(
new common_report_Report(
common_report_Report::TYPE_WARNING,
__('The IMS QTI Item referenced as "%s" in the IMS Manifest was successfully rolled back.', $id)
)
);
}
}
// 2. Restore overwritten item contents.
foreach ($overwrittenItems as $overwrittenItemId => $backupName) {
common_Logger::d("Restoring content for item '${overwrittenItemId}'...");
@$qtiService->restoreContentByRdfItem(new core_kernel_classes_Resource($overwrittenItemId), $backupName);
}
foreach ($createdClasses as $createdClass) {
@$createdClass->delete();
}
}
/**
* Get the lom metadata importer
*
* @return MetadataImporter
*/
protected function getMetadataImporter()
{
if (!$this->metadataImporter) {
$this->metadataImporter = $this->getServiceLocator()->get(MetadataService::SERVICE_ID)->getImporter();
}
return $this->metadataImporter;
}
/**
* Retrieve the labels of all parent classes up to base item class.
*
* Return values: an empty string if the $class parameter is not a class object
* an empty array if the $class parameter is the base item class
* an array in other cases, ordered from base item class to the class given as parameter
*
* @param mixed $class Class from which to retrieve.
* @return array|string
*/
public function retrieveFullPathLabels($class)
{
if (!$class instanceof core_kernel_classes_Class) {
return '';
}
$labels = [];
while ($class->getUri() !== TaoOntology::CLASS_URI_ITEM) {
$labels []= $class->getLabel();
$parentClasses = $class->getParentClasses();
$class = reset($parentClasses);
}
return array_reverse($labels);
}
private function getItemEventDispatcher(): UpdatedItemEventDispatcher
{
return $this->getServiceLocator()->get(UpdatedItemEventDispatcher::class);
}
}