Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow downgrade conversion from QTI 3.0 to QTI 2.2 #2547

Open
wants to merge 7 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"oat-sa/generis": ">=16.0.0",
"oat-sa/tao-core": ">=54.27.0",
"oat-sa/extension-tao-item": ">=12.4.0",
"oat-sa/extension-tao-itemqti": ">=30.23.0",
"oat-sa/extension-tao-itemqti": ">=30.26.0",
"oat-sa/extension-tao-test": ">=16.4.3",
"oat-sa/extension-tao-delivery": ">=15.0.0",
"oat-sa/extension-tao-outcome": ">=13.0.0",
Expand Down
35 changes: 35 additions & 0 deletions models/classes/Qti/Converter/AssessmentSectionConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?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) 2024 (original work) Open Assessment Technologies SA;
*/

declare(strict_types=1);

namespace oat\taoQtiTest\models\Qti\Converter;

use oat\taoQtiItem\model\qti\converter\AbstractQtiConverter;

class AssessmentSectionConverter extends AbstractQtiConverter
{
private const ROOT_ELEMENT = 'qti-assessment-section';

protected function getRootElement(): string
{
return self::ROOT_ELEMENT;
}
}
35 changes: 35 additions & 0 deletions models/classes/Qti/Converter/TestConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?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) 2024 (original work) Open Assessment Technologies SA;
*/

declare(strict_types=1);

namespace oat\taoQtiTest\models\Qti\Converter;

use oat\taoQtiItem\model\qti\converter\AbstractQtiConverter;

class TestConverter extends AbstractQtiConverter
{
private const ROOT_ELEMENT = 'qti-assessment-test';

protected function getRootElement(): string
{
return self::ROOT_ELEMENT;
}
}
20 changes: 20 additions & 0 deletions models/classes/Qti/ServiceProvider/QtiServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@

use oat\generis\model\DependencyInjection\ContainerServiceProviderInterface;
use oat\oatbox\log\LoggerService;
use oat\taoQtiItem\model\qti\converter\CaseConversionService;
use oat\taoQtiItem\model\ValidationService;
use oat\taoQtiTest\models\Qti\Converter\AssessmentSectionConverter;
use oat\taoQtiTest\models\Qti\Converter\TestConverter;
use oat\taoQtiTest\models\Qti\Identifier\Service\QtiIdentifierSetter;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use taoQtiTest_models_classes_QtiTestService;
Expand All @@ -42,5 +46,21 @@ public function __invoke(ContainerConfigurator $configurator): void
service(taoQtiTest_models_classes_QtiTestService::class),
service(LoggerService::SERVICE_ID),
]);

$services
->set(TestConverter::class)
->args([
service(CaseConversionService::class),
service(ValidationService::SERVICE_ID)
])
->public();

$services
->set(AssessmentSectionConverter::class)
->args([
service(CaseConversionService::class),
service(ValidationService::SERVICE_ID)
])
->public();
}
}
104 changes: 76 additions & 28 deletions models/classes/class.QtiTestService.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,42 +22,47 @@
use oat\oatbox\filesystem\Directory;
use oat\oatbox\filesystem\File;
use oat\oatbox\filesystem\FileSystemService;
use oat\oatbox\reporting\Report;
use oat\tao\model\IdentifierGenerator\Generator\IdentifierGeneratorInterface;
use oat\tao\model\IdentifierGenerator\Generator\IdentifierGeneratorProxy;
use oat\tao\model\resources\ResourceAccessDeniedException;
use oat\tao\model\resources\SecureResourceServiceInterface;
use oat\tao\model\TaoOntology;
use oat\taoItems\model\Command\DeleteItemCommand;
use oat\taoQtiItem\model\qti\converter\ManifestConverter;
use oat\taoQtiItem\model\qti\ImportService;
use oat\taoQtiItem\model\qti\metadata\importer\MetadataImporter;
use oat\taoQtiItem\model\qti\metadata\imsManifest\MetaMetadataExtractor;
use oat\taoQtiItem\model\qti\metadata\importer\MetaMetadataImportMapper;
use oat\taoQtiItem\model\qti\metadata\importer\PropertyDoesNotExistException;
use oat\taoQtiItem\model\qti\metadata\imsManifest\MetaMetadataExtractor;
use oat\taoQtiItem\model\qti\metadata\MetadataGuardianResource;
use oat\taoQtiItem\model\qti\metadata\MetadataService;
use oat\taoQtiItem\model\qti\metadata\ontology\MappedMetadataInjector;
use oat\taoQtiItem\model\qti\PackageParser;
use oat\taoQtiItem\model\qti\Resource;
use oat\taoQtiItem\model\qti\Service;
use oat\taoQtiTest\models\cat\AdaptiveSectionInjectionException;
use oat\taoQtiTest\models\cat\CatEngineNotFoundException;
use oat\taoQtiTest\models\cat\CatService;
use oat\taoQtiTest\models\classes\event\TestImportedEvent;
use oat\taoQtiTest\models\metadata\MetadataTestContextAware;
use oat\taoQtiTest\models\Qti\Converter\AssessmentSectionConverter;
use oat\taoQtiTest\models\Qti\Converter\TestConverter;
use oat\taoQtiTest\models\render\QtiPackageImportPreprocessing;
use oat\taoQtiTest\models\test\AssessmentTestXmlFactory;
use oat\taoTests\models\event\TestUpdatedEvent;
use Psr\Container\ContainerInterface;
use qtism\common\utils\Format;
use qtism\data\AssessmentItemRef;
use qtism\data\AssessmentSectionRef;
use qtism\data\QtiComponentCollection;
use qtism\data\SectionPartCollection;
use qtism\data\storage\StorageException;
use qtism\data\storage\xml\marshalling\UnmarshallingException;
use qtism\data\storage\xml\XmlDocument;
use qtism\data\storage\xml\XmlStorageException;
use taoTests_models_classes_TestsService as TestService;
use oat\oatbox\reporting\Report;
use taoQtiTest_models_classes_import_TestImportForm as TestImportForm;
use taoTests_models_classes_TestsService as TestService;

/**
* the QTI TestModel service.
Expand Down Expand Up @@ -201,7 +206,7 @@ protected function setDefaultModel($test): void
* Save the json formated test into the test resource.
*
* @param core_kernel_classes_Resource $test
* @param string $json
* @param string $json
*
* @return bool true if saved
*
Expand All @@ -212,7 +217,7 @@ public function saveJsonTest(core_kernel_classes_Resource $test, $json): bool
{
$saved = false;

if (! empty($json)) {
if (!empty($json)) {
$this->verifyItemPermissions($test, $json);

$doc = $this->getDoc($test);
Expand Down Expand Up @@ -266,7 +271,7 @@ public function setItems(core_kernel_classes_Resource $test, array $items)
return false;
}

/**
/**
* Save the QTI test : set the items sequence and some options.
*
* @param core_kernel_classes_Resource $test A Resource describing a QTI Assessment Test.
Expand All @@ -280,7 +285,7 @@ public function save(core_kernel_classes_Resource $test, array $items)
try {
$doc = $this->getDoc($test);
$this->setItemsToDoc($doc, $items);
$saved = $this->saveDoc($test, $doc);
$saved = $this->saveDoc($test, $doc);
} catch (StorageException $e) {
throw new taoQtiTest_models_classes_QtiTestServiceException(
"An error occured while dealing with the QTI-XML test: " . $e->getMessage(),
Expand All @@ -306,7 +311,7 @@ public function getIdentifierFor(XmlDocument $doc, $qtiType)
do {
$identifier = $this->generateIdentifier($doc, $qtiType, $index);
$index++;
} while (! $this->isIdentifierUnique($components, $identifier));
} while (!$this->isIdentifierUnique($components, $identifier));

return $identifier;
}
Expand Down Expand Up @@ -363,7 +368,7 @@ public function importMultipleTests(
?string $packageLabel = null
) {
$testClass = $targetClass;
$report = new common_report_Report(common_report_Report::TYPE_INFO);
$report = new Report(Report::TYPE_INFO);
$validPackage = false;
$validManifest = false;
$testsFound = false;
Expand All @@ -381,24 +386,30 @@ public function importMultipleTests(
// phpcs:enable Generic.Files.LineLength

try {
$qtiPackageParser = new taoQtiTest_models_classes_PackageParser($file);
$qtiPackageParser = new PackageParser($file);
$qtiPackageParser->validate();
$validPackage = true;
} catch (Exception $e) {
$report->add(common_report_Report::createFailure($invalidArchiveMsg));
$report->add(Report::createError($invalidArchiveMsg));
}

// Validate the manifest (well formed XML, valid against the schema).
if ($validPackage === true) {
$folder = $qtiPackageParser->extract();

if (is_dir($folder) === false) {
$report->add(common_report_Report::createFailure($invalidArchiveMsg));
$report->add(Report::createError($invalidArchiveMsg));
} else {
$qtiManifestParser = new taoQtiTest_models_classes_ManifestParser($folder . 'imsmanifest.xml');
$file = $folder . 'imsmanifest.xml';
$qtiManifestParser = new taoQtiTest_models_classes_ManifestParser($file);
$this->propagate($qtiManifestParser);
// For taoSetup PsrContainer is not available
// It is not required to perform manifest conversion in this process
// therefore we can skip it during taoSetup
if ($this->getPsrContainer()->has(ManifestConverter::class)) {
$this->getManifestConverter()->convertToQti2($file, $qtiManifestParser);
}
// We validate manifest file against QTI 3.0
$qtiManifestParser->validate();

if ($qtiManifestParser->isValid() === true) {
$validManifest = true;

Expand All @@ -411,10 +422,10 @@ public function importMultipleTests(

if ($testsFound !== true) {
$report->add(
common_report_Report::createFailure(
// phpcs:disable Generic.Files.LineLength
Report::createError(
// phpcs:disable Generic.Files.LineLength
__("Package is valid but no tests were found. Make sure that it contains valid QTI tests.")
// phpcs:enable Generic.Files.LineLength
// phpcs:enable Generic.Files.LineLength
)
);
} else {
Expand Down Expand Up @@ -447,7 +458,7 @@ public function importMultipleTests(
}
} else {
$msg = __("The 'imsmanifest.xml' file found in the archive is not valid.");
$report->add(common_report_Report::createFailure($msg));
$report->add(Report::createError($msg));
}

// Cleanup the folder where the archive was extracted.
Expand All @@ -457,10 +468,10 @@ public function importMultipleTests(

if ($report->containsError() === true) {
$report->setMessage(__('The IMS QTI Test Package could not be imported.'));
$report->setType(common_report_Report::TYPE_ERROR);
$report->setType(Report::TYPE_ERROR);
} else {
$report->setMessage(__('IMS QTI Test Package successfully imported.'));
$report->setType(common_report_Report::TYPE_SUCCESS);
$report->setType(Report::TYPE_SUCCESS);
}

if (
Expand Down Expand Up @@ -577,7 +588,7 @@ protected function importTest(

// Create the report that will hold information about the import
// of $qtiTestResource in TAO.
$report = new common_report_Report(common_report_Report::TYPE_INFO);
$report = new Report(Report::TYPE_INFO);

// Load and validate the manifest
$qtiManifestParser = new taoQtiTest_models_classes_ManifestParser($folder . 'imsmanifest.xml');
Expand Down Expand Up @@ -615,17 +626,25 @@ protected function importTest(
// -- Check if the file referenced by the test QTI resource exists.
if (is_readable($expectedTestFile) === false) {
$report->add(
common_report_Report::createFailure(
Report::createError(
__('No file found at location "%s".', $qtiTestResource->getFile())
)
);
} else {
//Convert to QTI 2.2
if ($this->getPsrContainer()->has(TestConverter::class)) {
$this->getTestConverter()->convertToQti2($expectedTestFile);
}
// -- Load the test in a QTISM flavour.
$testDefinition = new XmlDocument();

try {
$testDefinition->load($expectedTestFile, true);

$this->convertAssessmentSectionRefs(
$testDefinition->getDocumentComponent()
->getComponentsByClassName('assessmentSectionRef'),
$folder
);
// If any, assessmentSectionRefs will be resolved and included as part of the main test definition.
$testDefinition->includeAssessmentSectionRefs(true);
$testLabel = $packageLabel ?? $this->getTestLabel($reportCtx->testMetadata, $testDefinition);
Expand Down Expand Up @@ -1079,7 +1098,6 @@ public function getTestFile(core_kernel_classes_Resource $test)
*/
public function getDoc(core_kernel_classes_Resource $test)
{

$doc = new XmlDocument('2.1');
$doc->loadFromString($this->getQtiTestFile($test)->read());
return $doc;
Expand Down Expand Up @@ -1165,7 +1183,7 @@ private function setItemsToDoc(XmlDocument $doc, array $items, $sectionIndex = 0
* Get root qti test directory or crate if not exists
*
* @param core_kernel_classes_Resource $test
* @param boolean $createTestFile Whether or not create an empty QTI XML test file. Default is
* @param boolean $createTestFile Whether or not create an empty QTI XML test file. Default is
* (boolean) true.
*
* @return Directory
Expand Down Expand Up @@ -1271,7 +1289,7 @@ private function saveDoc(core_kernel_classes_Resource $test, XmlDocument $doc)
* Create the default content directory of a QTI test.
*
* @param core_kernel_classes_Resource $test
* @param boolean $createTestFile Whether or not create an empty QTI XML test file. Default is (boolean) true.
* @param boolean $createTestFile Whether or not create an empty QTI XML test file. Default is (boolean) true.
* @param boolean $preventOverride Prevent data to be overriden Default is (boolean) true.
*
* @return Directory the content directory
Expand Down Expand Up @@ -1433,7 +1451,7 @@ public function getQtiTestTemplateFileAsString()
*/
protected function getMetadataImporter()
{
if (! $this->metadataImporter) {
if (!$this->metadataImporter) {
$this->metadataImporter = $this->getServiceLocator()->get(MetadataService::SERVICE_ID)->getImporter();
}
return $this->metadataImporter;
Expand Down Expand Up @@ -1601,4 +1619,34 @@ private function getTestLabel(array $testMetadata, XmlDocument $testDefinition):

return reset($labelMetadata)->getValue();
}

private function getManifestConverter(): ManifestConverter
{
return $this->getPsrContainer()->get(ManifestConverter::class);
}

private function getTestConverter(): TestConverter
{
return $this->getPsrContainer()->get(TestConverter::class);
}

private function getSectionConverter(): AssessmentSectionConverter
{
return $this->getPsrContainer()->get(AssessmentSectionConverter::class);
}

/**
* @param AssessmentSectionRef[] $testDefinition
*/
private function convertAssessmentSectionRefs(QtiComponentCollection $assessmentSectionRefs, string $folder): void
{
if(!$this->getPsrContainer()->has(AssessmentSectionConverter::class)) {
return;
}

foreach ($assessmentSectionRefs as $assessmentSectionRef) {
$file = $folder . $assessmentSectionRef->getHref();
$this->getSectionConverter()->convertToQti2($file);
}
}
}
Loading