From 59a426fd1cf4b3e492e57b210d1207079be8ae76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Tue, 9 Apr 2024 11:09:25 +0200 Subject: [PATCH 01/30] feat: export test will inject properties description in root metadata as metaMetadta lom objects --- manifest.php | 3 +- .../export/AbstractQtiTestExporter.php | 13 +- .../metadata/GenericLomOntologyExtractor.php | 83 +++++++++++++ .../metadata/GenericMetadataExtractor.php | 35 ++++++ .../classes/metadata/MetadataLomService.php | 57 +++++++++ .../metadata/MetadataServiceProvider.php | 60 +++++++++ .../metadata/metaMetadata/PropertyMapper.php | 97 +++++++++++++++ .../GenericLomOntologyExtractorTest.php | 112 +++++++++++++++++ .../metadata/MetadataLomServiceTest.php | 63 ++++++++++ .../classes/metadata/imsManifestMetadata.xml | 23 ++++ .../metaMetadata/PropertyMapperTest.php | 114 ++++++++++++++++++ 11 files changed, 658 insertions(+), 2 deletions(-) create mode 100644 models/classes/metadata/GenericLomOntologyExtractor.php create mode 100644 models/classes/metadata/GenericMetadataExtractor.php create mode 100644 models/classes/metadata/MetadataLomService.php create mode 100644 models/classes/metadata/MetadataServiceProvider.php create mode 100644 models/classes/metadata/metaMetadata/PropertyMapper.php create mode 100644 test/unit/models/classes/metadata/GenericLomOntologyExtractorTest.php create mode 100644 test/unit/models/classes/metadata/MetadataLomServiceTest.php create mode 100644 test/unit/models/classes/metadata/imsManifestMetadata.xml create mode 100644 test/unit/models/classes/metadata/metaMetadata/PropertyMapperTest.php diff --git a/manifest.php b/manifest.php index f6cfc386b..6c46ba795 100755 --- a/manifest.php +++ b/manifest.php @@ -182,6 +182,7 @@ CustomInteractionPostProcessingServiceProvider::class, ItemsReferencesServiceProvider::class, TestQtiServiceProvider::class, - TestSessionStateServiceProvider::class + TestSessionStateServiceProvider::class, + MetadataServiceProvider::class ], ]; diff --git a/models/classes/export/AbstractQtiTestExporter.php b/models/classes/export/AbstractQtiTestExporter.php index 0de4e8c55..70309f582 100644 --- a/models/classes/export/AbstractQtiTestExporter.php +++ b/models/classes/export/AbstractQtiTestExporter.php @@ -32,6 +32,7 @@ use DOMXPath; use oat\oatbox\reporting\Report; use oat\oatbox\reporting\ReportInterface; +use oat\taoQtiTest\models\classes\metadata\GenericLomOntologyExtractor; use qtism\data\storage\xml\marshalling\MarshallingException; use qtism\data\storage\xml\XmlDocument; use oat\oatbox\filesystem\Directory; @@ -172,7 +173,12 @@ public function export(array $options = []): Report $this->exportTest($report->getData()); // 3. Export test metadata to manifest - $this->getMetadataExporter()->export($this->getItem(), $this->getManifest()); + $this->getMetadataExporter()->export($this->getItem(), $this->getManifest());; + $this->genericLomOntologyExtractor()->extract( + array_merge([$this->getItem()], $this->getItems()), + $this->getManifest() + ); + // 4. Persist manifest in archive. $this->getZip()->addFromString('imsmanifest.xml', $this->getManifest()->saveXML()); @@ -350,4 +356,9 @@ protected function getServiceManager(): ServiceManager { return ServiceManager::getServiceManager(); } + + private function genericLomOntologyExtractor(): GenericLomOntologyExtractor + { + return $this->getServiceManager()->getContainer()->get(GenericLomOntologyExtractor::class); + } } diff --git a/models/classes/metadata/GenericLomOntologyExtractor.php b/models/classes/metadata/GenericLomOntologyExtractor.php new file mode 100644 index 000000000..3513136ab --- /dev/null +++ b/models/classes/metadata/GenericLomOntologyExtractor.php @@ -0,0 +1,83 @@ +ontology = $ontology; + $this->propertyMapper = $propertyMapper; + $this->metadataLomService = $metadataLomService; + } + + /** + * @param Resource[] $resourceCollection + * @throws MetadataExtractionException + */ + public function extract(array $resourceCollection, DOMDocument $manifest): void + { + $properties = []; + + foreach ($resourceCollection as $resource) { + if (!$resource instanceof Resource) { + throw new MetadataExtractionException( + __('The given target is not an instance of core_kernel_classes_Resource') + ); + } + + foreach ($resource->getRdfTriples() as $triple) { + if ($this->mappingRequired($properties, $triple)) { + $properties[] = $this->propertyMapper + ->getMetadataProperties( + $this->ontology->getProperty($triple->predicate) + ); + } + } + } + + $this->metadataLomService->addPropertiesToMetadataBlock($properties, $manifest); + } + + /** + * Mapping action only applies for confirmed properties that are not already mapped + */ + private function mappingRequired(array $properties, Triple $triple): bool + { + return $this->ontology->getProperty($triple->predicate)->isProperty() && + array_filter($properties, function ($property) use ($triple) { + return $property['uri'] === $triple->predicate; + }) === []; + } +} diff --git a/models/classes/metadata/GenericMetadataExtractor.php b/models/classes/metadata/GenericMetadataExtractor.php new file mode 100644 index 000000000..54ff555bd --- /dev/null +++ b/models/classes/metadata/GenericMetadataExtractor.php @@ -0,0 +1,35 @@ +getElementsByTagName('metadata'); + + if ($metadataBlock === null) { + $metadataBlock = $manifest->createElement('metadata'); + $manifest->documentElement->appendChild($metadataBlock); + } + + $metadataBlock->item(0) + ->appendChild($manifest->createElement('imsmd:lom')) + ->appendChild($manifest->createElement('imsmd:metaMetadata')) + ->appendChild($manifest->createElement('extension')) + ->appendChild($manifest->createElement('customProperties')); + + foreach ($properties as $property) { + $propertyNode = $manifest->createElement('property'); + foreach ($property as $key => $value) { + $propertyNode->appendChild($manifest->createElement($key, $value)); + } + $metadataBlock->item(0) + ->getElementsByTagName('customProperties') + ->item(0) + ->appendChild($propertyNode); + } + } +} diff --git a/models/classes/metadata/MetadataServiceProvider.php b/models/classes/metadata/MetadataServiceProvider.php new file mode 100644 index 000000000..bd864a7bd --- /dev/null +++ b/models/classes/metadata/MetadataServiceProvider.php @@ -0,0 +1,60 @@ +services(); + $services->set(MetadataLomService::class, MetadataLomService::class); + + $services->set(PropertyMapper::class, PropertyMapper::class) + ->args([ + service(Ontology::SERVICE_ID), + [ + 'label' => RDFS_LABEL, + 'domain' => RDFS_DOMAIN, + 'alias' => GenerisRdf::PROPERTY_ALIAS, + 'multiple' => GenerisRdf::PROPERTY_MULTIPLE + ] + ]); + + $services + ->set(GenericLomOntologyExtractor::class, GenericMetadataExtractor::class) + ->public() + ->args([ + service(Ontology::SERVICE_ID), + service(PropertyMapper::class), + service(MetadataLomService::class) + ]); + + } +} diff --git a/models/classes/metadata/metaMetadata/PropertyMapper.php b/models/classes/metadata/metaMetadata/PropertyMapper.php new file mode 100644 index 000000000..d2a86316d --- /dev/null +++ b/models/classes/metadata/metaMetadata/PropertyMapper.php @@ -0,0 +1,97 @@ +metaMetadataCollectionToExport = $metaMetadataCollectionToExport; + $this->ontology = $ontology; + } + + public function getMetadataProperties(Property $property): array + { + $fields = []; + + foreach ($this->metaMetadataCollectionToExport as $key => $stringProperty) { + $fields['uri'] = $property->getUri(); + $metaProperty = $property->getOnePropertyValue(new Property($stringProperty)); + if ($metaProperty !== null) { + $fields[$key] = $metaProperty instanceof Resource + ? $metaProperty->getUri() + : (string) $metaProperty; + } + } + + if (!$this->isIgnoredForCollectionGathering($property)) { + $fields[self::DATATYPE_CHECKSUM] = $this->getRangeChecksum($property); + } + + return $fields; + } + + private function getRangeChecksum(Property $property): string + { + $checksum = ''; + + $listValues = array_filter($property->getRange()->getNestedResources(), function ($range) { + return $range['isclass'] === 0; + }); + + if (empty($listValues)) { + return ''; + } + + foreach ($listValues as $value) { + $checksum .= $this->ontology->getResource($value['id'])->getLabel(); + } + + return sha1($checksum); + } + + private function isIgnoredForCollectionGathering(Property $property): bool + { + return in_array($property->getUri(), $this->getIgnoredProperties()); + } + + private function getIgnoredProperties(): array + { + return [ + OntologyRdf::RDF_TYPE, + taoTests_models_classes_TestsService::PROPERTY_TEST_TESTMODEL, + RDFS_LABEL + ]; + } +} diff --git a/test/unit/models/classes/metadata/GenericLomOntologyExtractorTest.php b/test/unit/models/classes/metadata/GenericLomOntologyExtractorTest.php new file mode 100644 index 000000000..e7e8f3089 --- /dev/null +++ b/test/unit/models/classes/metadata/GenericLomOntologyExtractorTest.php @@ -0,0 +1,112 @@ +ontologyMock = $this->createMock(Ontology::class); + $this->propertyMapperMock = $this->createMock(PropertyMapper::class); + $this->metadataLomServiceMock = $this->createMock(MetadataLomService::class); + + $this->subject = new GenericLomOntologyExtractor( + $this->ontologyMock, + $this->propertyMapperMock, + $this->metadataLomServiceMock + ); + } + + /** + * @noinspection PhpParamsInspection + */ + public function testExtractExceptionForCollectionWithNonResourceContent(): void + { + $this->expectException(MetadataExtractionException::class); + $resourceCollection = ['ResourceThatIsAString']; + $manifest = new DOMDocument(); + + $this->subject->extract($resourceCollection, $manifest); + } + + public function testExtract(): void + { + $resourceMock = $this->createMock(Resource::class); + $tripleMock1 = $this->createMock(Triple::class); + $tripleMock2 = $this->createMock(Triple::class); + $tripleMock3 = $this->createMock(Triple::class); + $propertyMock = $this->createMock(Property::class); + $manifest = new DOMDocument(); + $tripleMock1->predicate = 'predicate1'; + $tripleMock2->predicate = 'predicate2'; + $tripleMock3->predicate = 'predicate3'; + + $resourceMock->method('getRdfTriples')->willReturn([ + $tripleMock1, + $tripleMock2, + $tripleMock3 + ]); + + $propertyMock + ->method('isProperty') + ->willReturn(true); + + $this->ontologyMock + ->method('getProperty') + ->willReturn($propertyMock); + + $this->propertyMapperMock + ->method('getMetadataProperties') + ->willReturnOnConsecutiveCalls( + ['uri' => 'predicate1'], + ['uri' => 'predicate2'], + ['uri' => 'predicate3'], + ['uri' => 'predicate1'], + ['uri' => 'predicate2'], + ['uri' => 'predicate3'] + ); + + $this->metadataLomServiceMock + ->expects($this->once()) + ->method('addPropertiesToMetadataBlock') + ->with([ + ['uri' => 'predicate1'], + ['uri' => 'predicate2'], + ['uri' => 'predicate3'], + ], $manifest); + + $this->subject->extract([$resourceMock, $resourceMock], $manifest); + } +} diff --git a/test/unit/models/classes/metadata/MetadataLomServiceTest.php b/test/unit/models/classes/metadata/MetadataLomServiceTest.php new file mode 100644 index 000000000..d0ab90fe5 --- /dev/null +++ b/test/unit/models/classes/metadata/MetadataLomServiceTest.php @@ -0,0 +1,63 @@ +metadataLomService = new MetadataLomService(); + } + + public function testAddPropertiesToMetadataBlock(): void + { + $manifest = new DOMDocument(); + $manifest->appendChild($manifest->createElement('metadata')); + + $proerties = [ + [ + 'label' => 'label_example', + 'domain' => 'domain_example', + 'alias' => 'alias_example', + 'multiple' => 'multiple_example', + ], + [ + 'label' => 'label_another_example', + 'domain' => 'domain_another_example', + 'alias' => 'alias_another_example', + 'multiple' => 'multiple_another_example', + ] + ]; + + $this->metadataLomService->addPropertiesToMetadataBlock($proerties, $manifest); + self::assertXmlStringEqualsXmlFile( + __DIR__ . '/imsManifestMetadata.xml', + $manifest->saveXML() + ); + } + +} diff --git a/test/unit/models/classes/metadata/imsManifestMetadata.xml b/test/unit/models/classes/metadata/imsManifestMetadata.xml new file mode 100644 index 000000000..5fdf00e69 --- /dev/null +++ b/test/unit/models/classes/metadata/imsManifestMetadata.xml @@ -0,0 +1,23 @@ + + + + + + + + + domain_example + alias_example + multiple_example + + + + domain_another_example + alias_another_example + multiple_another_example + + + + + + diff --git a/test/unit/models/classes/metadata/metaMetadata/PropertyMapperTest.php b/test/unit/models/classes/metadata/metaMetadata/PropertyMapperTest.php new file mode 100644 index 000000000..7d7964978 --- /dev/null +++ b/test/unit/models/classes/metadata/metaMetadata/PropertyMapperTest.php @@ -0,0 +1,114 @@ +metaMetadataCollectionToExport = [ + 'label' => RDFS_LABEL, + 'domain' => RDFS_DOMAIN, + 'alias' => GenerisRdf::PROPERTY_ALIAS, + 'multiple' => GenerisRdf::PROPERTY_MULTIPLE + ]; + + $this->ontology = $this->createMock(Ontology::class); + $this->subject = new PropertyMapper($this->ontology, $this->metaMetadataCollectionToExport); + } + + public function testGetMetadataProperties(): void + { + $classMock = $this->createMock(core_kernel_classes_Class::class); + $property = $this->createMock(Property::class); + $resourceMock = $this->createMock(Resource::class); + $property->method('getUri')->willReturn('uri'); + $resourceMock->method('getUri')->willReturn('resource_uri'); + + $property + ->method('getOnePropertyValue') + ->willReturnOnConsecutiveCalls( + $resourceMock, + 'value', + new core_kernel_classes_Literal('literal_value'), + null + ); + + + $property->method('getRange')->willReturn($classMock); + $classMock->method('getNestedResources')->willReturn( + [ + [ + 'id' => 'id', + 'isclass' => 1, + ], + [ + 'id' => 'non_class_id', + 'isclass' => 0, + ], + [ + 'id' => 'non_class_id_2', + 'isclass' => 0, + ] + ] + ); + + $this->ontology + ->expects($this->exactly(2)) + ->method('getResource') + ->with($this->logicalOr( + $this->equalTo('non_class_id'), + $this->equalTo('non_class_id_2') + )) + ->willReturn($resourceMock); + + $resourceMock->expects($this->exactly(2)) + ->method('getLabel') + ->willReturn('label'); + + + $result = $this->subject->getMetadataProperties($property); + + $this->assertIsArray($result); + $this->assertArrayHasKey('uri', $result); + $this->assertArrayHasKey('label', $result); + $this->assertArrayHasKey('domain', $result); + $this->assertArrayHasKey('alias', $result); + $this->assertArrayNotHasKey('multiple', $result); + $this->assertArrayHasKey(PropertyMapper::DATATYPE_CHECKSUM, $result); + $this->assertEquals('uri', $result['uri']); + $this->assertEquals('resource_uri', $result['label']); + $this->assertEquals('value', $result['domain']); + $this->assertEquals('literal_value', $result['alias']); + $this->assertEquals('c315a4bd4fa0f4479b1ea4b5998aa548eed3b670', $result['checksum']); + } +} From 393bdbb72bcdd21fc6187cc0c96e79adb17a261b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Tue, 9 Apr 2024 11:22:19 +0200 Subject: [PATCH 02/30] feat: fix phpcs issues --- manifest.php | 1 + models/classes/metadata/MetadataServiceProvider.php | 2 +- .../models/classes/metadata/GenericLomOntologyExtractorTest.php | 1 - test/unit/models/classes/metadata/MetadataLomServiceTest.php | 1 - 4 files changed, 2 insertions(+), 3 deletions(-) diff --git a/manifest.php b/manifest.php index 6c46ba795..6840e9446 100755 --- a/manifest.php +++ b/manifest.php @@ -21,6 +21,7 @@ use oat\tao\model\user\TaoRoles; use oat\taoQtiTest\model\Container\TestQtiServiceProvider; +use oat\taoQtiTest\models\classes\metadata\MetadataServiceProvider; // phpcs:disable Generic.Files.LineLength use oat\taoQtiTest\models\classes\render\CustomInteraction\ServiceProvider\CustomInteractionPostProcessingServiceProvider; // phpcs:enable Generic.Files.LineLength diff --git a/models/classes/metadata/MetadataServiceProvider.php b/models/classes/metadata/MetadataServiceProvider.php index bd864a7bd..a21069dbf 100644 --- a/models/classes/metadata/MetadataServiceProvider.php +++ b/models/classes/metadata/MetadataServiceProvider.php @@ -27,6 +27,7 @@ use oat\generis\model\GenerisRdf; use oat\taoQtiTest\models\classes\metadata\metaMetadata\PropertyMapper; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + use function Symfony\Component\DependencyInjection\Loader\Configurator\service; class MetadataServiceProvider implements ContainerServiceProviderInterface @@ -55,6 +56,5 @@ public function __invoke(ContainerConfigurator $configurator): void service(PropertyMapper::class), service(MetadataLomService::class) ]); - } } diff --git a/test/unit/models/classes/metadata/GenericLomOntologyExtractorTest.php b/test/unit/models/classes/metadata/GenericLomOntologyExtractorTest.php index e7e8f3089..bc0c80bc0 100644 --- a/test/unit/models/classes/metadata/GenericLomOntologyExtractorTest.php +++ b/test/unit/models/classes/metadata/GenericLomOntologyExtractorTest.php @@ -33,7 +33,6 @@ use core_kernel_classes_Property as Property; use core_kernel_classes_Triple as Triple; - class GenericLomOntologyExtractorTest extends TestCase { public function setUp(): void diff --git a/test/unit/models/classes/metadata/MetadataLomServiceTest.php b/test/unit/models/classes/metadata/MetadataLomServiceTest.php index d0ab90fe5..cb5a697e3 100644 --- a/test/unit/models/classes/metadata/MetadataLomServiceTest.php +++ b/test/unit/models/classes/metadata/MetadataLomServiceTest.php @@ -59,5 +59,4 @@ public function testAddPropertiesToMetadataBlock(): void $manifest->saveXML() ); } - } From d0b57ba67d6f44e6cef479ab5c2a9e29961bd4f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Tue, 9 Apr 2024 11:28:27 +0200 Subject: [PATCH 03/30] feat: fix phpcs issues --- models/classes/export/AbstractQtiTestExporter.php | 2 +- models/classes/metadata/GenericLomOntologyExtractor.php | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/models/classes/export/AbstractQtiTestExporter.php b/models/classes/export/AbstractQtiTestExporter.php index 70309f582..fed6ddc5b 100644 --- a/models/classes/export/AbstractQtiTestExporter.php +++ b/models/classes/export/AbstractQtiTestExporter.php @@ -173,7 +173,7 @@ public function export(array $options = []): Report $this->exportTest($report->getData()); // 3. Export test metadata to manifest - $this->getMetadataExporter()->export($this->getItem(), $this->getManifest());; + $this->getMetadataExporter()->export($this->getItem(), $this->getManifest()); $this->genericLomOntologyExtractor()->extract( array_merge([$this->getItem()], $this->getItems()), $this->getManifest() diff --git a/models/classes/metadata/GenericLomOntologyExtractor.php b/models/classes/metadata/GenericLomOntologyExtractor.php index 3513136ab..d96f6ef03 100644 --- a/models/classes/metadata/GenericLomOntologyExtractor.php +++ b/models/classes/metadata/GenericLomOntologyExtractor.php @@ -35,8 +35,11 @@ class GenericLomOntologyExtractor implements GenericMetadataExtractor private PropertyMapper $propertyMapper; private MetadataLomService $metadataLomService; - public function __construct(Ontology $ontology, PropertyMapper $propertyMapper, MetadataLomService $metadataLomService) - { + public function __construct( + Ontology $ontology, + PropertyMapper $propertyMapper, + MetadataLomService $metadataLomService + ) { $this->ontology = $ontology; $this->propertyMapper = $propertyMapper; $this->metadataLomService = $metadataLomService; From f946de09771eefc4fc320c43125a3f98c3113f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Tue, 9 Apr 2024 17:00:29 +0200 Subject: [PATCH 04/30] feat: add Feature Flag --- .../classes/export/AbstractQtiTestExporter.php | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/models/classes/export/AbstractQtiTestExporter.php b/models/classes/export/AbstractQtiTestExporter.php index fed6ddc5b..31989f1e3 100644 --- a/models/classes/export/AbstractQtiTestExporter.php +++ b/models/classes/export/AbstractQtiTestExporter.php @@ -32,6 +32,7 @@ use DOMXPath; use oat\oatbox\reporting\Report; use oat\oatbox\reporting\ReportInterface; +use oat\tao\model\featureFlag\FeatureFlagChecker; use oat\taoQtiTest\models\classes\metadata\GenericLomOntologyExtractor; use qtism\data\storage\xml\marshalling\MarshallingException; use qtism\data\storage\xml\XmlDocument; @@ -49,6 +50,8 @@ abstract class AbstractQtiTestExporter extends ItemExporter implements QtiTestExporterInterface { + public const FEATURE_FLAG_LOM_ONTOLOGY_EXTRACTION = 'FEATURE_FLAG_LOM_ONTOLOGY_EXTRACTION'; + /** The QTISM XmlDocument representing the Test to be exported. */ private XmlDocument $testDocument; @@ -174,11 +177,13 @@ public function export(array $options = []): Report // 3. Export test metadata to manifest $this->getMetadataExporter()->export($this->getItem(), $this->getManifest()); - $this->genericLomOntologyExtractor()->extract( - array_merge([$this->getItem()], $this->getItems()), - $this->getManifest() - ); + if ($this->getFeatureFlagChecker()->isEnabled(self::FEATURE_FLAG_LOM_ONTOLOGY_EXTRACTION)) { + $this->genericLomOntologyExtractor()->extract( + array_merge([$this->getItem()], $this->getItems()), + $this->getManifest() + ); + } // 4. Persist manifest in archive. $this->getZip()->addFromString('imsmanifest.xml', $this->getManifest()->saveXML()); @@ -361,4 +366,9 @@ private function genericLomOntologyExtractor(): GenericLomOntologyExtractor { return $this->getServiceManager()->getContainer()->get(GenericLomOntologyExtractor::class); } + + private function getFeatureFlagChecker(): FeatureFlagChecker + { + return $this->getServiceManager()->getContainer()->get(FeatureFlagChecker::class); + } } From 9dac940c3bc27ccb73169cc7fc2dafd74750df71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Wed, 10 Apr 2024 11:44:55 +0200 Subject: [PATCH 05/30] feat: extract checksum to service, remove interface --- ...ataExtractor.php => ChecksumGenerator.php} | 36 ++++++-- .../metadata/GenericLomOntologyExtractor.php | 2 +- .../metadata/MetadataServiceProvider.php | 8 +- .../metadata/metaMetadata/PropertyMapper.php | 28 ++----- .../metadata/ChecksumGeneratorTest.php | 82 +++++++++++++++++++ .../GenericLomOntologyExtractorTest.php | 5 +- .../metaMetadata/PropertyMapperTest.php | 41 ++-------- 7 files changed, 132 insertions(+), 70 deletions(-) rename models/classes/metadata/{GenericMetadataExtractor.php => ChecksumGenerator.php} (54%) create mode 100644 test/unit/models/classes/metadata/ChecksumGeneratorTest.php diff --git a/models/classes/metadata/GenericMetadataExtractor.php b/models/classes/metadata/ChecksumGenerator.php similarity index 54% rename from models/classes/metadata/GenericMetadataExtractor.php rename to models/classes/metadata/ChecksumGenerator.php index 54ff555bd..8ea6b8976 100644 --- a/models/classes/metadata/GenericMetadataExtractor.php +++ b/models/classes/metadata/ChecksumGenerator.php @@ -22,14 +22,34 @@ namespace oat\taoQtiTest\models\classes\metadata; -use core_kernel_classes_Resource; -use DOMDocument; +use core_kernel_classes_Property as Property; +use oat\generis\model\data\Ontology; -interface GenericMetadataExtractor +class ChecksumGenerator { - /** - * @param core_kernel_classes_Resource[] $resourceCollection - * @param DOMDocument $manifest - */ - public function extract(array $resourceCollection, DOMDocument $manifest); + private Ontology $ontology; + + public function __construct(Ontology $ontology) + { + $this->ontology = $ontology; + } + + public function getRangeChecksum(Property $property): string + { + $checksum = ''; + + $listValues = array_filter($property->getRange()->getNestedResources(), function ($range) { + return $range['isclass'] === 0; + }); + + if (empty($listValues)) { + return ''; + } + + foreach ($listValues as $value) { + $checksum .= $this->ontology->getResource($value['id'])->getLabel(); + } + + return sha1($checksum); + } } diff --git a/models/classes/metadata/GenericLomOntologyExtractor.php b/models/classes/metadata/GenericLomOntologyExtractor.php index d96f6ef03..03b310293 100644 --- a/models/classes/metadata/GenericLomOntologyExtractor.php +++ b/models/classes/metadata/GenericLomOntologyExtractor.php @@ -29,7 +29,7 @@ use oat\taoQtiItem\model\qti\metadata\MetadataExtractionException; use oat\taoQtiTest\models\classes\metadata\metaMetadata\PropertyMapper; -class GenericLomOntologyExtractor implements GenericMetadataExtractor +class GenericLomOntologyExtractor { private Ontology $ontology; private PropertyMapper $propertyMapper; diff --git a/models/classes/metadata/MetadataServiceProvider.php b/models/classes/metadata/MetadataServiceProvider.php index a21069dbf..b89c1549e 100644 --- a/models/classes/metadata/MetadataServiceProvider.php +++ b/models/classes/metadata/MetadataServiceProvider.php @@ -35,11 +35,15 @@ class MetadataServiceProvider implements ContainerServiceProviderInterface public function __invoke(ContainerConfigurator $configurator): void { $services = $configurator->services(); + + $services->set(ChecksumGenerator::class, ChecksumGenerator::class) + ->args([service(Ontology::SERVICE_ID)]); + $services->set(MetadataLomService::class, MetadataLomService::class); $services->set(PropertyMapper::class, PropertyMapper::class) ->args([ - service(Ontology::SERVICE_ID), + service(ChecksumGenerator::class), [ 'label' => RDFS_LABEL, 'domain' => RDFS_DOMAIN, @@ -49,7 +53,7 @@ public function __invoke(ContainerConfigurator $configurator): void ]); $services - ->set(GenericLomOntologyExtractor::class, GenericMetadataExtractor::class) + ->set(GenericLomOntologyExtractor::class, GenericLomOntologyExtractor::class) ->public() ->args([ service(Ontology::SERVICE_ID), diff --git a/models/classes/metadata/metaMetadata/PropertyMapper.php b/models/classes/metadata/metaMetadata/PropertyMapper.php index d2a86316d..4bb2fa0d6 100644 --- a/models/classes/metadata/metaMetadata/PropertyMapper.php +++ b/models/classes/metadata/metaMetadata/PropertyMapper.php @@ -26,6 +26,7 @@ use core_kernel_classes_Resource as Resource; use oat\generis\model\data\Ontology; use oat\generis\model\OntologyRdf; +use oat\taoQtiTest\models\classes\metadata\ChecksumGenerator; use taoTests_models_classes_TestsService; class PropertyMapper @@ -33,12 +34,12 @@ class PropertyMapper public const DATATYPE_CHECKSUM = 'checksum'; private array $metaMetadataCollectionToExport; - private Ontology $ontology; + private ChecksumGenerator $checksumGenerator; - public function __construct(Ontology $ontology, array $metaMetadataCollectionToExport) + public function __construct(ChecksumGenerator $checksumGenerator, $metaMetadataCollectionToExport) { $this->metaMetadataCollectionToExport = $metaMetadataCollectionToExport; - $this->ontology = $ontology; + $this->checksumGenerator = $checksumGenerator; } public function getMetadataProperties(Property $property): array @@ -56,31 +57,12 @@ public function getMetadataProperties(Property $property): array } if (!$this->isIgnoredForCollectionGathering($property)) { - $fields[self::DATATYPE_CHECKSUM] = $this->getRangeChecksum($property); + $fields[self::DATATYPE_CHECKSUM] = $this->checksumGenerator->getRangeChecksum($property); } return $fields; } - private function getRangeChecksum(Property $property): string - { - $checksum = ''; - - $listValues = array_filter($property->getRange()->getNestedResources(), function ($range) { - return $range['isclass'] === 0; - }); - - if (empty($listValues)) { - return ''; - } - - foreach ($listValues as $value) { - $checksum .= $this->ontology->getResource($value['id'])->getLabel(); - } - - return sha1($checksum); - } - private function isIgnoredForCollectionGathering(Property $property): bool { return in_array($property->getUri(), $this->getIgnoredProperties()); diff --git a/test/unit/models/classes/metadata/ChecksumGeneratorTest.php b/test/unit/models/classes/metadata/ChecksumGeneratorTest.php new file mode 100644 index 000000000..d499340ad --- /dev/null +++ b/test/unit/models/classes/metadata/ChecksumGeneratorTest.php @@ -0,0 +1,82 @@ +propertyMock = $this->createMock(Property::class); + $this->ontologyMock = $this->createMock(Ontology::class); + $this->checksumGenerator = new ChecksumGenerator($this->ontologyMock); + } + + public function testGetRangeChecksum(): void + { + $classMock = $this->createMock(ClassResource::class); + $resourceMock = $this->createMock(Resource::class); + + $classMock->method('getNestedResources')->willReturn( + [ + [ + 'id' => 'id', + 'isclass' => 1, + ], + [ + 'id' => 'non_class_id', + 'isclass' => 0, + ], + [ + 'id' => 'non_class_id_2', + 'isclass' => 0, + ] + ] + ); + + $resourceMock->expects($this->exactly(2)) + ->method('getLabel') + ->willReturn('label'); + + $this->propertyMock->method('getRange')->willReturn($classMock); + $this->ontologyMock + ->expects($this->exactly(2)) + ->method('getResource') + ->with($this->logicalOr( + $this->equalTo('non_class_id'), + $this->equalTo('non_class_id_2') + )) + ->willReturn($resourceMock); + + $this->assertEquals( + 'c315a4bd4fa0f4479b1ea4b5998aa548eed3b670', + $this->checksumGenerator->getRangeChecksum($this->propertyMock) + ); + } +} diff --git a/test/unit/models/classes/metadata/GenericLomOntologyExtractorTest.php b/test/unit/models/classes/metadata/GenericLomOntologyExtractorTest.php index bc0c80bc0..27f02cd30 100644 --- a/test/unit/models/classes/metadata/GenericLomOntologyExtractorTest.php +++ b/test/unit/models/classes/metadata/GenericLomOntologyExtractorTest.php @@ -24,6 +24,7 @@ use DOMDocument; use oat\generis\model\data\Ontology; +use oat\taoQtiTest\models\classes\metadata\ChecksumGenerator; use oat\taoQtiTest\models\classes\metadata\GenericLomOntologyExtractor; use oat\taoQtiTest\models\classes\metadata\MetadataLomService; use oat\taoQtiTest\models\classes\metadata\metaMetadata\PropertyMapper; @@ -40,11 +41,13 @@ public function setUp(): void $this->ontologyMock = $this->createMock(Ontology::class); $this->propertyMapperMock = $this->createMock(PropertyMapper::class); $this->metadataLomServiceMock = $this->createMock(MetadataLomService::class); + $this->checksumGeneratorMock = $this->createMock(ChecksumGenerator::class); $this->subject = new GenericLomOntologyExtractor( $this->ontologyMock, $this->propertyMapperMock, - $this->metadataLomServiceMock + $this->metadataLomServiceMock, + $this->checksumGeneratorMock ); } diff --git a/test/unit/models/classes/metadata/metaMetadata/PropertyMapperTest.php b/test/unit/models/classes/metadata/metaMetadata/PropertyMapperTest.php index 7d7964978..b57cd5200 100644 --- a/test/unit/models/classes/metadata/metaMetadata/PropertyMapperTest.php +++ b/test/unit/models/classes/metadata/metaMetadata/PropertyMapperTest.php @@ -26,8 +26,8 @@ use core_kernel_classes_Literal; use core_kernel_classes_Property as Property; use core_kernel_classes_Resource as Resource; -use oat\generis\model\data\Ontology; use oat\generis\model\GenerisRdf; +use oat\taoQtiTest\models\classes\metadata\ChecksumGenerator; use oat\taoQtiTest\models\classes\metadata\metaMetadata\PropertyMapper; use PHPUnit\Framework\TestCase; @@ -35,6 +35,7 @@ class PropertyMapperTest extends TestCase { public function setUp(): void { + $this->checksumGeneratorMock = $this->createMock(ChecksumGenerator::class); $this->metaMetadataCollectionToExport = [ 'label' => RDFS_LABEL, 'domain' => RDFS_DOMAIN, @@ -42,13 +43,11 @@ public function setUp(): void 'multiple' => GenerisRdf::PROPERTY_MULTIPLE ]; - $this->ontology = $this->createMock(Ontology::class); - $this->subject = new PropertyMapper($this->ontology, $this->metaMetadataCollectionToExport); + $this->subject = new PropertyMapper($this->checksumGeneratorMock, $this->metaMetadataCollectionToExport); } public function testGetMetadataProperties(): void { - $classMock = $this->createMock(core_kernel_classes_Class::class); $property = $this->createMock(Property::class); $resourceMock = $this->createMock(Resource::class); $property->method('getUri')->willReturn('uri'); @@ -63,37 +62,9 @@ public function testGetMetadataProperties(): void null ); - - $property->method('getRange')->willReturn($classMock); - $classMock->method('getNestedResources')->willReturn( - [ - [ - 'id' => 'id', - 'isclass' => 1, - ], - [ - 'id' => 'non_class_id', - 'isclass' => 0, - ], - [ - 'id' => 'non_class_id_2', - 'isclass' => 0, - ] - ] - ); - - $this->ontology - ->expects($this->exactly(2)) - ->method('getResource') - ->with($this->logicalOr( - $this->equalTo('non_class_id'), - $this->equalTo('non_class_id_2') - )) - ->willReturn($resourceMock); - - $resourceMock->expects($this->exactly(2)) - ->method('getLabel') - ->willReturn('label'); + $this->checksumGeneratorMock + ->method('getRangeChecksum') + ->willReturn('c315a4bd4fa0f4479b1ea4b5998aa548eed3b670'); $result = $this->subject->getMetadataProperties($property); From 129d4bca4a122c2350c1c132c14b0a96b9ae12a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Wed, 10 Apr 2024 11:48:32 +0200 Subject: [PATCH 06/30] feat: phpcs fix --- models/classes/metadata/metaMetadata/PropertyMapper.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/models/classes/metadata/metaMetadata/PropertyMapper.php b/models/classes/metadata/metaMetadata/PropertyMapper.php index 4bb2fa0d6..32295d3ea 100644 --- a/models/classes/metadata/metaMetadata/PropertyMapper.php +++ b/models/classes/metadata/metaMetadata/PropertyMapper.php @@ -24,7 +24,6 @@ use core_kernel_classes_Property as Property; use core_kernel_classes_Resource as Resource; -use oat\generis\model\data\Ontology; use oat\generis\model\OntologyRdf; use oat\taoQtiTest\models\classes\metadata\ChecksumGenerator; use taoTests_models_classes_TestsService; @@ -36,7 +35,7 @@ class PropertyMapper private array $metaMetadataCollectionToExport; private ChecksumGenerator $checksumGenerator; - public function __construct(ChecksumGenerator $checksumGenerator, $metaMetadataCollectionToExport) + public function __construct(ChecksumGenerator $checksumGenerator, array $metaMetadataCollectionToExport) { $this->metaMetadataCollectionToExport = $metaMetadataCollectionToExport; $this->checksumGenerator = $checksumGenerator; From 5b0794d99a9013c3fbcc7d7cdc2312a92deb0927 Mon Sep 17 00:00:00 2001 From: Karol Stelmaczonek Date: Wed, 10 Apr 2024 15:53:37 +0200 Subject: [PATCH 07/30] feat: add import metametadata --- models/classes/class.QtiTestService.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/models/classes/class.QtiTestService.php b/models/classes/class.QtiTestService.php index a3d6339a4..1683e684e 100644 --- a/models/classes/class.QtiTestService.php +++ b/models/classes/class.QtiTestService.php @@ -31,6 +31,8 @@ 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\metaMetadata\Importer as MetaMetadataImporter; +use oat\taoQtiItem\model\qti\metaMetadata\MetaMetadataService; use oat\taoQtiItem\model\qti\Resource; use oat\taoQtiItem\model\qti\Service; use oat\taoQtiTest\models\cat\AdaptiveSectionInjectionException; @@ -570,6 +572,7 @@ protected function importTest( $domManifest->load($folder . 'imsmanifest.xml'); $metadataValues = $this->getMetadataImporter()->extract($domManifest); + $metaMetadataValues = $this->getMetaMetadataImporter()->extract($domManifest); // Note: without this fix, metadata guardians do not work. $this->getMetadataImporter()->setMetadataValues($metadataValues); @@ -697,7 +700,8 @@ protected function importTest( $this->useMetadataValidators, $this->itemMustExist, $this->itemMustBeOverwritten, - $reportCtx->overwrittenItems + $reportCtx->overwrittenItems, + $metaMetadataValues ); $reportCtx->createdClasses = array_merge( @@ -1411,6 +1415,11 @@ protected function getMetadataImporter() return $this->metadataImporter; } + protected function getMetaMetadataImporter(): MetaMetadataImporter + { + return $this->getServiceLocator()->get(MetaMetadataService::SERVICE_ID)->getImporter(); + } + private function getSecureResourceService(): SecureResourceServiceInterface { return $this->getServiceLocator()->get(SecureResourceServiceInterface::SERVICE_ID); From 8d0bb00ce6dea6e71c8b91dc10f02296c1454b90 Mon Sep 17 00:00:00 2001 From: Karol Stelmaczonek Date: Thu, 11 Apr 2024 14:20:23 +0200 Subject: [PATCH 08/30] chore: add debug for checksum --- models/classes/metadata/ChecksumGenerator.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/models/classes/metadata/ChecksumGenerator.php b/models/classes/metadata/ChecksumGenerator.php index 8ea6b8976..ad1573eee 100644 --- a/models/classes/metadata/ChecksumGenerator.php +++ b/models/classes/metadata/ChecksumGenerator.php @@ -22,6 +22,7 @@ namespace oat\taoQtiTest\models\classes\metadata; +use common_Logger; use core_kernel_classes_Property as Property; use oat\generis\model\data\Ontology; @@ -49,7 +50,7 @@ public function getRangeChecksum(Property $property): string foreach ($listValues as $value) { $checksum .= $this->ontology->getResource($value['id'])->getLabel(); } - + common_Logger::d(sprintf('ChecksumGenerator value before sha1 : "%s"', $checksum)); return sha1($checksum); } } From aacccee24ef59daa8e9488ffad8b4485997f5b98 Mon Sep 17 00:00:00 2001 From: Karol Stelmaczonek Date: Thu, 11 Apr 2024 16:09:25 +0200 Subject: [PATCH 09/30] feat: add sorting and to lower for checksum --- models/classes/metadata/ChecksumGenerator.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/models/classes/metadata/ChecksumGenerator.php b/models/classes/metadata/ChecksumGenerator.php index 8ea6b8976..2c45e0c45 100644 --- a/models/classes/metadata/ChecksumGenerator.php +++ b/models/classes/metadata/ChecksumGenerator.php @@ -22,6 +22,7 @@ namespace oat\taoQtiTest\models\classes\metadata; +use common_Logger; use core_kernel_classes_Property as Property; use oat\generis\model\data\Ontology; @@ -45,11 +46,11 @@ public function getRangeChecksum(Property $property): string if (empty($listValues)) { return ''; } - + asort($listValues); foreach ($listValues as $value) { - $checksum .= $this->ontology->getResource($value['id'])->getLabel(); + $checksum .= strtolower($this->ontology->getResource($value['id'])->getLabel()); } - - return sha1($checksum); + common_Logger::e(sprintf('ChecksumGenerator value before sha1 : "%s"', $checksum)); + return sha1(trim($checksum)); } } From 926c5539a6a63904bbcdfef6ec6330fd2f8a1a06 Mon Sep 17 00:00:00 2001 From: Karol Stelmaczonek Date: Thu, 11 Apr 2024 16:30:50 +0200 Subject: [PATCH 10/30] feat: additional fixes for checksum --- models/classes/metadata/ChecksumGenerator.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/models/classes/metadata/ChecksumGenerator.php b/models/classes/metadata/ChecksumGenerator.php index 2c45e0c45..6a688be00 100644 --- a/models/classes/metadata/ChecksumGenerator.php +++ b/models/classes/metadata/ChecksumGenerator.php @@ -39,18 +39,20 @@ public function getRangeChecksum(Property $property): string { $checksum = ''; - $listValues = array_filter($property->getRange()->getNestedResources(), function ($range) { + $resourceList = array_filter($property->getRange()->getNestedResources(), function ($range) { return $range['isclass'] === 0; }); - if (empty($listValues)) { + if (empty($resourceList)) { return ''; } - asort($listValues); - foreach ($listValues as $value) { - $checksum .= strtolower($this->ontology->getResource($value['id'])->getLabel()); + $labels = []; + foreach ($resourceList as $resource) { + $labels[] = strtolower($this->ontology->getResource($resource['id'])->getLabel()); } - common_Logger::e(sprintf('ChecksumGenerator value before sha1 : "%s"', $checksum)); + asort($labels); + $checksum = implode('', $labels); + common_Logger::e(sprintf('ChecksumGenerator resource before sha1 : "%s"', $checksum)); return sha1(trim($checksum)); } } From 4d88aed489d4493738c00503955d81f644c7cf43 Mon Sep 17 00:00:00 2001 From: Karol Stelmaczonek Date: Fri, 12 Apr 2024 17:33:31 +0200 Subject: [PATCH 11/30] chore: refactor & rename --- models/classes/class.QtiTestService.php | 8 ++++---- models/classes/metadata/ChecksumGenerator.php | 2 -- models/classes/metadata/MetadataServiceProvider.php | 3 ++- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/models/classes/class.QtiTestService.php b/models/classes/class.QtiTestService.php index 1683e684e..fd21f8914 100644 --- a/models/classes/class.QtiTestService.php +++ b/models/classes/class.QtiTestService.php @@ -29,9 +29,9 @@ use oat\taoItems\model\Command\DeleteItemCommand; 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\MetadataGuardianResource; use oat\taoQtiItem\model\qti\metadata\MetadataService; -use oat\taoQtiItem\model\qti\metaMetadata\Importer as MetaMetadataImporter; use oat\taoQtiItem\model\qti\metaMetadata\MetaMetadataService; use oat\taoQtiItem\model\qti\Resource; use oat\taoQtiItem\model\qti\Service; @@ -572,7 +572,7 @@ protected function importTest( $domManifest->load($folder . 'imsmanifest.xml'); $metadataValues = $this->getMetadataImporter()->extract($domManifest); - $metaMetadataValues = $this->getMetaMetadataImporter()->extract($domManifest); + $metaMetadataValues = $this->getMetaMetadataExtractor()->extract($domManifest); // Note: without this fix, metadata guardians do not work. $this->getMetadataImporter()->setMetadataValues($metadataValues); @@ -1415,9 +1415,9 @@ protected function getMetadataImporter() return $this->metadataImporter; } - protected function getMetaMetadataImporter(): MetaMetadataImporter + protected function getMetaMetadataExtractor(): MetaMetadataExtractor { - return $this->getServiceLocator()->get(MetaMetadataService::SERVICE_ID)->getImporter(); + return $this->getServiceLocator()->getContainer()->get(MetaMetadataExtractor::class); } private function getSecureResourceService(): SecureResourceServiceInterface diff --git a/models/classes/metadata/ChecksumGenerator.php b/models/classes/metadata/ChecksumGenerator.php index 6a688be00..949009c0a 100644 --- a/models/classes/metadata/ChecksumGenerator.php +++ b/models/classes/metadata/ChecksumGenerator.php @@ -37,8 +37,6 @@ public function __construct(Ontology $ontology) public function getRangeChecksum(Property $property): string { - $checksum = ''; - $resourceList = array_filter($property->getRange()->getNestedResources(), function ($range) { return $range['isclass'] === 0; }); diff --git a/models/classes/metadata/MetadataServiceProvider.php b/models/classes/metadata/MetadataServiceProvider.php index b89c1549e..d4d22df3f 100644 --- a/models/classes/metadata/MetadataServiceProvider.php +++ b/models/classes/metadata/MetadataServiceProvider.php @@ -37,7 +37,8 @@ public function __invoke(ContainerConfigurator $configurator): void $services = $configurator->services(); $services->set(ChecksumGenerator::class, ChecksumGenerator::class) - ->args([service(Ontology::SERVICE_ID)]); + ->args([service(Ontology::SERVICE_ID)]) + ->public(); $services->set(MetadataLomService::class, MetadataLomService::class); From 95b23367fb9bfa4360e2ad8dd33844557db88cd3 Mon Sep 17 00:00:00 2001 From: Karol Stelmaczonek Date: Mon, 15 Apr 2024 15:26:35 +0200 Subject: [PATCH 12/30] chore: phpcbf fixes --- models/classes/class.QtiTestService.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/models/classes/class.QtiTestService.php b/models/classes/class.QtiTestService.php index fd21f8914..a654af22d 100644 --- a/models/classes/class.QtiTestService.php +++ b/models/classes/class.QtiTestService.php @@ -23,16 +23,18 @@ use oat\oatbox\filesystem\Directory; use oat\oatbox\filesystem\File; use oat\oatbox\filesystem\FileSystemService; +use oat\oatbox\reporting\Report as Reporter; 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\ImportService; use oat\taoQtiItem\model\qti\metadata\importer\MetadataImporter; +use oat\taoQtiItem\model\qti\metadata\imsManifest\MetaMetadataException; use oat\taoQtiItem\model\qti\metadata\imsManifest\MetaMetadataExtractor; +use oat\taoQtiItem\model\qti\metadata\imsManifest\MetaMetadataValidator; use oat\taoQtiItem\model\qti\metadata\MetadataGuardianResource; use oat\taoQtiItem\model\qti\metadata\MetadataService; -use oat\taoQtiItem\model\qti\metaMetadata\MetaMetadataService; use oat\taoQtiItem\model\qti\Resource; use oat\taoQtiItem\model\qti\Service; use oat\taoQtiTest\models\cat\AdaptiveSectionInjectionException; @@ -609,6 +611,7 @@ protected function importTest( $testDefinition = new XmlDocument(); try { + $this->getMetaMetadataValidator()->validateClass($testClass, $metaMetadataValues); $testDefinition->load($expectedTestFile, true); // If any, assessmentSectionRefs will be resolved and included as part of the main test definition. @@ -853,6 +856,11 @@ protected function importTest( // phpcs:enable Generic.Files.LineLength ) ); + } catch(MetaMetadataException $e) { + $report = Reporter::createError( + sprintf('Import failed at validating metametadata with message: "%s"', $e->getMessage()) + ); + common_Logger::e($e->getMessage()); } } @@ -1497,4 +1505,9 @@ private function getPsrContainer(): ContainerInterface { return $this->getServiceLocator()->getContainer(); } + + protected function getMetaMetadataValidator(): MetaMetadataValidator + { + return $this->getServiceManager()->getContainer()->get(MetaMetadataValidator::class); + } } From 0bc78b6bc903bacc79d8be790ebc8e9465b6babf Mon Sep 17 00:00:00 2001 From: Karol Stelmaczonek Date: Mon, 15 Apr 2024 15:36:23 +0200 Subject: [PATCH 13/30] chore: phpcbf fixes --- models/classes/class.QtiTestService.php | 2 +- models/classes/metadata/ChecksumGenerator.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/models/classes/class.QtiTestService.php b/models/classes/class.QtiTestService.php index a654af22d..666b36ee0 100644 --- a/models/classes/class.QtiTestService.php +++ b/models/classes/class.QtiTestService.php @@ -856,7 +856,7 @@ protected function importTest( // phpcs:enable Generic.Files.LineLength ) ); - } catch(MetaMetadataException $e) { + } catch (MetaMetadataException $e) { $report = Reporter::createError( sprintf('Import failed at validating metametadata with message: "%s"', $e->getMessage()) ); diff --git a/models/classes/metadata/ChecksumGenerator.php b/models/classes/metadata/ChecksumGenerator.php index 949009c0a..f634fdedd 100644 --- a/models/classes/metadata/ChecksumGenerator.php +++ b/models/classes/metadata/ChecksumGenerator.php @@ -50,7 +50,7 @@ public function getRangeChecksum(Property $property): string } asort($labels); $checksum = implode('', $labels); - common_Logger::e(sprintf('ChecksumGenerator resource before sha1 : "%s"', $checksum)); + return sha1(trim($checksum)); } } From 3e30b13de564d21a892c528927acb20870b7edfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Tue, 16 Apr 2024 15:29:46 +0200 Subject: [PATCH 14/30] feat: add metadata import for items and tests --- models/classes/class.QtiTestService.php | 32 +++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/models/classes/class.QtiTestService.php b/models/classes/class.QtiTestService.php index fd21f8914..19db01da4 100644 --- a/models/classes/class.QtiTestService.php +++ b/models/classes/class.QtiTestService.php @@ -29,9 +29,12 @@ use oat\taoItems\model\Command\DeleteItemCommand; use oat\taoQtiItem\model\qti\ImportService; use oat\taoQtiItem\model\qti\metadata\importer\MetadataImporter; +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\metaMetadata\MetaMetadataService; use oat\taoQtiItem\model\qti\Resource; use oat\taoQtiItem\model\qti\Service; @@ -586,6 +589,7 @@ protected function importTest( $reportCtx->overwrittenItems = []; $reportCtx->itemQtiResources = []; $reportCtx->testMetadata = $metadataValues[$qtiTestResourceIdentifier] ?? []; + $reportCtx->metaMetadata = $metaMetadataValues; $reportCtx->createdClasses = []; // 'uriResource' key is needed by javascript in tao/views/templates/form/import.tpl @@ -628,6 +632,7 @@ protected function importTest( } $targetItemClass = $itemParentClass->createSubClass(self::IN_PROGRESS_LABEL); + $mappedProperties = $this->getMetaMetadataImporter()->mapMetaMetadataToProperties($metaMetadataValues, $targetItemClass, $testClass); // add real label without saving (to not pass it separately to item importer) $targetItemClass->label = $testLabel; @@ -682,7 +687,6 @@ protected function importTest( ); } } - // Skip if $qtiFile already imported (multiple assessmentItemRef "hrefing" the same // file). if (array_key_exists($qtiFile, $alreadyImportedTestItemFiles) === false) { @@ -701,7 +705,7 @@ protected function importTest( $this->itemMustExist, $this->itemMustBeOverwritten, $reportCtx->overwrittenItems, - $metaMetadataValues + $mappedProperties['itemProperties'] ); $reportCtx->createdClasses = array_merge( @@ -784,6 +788,11 @@ protected function importTest( // 4. Import metadata for the resource (use same mechanics as item resources). // Metadata will be set as property values. $this->getMetadataImporter()->inject($qtiTestResource->getIdentifier(), $testResource); + $this->getServiceManager()->getContainer()->get(MappedMetadataInjector::class)->inject( + $mappedProperties['testProperties'], + $metadataValues[$qtiTestResourceIdentifier], + $testResource + ); // 5. if $targetClass does not contain any instances // (because everything resolved by class lookups), @@ -837,14 +846,24 @@ protected function importTest( $msg = __("Error found in the IMS QTI Test:\n%s", $finalErrorString); $report->add(common_report_Report::createFailure($msg)); - } catch (CatEngineNotFoundException $e) { + } + catch (PropertyDoesNotExistException $e) { + $report->add( + new common_report_Report( + common_report_Report::TYPE_ERROR, + __("Property '%s' does not exist.", $e->getProperty()) + ) + ); + } + catch (CatEngineNotFoundException $e) { $report->add( new common_report_Report( common_report_Report::TYPE_ERROR, __('No CAT Engine configured for CAT Endpoint "%s".', $e->getRequestedEndpoint()) ) ); - } catch (AdaptiveSectionInjectionException $e) { + } + catch (AdaptiveSectionInjectionException $e) { $report->add( new common_report_Report( common_report_Report::TYPE_ERROR, @@ -1497,4 +1516,9 @@ private function getPsrContainer(): ContainerInterface { return $this->getServiceLocator()->getContainer(); } + + private function getMetaMetadataImporter(): MetaMetadataImportMapper + { + return $this->getServiceManager()->getContainer()->get(MetaMetadataImportMapper::class); + } } From bea3f558552f66242c038bfc50d63e260add9f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Thu, 18 Apr 2024 09:53:56 +0200 Subject: [PATCH 15/30] feat: Remove MetaMetadataValidator. Move business logic into matcher --- models/classes/class.QtiTestService.php | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/models/classes/class.QtiTestService.php b/models/classes/class.QtiTestService.php index 2c78dee38..69835b492 100644 --- a/models/classes/class.QtiTestService.php +++ b/models/classes/class.QtiTestService.php @@ -32,11 +32,9 @@ 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\MetaMetadataValidator; 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\metaMetadata\imsManifest\MetaMetadataException; use oat\taoQtiItem\model\qti\Resource; use oat\taoQtiItem\model\qti\Service; use oat\taoQtiTest\models\cat\AdaptiveSectionInjectionException; @@ -46,7 +44,6 @@ use oat\taoQtiTest\models\render\QtiPackageImportPreprocessing; use oat\taoQtiTest\models\test\AssessmentTestXmlFactory; use oat\taoTests\models\event\TestUpdatedEvent; -use PHP_CodeSniffer\Reporter; use Psr\Container\ContainerInterface; use qtism\common\utils\Format; use qtism\data\AssessmentItemRef; @@ -57,6 +54,7 @@ use qtism\data\storage\xml\XmlDocument; use qtism\data\storage\xml\XmlStorageException; use taoTests_models_classes_TestsService as TestService; +use oat\oatbox\reporting\Report; /** * the QTI TestModel service. @@ -615,7 +613,6 @@ protected function importTest( $testDefinition = new XmlDocument(); try { - $this->getMetaMetadataValidator()->validateClass($testClass, $metaMetadataValues); $testDefinition->load($expectedTestFile, true); // If any, assessmentSectionRefs will be resolved and included as part of the main test definition. @@ -851,12 +848,8 @@ protected function importTest( $report->add(common_report_Report::createFailure($msg)); } catch (PropertyDoesNotExistException $e) { - $report->add( - new common_report_Report( - common_report_Report::TYPE_ERROR, - __("Property '%s' does not exist.", $e->getProperty()) - ) - ); + $reportCtx->itemClass = $targetItemClass; + $report->add(Report::createError($e->getMessage())); } catch (CatEngineNotFoundException $e) { $report->add( @@ -875,11 +868,6 @@ protected function importTest( // phpcs:enable Generic.Files.LineLength ) ); - } catch (MetaMetadataException $e) { - $report = Reporter::createError( - sprintf('Import failed at validating metametadata with message: "%s"', $e->getMessage()) - ); - common_Logger::e($e->getMessage()); } } @@ -1529,9 +1517,4 @@ private function getMetaMetadataImporter(): MetaMetadataImportMapper { return $this->getServiceManager()->getContainer()->get(MetaMetadataImportMapper::class); } - - protected function getMetaMetadataValidator(): MetaMetadataValidator - { - return $this->getServiceManager()->getContainer()->get(MetaMetadataValidator::class); - } } From aa3aa6bfa876f8d43f87fafc8237e980bffa411e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Thu, 18 Apr 2024 16:00:07 +0200 Subject: [PATCH 16/30] feat: phpsbf fix --- models/classes/class.QtiTestService.php | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/models/classes/class.QtiTestService.php b/models/classes/class.QtiTestService.php index 69835b492..f30e7f0d8 100644 --- a/models/classes/class.QtiTestService.php +++ b/models/classes/class.QtiTestService.php @@ -632,7 +632,8 @@ protected function importTest( } $targetItemClass = $itemParentClass->createSubClass(self::IN_PROGRESS_LABEL); - $mappedProperties = $this->getMetaMetadataImporter()->mapMetaMetadataToProperties($metaMetadataValues, $targetItemClass, $testClass); + $mappedProperties = $this->getMetaMetadataImporter() + ->mapMetaMetadataToProperties($metaMetadataValues, $targetItemClass, $testClass); // add real label without saving (to not pass it separately to item importer) $targetItemClass->label = $testLabel; @@ -846,20 +847,17 @@ protected function importTest( $msg = __("Error found in the IMS QTI Test:\n%s", $finalErrorString); $report->add(common_report_Report::createFailure($msg)); - } - catch (PropertyDoesNotExistException $e) { + } catch (PropertyDoesNotExistException $e) { $reportCtx->itemClass = $targetItemClass; $report->add(Report::createError($e->getMessage())); - } - catch (CatEngineNotFoundException $e) { + } catch (CatEngineNotFoundException $e) { $report->add( new common_report_Report( common_report_Report::TYPE_ERROR, __('No CAT Engine configured for CAT Endpoint "%s".', $e->getRequestedEndpoint()) ) ); - } - catch (AdaptiveSectionInjectionException $e) { + } catch (AdaptiveSectionInjectionException $e) { $report->add( new common_report_Report( common_report_Report::TYPE_ERROR, @@ -1430,9 +1428,10 @@ protected function getMetadataImporter() return $this->metadataImporter; } - protected function getMetaMetadataExtractor(): MetaMetadataExtractor + private function getMetaMetadataExtractor(): MetaMetadataExtractor { - return $this->getServiceLocator()->getContainer()->get(MetaMetadataExtractor::class); + return $this->getPsrContainer()->get(MetaMetadataExtractor::class); + return $this->getServiceManager()->getContainer()->get(MetaMetadataExtractor::class); } private function getSecureResourceService(): SecureResourceServiceInterface From f1f66168d2ce2a46b2386fc25b78459f2bbf4a1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Thu, 18 Apr 2024 16:16:30 +0200 Subject: [PATCH 17/30] feat: move ChecksumGenerator to taoQtiItems --- models/classes/metadata/ChecksumGenerator.php | 56 ------------- .../metadata/MetadataServiceProvider.php | 6 +- .../metadata/metaMetadata/PropertyMapper.php | 2 +- .../metadata/ChecksumGeneratorTest.php | 82 ------------------- .../metaMetadata/PropertyMapperTest.php | 3 +- 5 files changed, 3 insertions(+), 146 deletions(-) delete mode 100644 models/classes/metadata/ChecksumGenerator.php delete mode 100644 test/unit/models/classes/metadata/ChecksumGeneratorTest.php diff --git a/models/classes/metadata/ChecksumGenerator.php b/models/classes/metadata/ChecksumGenerator.php deleted file mode 100644 index f634fdedd..000000000 --- a/models/classes/metadata/ChecksumGenerator.php +++ /dev/null @@ -1,56 +0,0 @@ -ontology = $ontology; - } - - public function getRangeChecksum(Property $property): string - { - $resourceList = array_filter($property->getRange()->getNestedResources(), function ($range) { - return $range['isclass'] === 0; - }); - - if (empty($resourceList)) { - return ''; - } - $labels = []; - foreach ($resourceList as $resource) { - $labels[] = strtolower($this->ontology->getResource($resource['id'])->getLabel()); - } - asort($labels); - $checksum = implode('', $labels); - - return sha1(trim($checksum)); - } -} diff --git a/models/classes/metadata/MetadataServiceProvider.php b/models/classes/metadata/MetadataServiceProvider.php index d4d22df3f..ca3140ba6 100644 --- a/models/classes/metadata/MetadataServiceProvider.php +++ b/models/classes/metadata/MetadataServiceProvider.php @@ -25,9 +25,9 @@ use oat\generis\model\data\Ontology; use oat\generis\model\DependencyInjection\ContainerServiceProviderInterface; use oat\generis\model\GenerisRdf; +use oat\taoQtiItem\model\import\ChecksumGenerator; use oat\taoQtiTest\models\classes\metadata\metaMetadata\PropertyMapper; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; - use function Symfony\Component\DependencyInjection\Loader\Configurator\service; class MetadataServiceProvider implements ContainerServiceProviderInterface @@ -36,10 +36,6 @@ public function __invoke(ContainerConfigurator $configurator): void { $services = $configurator->services(); - $services->set(ChecksumGenerator::class, ChecksumGenerator::class) - ->args([service(Ontology::SERVICE_ID)]) - ->public(); - $services->set(MetadataLomService::class, MetadataLomService::class); $services->set(PropertyMapper::class, PropertyMapper::class) diff --git a/models/classes/metadata/metaMetadata/PropertyMapper.php b/models/classes/metadata/metaMetadata/PropertyMapper.php index 32295d3ea..af4fb9a19 100644 --- a/models/classes/metadata/metaMetadata/PropertyMapper.php +++ b/models/classes/metadata/metaMetadata/PropertyMapper.php @@ -25,7 +25,7 @@ use core_kernel_classes_Property as Property; use core_kernel_classes_Resource as Resource; use oat\generis\model\OntologyRdf; -use oat\taoQtiTest\models\classes\metadata\ChecksumGenerator; +use oat\taoQtiItem\model\import\ChecksumGenerator; use taoTests_models_classes_TestsService; class PropertyMapper diff --git a/test/unit/models/classes/metadata/ChecksumGeneratorTest.php b/test/unit/models/classes/metadata/ChecksumGeneratorTest.php deleted file mode 100644 index d499340ad..000000000 --- a/test/unit/models/classes/metadata/ChecksumGeneratorTest.php +++ /dev/null @@ -1,82 +0,0 @@ -propertyMock = $this->createMock(Property::class); - $this->ontologyMock = $this->createMock(Ontology::class); - $this->checksumGenerator = new ChecksumGenerator($this->ontologyMock); - } - - public function testGetRangeChecksum(): void - { - $classMock = $this->createMock(ClassResource::class); - $resourceMock = $this->createMock(Resource::class); - - $classMock->method('getNestedResources')->willReturn( - [ - [ - 'id' => 'id', - 'isclass' => 1, - ], - [ - 'id' => 'non_class_id', - 'isclass' => 0, - ], - [ - 'id' => 'non_class_id_2', - 'isclass' => 0, - ] - ] - ); - - $resourceMock->expects($this->exactly(2)) - ->method('getLabel') - ->willReturn('label'); - - $this->propertyMock->method('getRange')->willReturn($classMock); - $this->ontologyMock - ->expects($this->exactly(2)) - ->method('getResource') - ->with($this->logicalOr( - $this->equalTo('non_class_id'), - $this->equalTo('non_class_id_2') - )) - ->willReturn($resourceMock); - - $this->assertEquals( - 'c315a4bd4fa0f4479b1ea4b5998aa548eed3b670', - $this->checksumGenerator->getRangeChecksum($this->propertyMock) - ); - } -} diff --git a/test/unit/models/classes/metadata/metaMetadata/PropertyMapperTest.php b/test/unit/models/classes/metadata/metaMetadata/PropertyMapperTest.php index b57cd5200..056e50843 100644 --- a/test/unit/models/classes/metadata/metaMetadata/PropertyMapperTest.php +++ b/test/unit/models/classes/metadata/metaMetadata/PropertyMapperTest.php @@ -22,12 +22,11 @@ namespace oat\taoQtiTest\test\unit\models\classes\metadata\metaMetadata; -use core_kernel_classes_Class; use core_kernel_classes_Literal; use core_kernel_classes_Property as Property; use core_kernel_classes_Resource as Resource; use oat\generis\model\GenerisRdf; -use oat\taoQtiTest\models\classes\metadata\ChecksumGenerator; +use oat\taoQtiItem\model\import\ChecksumGenerator; use oat\taoQtiTest\models\classes\metadata\metaMetadata\PropertyMapper; use PHPUnit\Framework\TestCase; From 52cb224df5b8ea789f872b64c737d0058dd8f2d5 Mon Sep 17 00:00:00 2001 From: Karol Stelmaczonek Date: Thu, 18 Apr 2024 16:24:39 +0200 Subject: [PATCH 18/30] chore: remove unused code --- .../models/classes/metadata/GenericLomOntologyExtractorTest.php | 1 - .../models/classes/metadata/metaMetadata/PropertyMapperTest.php | 1 - 2 files changed, 2 deletions(-) diff --git a/test/unit/models/classes/metadata/GenericLomOntologyExtractorTest.php b/test/unit/models/classes/metadata/GenericLomOntologyExtractorTest.php index 27f02cd30..b27552257 100644 --- a/test/unit/models/classes/metadata/GenericLomOntologyExtractorTest.php +++ b/test/unit/models/classes/metadata/GenericLomOntologyExtractorTest.php @@ -47,7 +47,6 @@ public function setUp(): void $this->ontologyMock, $this->propertyMapperMock, $this->metadataLomServiceMock, - $this->checksumGeneratorMock ); } diff --git a/test/unit/models/classes/metadata/metaMetadata/PropertyMapperTest.php b/test/unit/models/classes/metadata/metaMetadata/PropertyMapperTest.php index b57cd5200..f79b44b1d 100644 --- a/test/unit/models/classes/metadata/metaMetadata/PropertyMapperTest.php +++ b/test/unit/models/classes/metadata/metaMetadata/PropertyMapperTest.php @@ -22,7 +22,6 @@ namespace oat\taoQtiTest\test\unit\models\classes\metadata\metaMetadata; -use core_kernel_classes_Class; use core_kernel_classes_Literal; use core_kernel_classes_Property as Property; use core_kernel_classes_Resource as Resource; From d0e78fc14bb031ce79eca4c73033045d6292a7f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Thu, 18 Apr 2024 16:24:57 +0200 Subject: [PATCH 19/30] feat: GenericLomOntologyExtractorTest --- .../models/classes/metadata/GenericLomOntologyExtractorTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/unit/models/classes/metadata/GenericLomOntologyExtractorTest.php b/test/unit/models/classes/metadata/GenericLomOntologyExtractorTest.php index 27f02cd30..10971293a 100644 --- a/test/unit/models/classes/metadata/GenericLomOntologyExtractorTest.php +++ b/test/unit/models/classes/metadata/GenericLomOntologyExtractorTest.php @@ -41,13 +41,11 @@ public function setUp(): void $this->ontologyMock = $this->createMock(Ontology::class); $this->propertyMapperMock = $this->createMock(PropertyMapper::class); $this->metadataLomServiceMock = $this->createMock(MetadataLomService::class); - $this->checksumGeneratorMock = $this->createMock(ChecksumGenerator::class); $this->subject = new GenericLomOntologyExtractor( $this->ontologyMock, $this->propertyMapperMock, $this->metadataLomServiceMock, - $this->checksumGeneratorMock ); } From 4cf0215c6a646616f174a97f14ef5e0706bfb3ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Thu, 18 Apr 2024 16:43:45 +0200 Subject: [PATCH 20/30] feat: temp composer fix --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 6380b9d7e..3424a60be 100644 --- a/composer.json +++ b/composer.json @@ -66,7 +66,7 @@ "oat-sa/generis" : ">=15.22", "oat-sa/tao-core": ">=54.0.0", "oat-sa/extension-tao-item" : ">=12.1.0", - "oat-sa/extension-tao-itemqti" : ">=30.0.0", + "oat-sa/extension-tao-itemqti" : "dev-feat/aut-3590-add-metadata-check-on-import", "oat-sa/extension-tao-test" : ">=16.0.0", "oat-sa/extension-tao-delivery" : ">=15.0.0", "oat-sa/extension-tao-outcome" : ">=13.0.0", From 959026a19b862ec8375756195b4c4ff6fc3dce00 Mon Sep 17 00:00:00 2001 From: Karol Stelmaczonek Date: Thu, 18 Apr 2024 16:58:31 +0200 Subject: [PATCH 21/30] chore: phpcbf fixes --- models/classes/metadata/MetadataServiceProvider.php | 1 + 1 file changed, 1 insertion(+) diff --git a/models/classes/metadata/MetadataServiceProvider.php b/models/classes/metadata/MetadataServiceProvider.php index ca3140ba6..c252528ef 100644 --- a/models/classes/metadata/MetadataServiceProvider.php +++ b/models/classes/metadata/MetadataServiceProvider.php @@ -28,6 +28,7 @@ use oat\taoQtiItem\model\import\ChecksumGenerator; use oat\taoQtiTest\models\classes\metadata\metaMetadata\PropertyMapper; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + use function Symfony\Component\DependencyInjection\Loader\Configurator\service; class MetadataServiceProvider implements ContainerServiceProviderInterface From 9a6a2058163af2579a1e106208dd11cb2ccd206d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Thu, 2 May 2024 14:28:49 +0200 Subject: [PATCH 22/30] feat: add form checkbox in import test form --- models/classes/class.QtiTestService.php | 42 +++++++++++++++--- models/classes/import/class.TestImport.php | 24 ++++++++++- .../classes/import/class.TestImportForm.php | 43 +++++++++++++------ .../classes/metadata/MetadataLomService.php | 2 + 4 files changed, 90 insertions(+), 21 deletions(-) diff --git a/models/classes/class.QtiTestService.php b/models/classes/class.QtiTestService.php index f30e7f0d8..0745ae7e7 100644 --- a/models/classes/class.QtiTestService.php +++ b/models/classes/class.QtiTestService.php @@ -27,6 +27,7 @@ use oat\tao\model\resources\SecureResourceServiceInterface; use oat\tao\model\TaoOntology; use oat\taoItems\model\Command\DeleteItemCommand; +use oat\taoQtiItem\model\import\QtiPackageImport; use oat\taoQtiItem\model\qti\ImportService; use oat\taoQtiItem\model\qti\metadata\importer\MetadataImporter; use oat\taoQtiItem\model\qti\metadata\imsManifest\MetaMetadataExtractor; @@ -354,7 +355,8 @@ public function importMultipleTests( core_kernel_classes_Class $targetClass, $file, bool $overwriteTest = false, - ?string $itemClassUri = null + ?string $itemClassUri = null, + array $form = [] ) { $testClass = $targetClass; $report = new common_report_Report(common_report_Report::TYPE_INFO); @@ -422,7 +424,8 @@ public function importMultipleTests( $folder, $alreadyImportedQtiResources, $overwriteTest, - $itemClassUri + $itemClassUri, + $form[QtiPackageImport::METADATA_IMPORT_ELEMENT_NAME] ?? false ); $report->add($importTestReport); @@ -542,7 +545,8 @@ protected function importTest( $folder, array $ignoreQtiResources = [], bool $overwriteTest = false, - ?string $itemClassUri = null + ?string $itemClassUri = null, + bool $importMetadata = false ) { /** @var ImportService $itemImportService */ $itemImportService = $this->getServiceLocator()->get(ImportService::SERVICE_ID); @@ -575,7 +579,6 @@ protected function importTest( $domManifest->load($folder . 'imsmanifest.xml'); $metadataValues = $this->getMetadataImporter()->extract($domManifest); - $metaMetadataValues = $this->getMetaMetadataExtractor()->extract($domManifest); // Note: without this fix, metadata guardians do not work. $this->getMetadataImporter()->setMetadataValues($metadataValues); @@ -589,9 +592,10 @@ protected function importTest( $reportCtx->overwrittenItems = []; $reportCtx->itemQtiResources = []; $reportCtx->testMetadata = $metadataValues[$qtiTestResourceIdentifier] ?? []; - $reportCtx->metaMetadata = $metaMetadataValues; $reportCtx->createdClasses = []; + + // 'uriResource' key is needed by javascript in tao/views/templates/form/import.tpl $reportCtx->uriResource = $testResource->getUri(); @@ -632,13 +636,20 @@ protected function importTest( } $targetItemClass = $itemParentClass->createSubClass(self::IN_PROGRESS_LABEL); - $mappedProperties = $this->getMetaMetadataImporter() - ->mapMetaMetadataToProperties($metaMetadataValues, $targetItemClass, $testClass); // add real label without saving (to not pass it separately to item importer) $targetItemClass->label = $testLabel; $reportCtx->itemClass = $targetItemClass; + + $mappedProperties = $this->getMappedProperties( + $importMetadata, + $domManifest, + $reportCtx, + $testClass, + $targetItemClass + ); + // -- Load all items related to test. $itemError = false; @@ -1516,4 +1527,21 @@ private function getMetaMetadataImporter(): MetaMetadataImportMapper { return $this->getServiceManager()->getContainer()->get(MetaMetadataImportMapper::class); } + + private function getMappedProperties( + bool $importMetadata, + DOMDocument $domManifest, + stdClass $reportCtx, + core_kernel_classes_Class $testClass, + core_kernel_classes_Class $targetItemClass + ): array { + if ($importMetadata === true) { + $metaMetadataValues = $this->getMetaMetadataExtractor()->extract($domManifest); + $reportCtx->metaMetadata = $metaMetadataValues; + return $this->getMetaMetadataImporter() + ->mapMetaMetadataToProperties($metaMetadataValues, $targetItemClass, $testClass); + } + + return []; + } } diff --git a/models/classes/import/class.TestImport.php b/models/classes/import/class.TestImport.php index 43b7a0aab..6380c074b 100755 --- a/models/classes/import/class.TestImport.php +++ b/models/classes/import/class.TestImport.php @@ -22,8 +22,10 @@ use oat\oatbox\event\EventManagerAwareTrait; use oat\oatbox\PhpSerializable; use oat\oatbox\PhpSerializeStateless; +use oat\tao\model\featureFlag\FeatureFlagChecker; use oat\tao\model\import\ImportHandlerHelperTrait; use oat\tao\model\import\TaskParameterProviderInterface; +use oat\taoQtiTest\models\classes\metadata\MetadataLomService; use oat\taoQtiTest\models\event\QtiTestImportEvent; use Zend\ServiceManager\ServiceLocatorAwareInterface; @@ -44,6 +46,9 @@ class taoQtiTest_models_classes_import_TestImport implements use EventManagerAwareTrait; use ImportHandlerHelperTrait; + public const DISABLED_FIELDS = 'disabledFields'; + public const METADATA_FIELD = 'metadataImport'; + /** * (non-PHPdoc) * @see tao_models_classes_import_ImportHandler::getLabel() @@ -59,7 +64,7 @@ public function getLabel() */ public function getForm() { - $form = new taoQtiTest_models_classes_import_TestImportForm(); + $form = new taoQtiTest_models_classes_import_TestImportForm([], $this->getFormOptions()); return $form->getForm(); } @@ -78,7 +83,8 @@ public function import($class, $form, $userId = null) // The zip extraction is a long process that can exceed the 30s timeout helpers_TimeOutHelper::setTimeOutLimit(helpers_TimeOutHelper::LONG); - $report = taoQtiTest_models_classes_QtiTestService::singleton()->importMultipleTests($class, $uploadedFile); + $report = taoQtiTest_models_classes_QtiTestService::singleton() + ->importMultipleTests($class, $uploadedFile, false, null, $form); helpers_TimeOutHelper::reset(); @@ -93,4 +99,18 @@ public function import($class, $form, $userId = null) return common_report_Report::createFailure($e->getMessage()); } } + + private function getFeatureFlagChecker(): FeatureFlagChecker + { + return $this->serviceLocator->getContainer()->get(FeatureFlagChecker::class); + } + + private function getFormOptions(): array + { + $options = []; + if (!$this->getFeatureFlagChecker()->isEnabled(MetadataLomService::FEATURE_FLAG)) { + $options[self::DISABLED_FIELDS] = [self::METADATA_FIELD]; + } + return $options; + } } diff --git a/models/classes/import/class.TestImportForm.php b/models/classes/import/class.TestImportForm.php index 4720efda7..97d8f9874 100755 --- a/models/classes/import/class.TestImportForm.php +++ b/models/classes/import/class.TestImportForm.php @@ -31,12 +31,8 @@ */ class taoQtiTest_models_classes_import_TestImportForm extends tao_helpers_form_FormContainer { - // --- ASSOCIATIONS --- + private const METADATA_FORM_ELEMENT_NAME = 'metadata'; - - // --- ATTRIBUTES --- - - // --- OPERATIONS --- /** * (non-PHPdoc) * @see tao_helpers_form_FormContainer::initForm() @@ -99,18 +95,41 @@ public function initElements() ) ]); + $this->form->addElement($fileElt); - /* - $disableValidationElt = tao_helpers_form_FormFactory::getElement("disable_validation", 'Checkbox'); - $disableValidationElt->setDescription(__("Disable validation")); - $disableValidationElt->setOptions(array("on" => "")); - $this->form->addElement($disableValidationElt); - */ - $this->form->createGroup('file', __('Import a QTI/APIP Content Package'), ['source']); + $this->form->createGroup( + 'file', + __('Import a QTI/APIP Content Package'), + [ + 'source', + ] + ); + $this->addMetadataImportElement(); $qtiSentElt = tao_helpers_form_FormFactory::getElement('import_sent_qti', 'Hidden'); $qtiSentElt->setValue(1); $this->form->addElement($qtiSentElt); } + + private function isMetadataDisabled(): bool + { + return isset($this->options[taoQtiTest_models_classes_import_TestImport::DISABLED_FIELDS]) && + in_array( + taoQtiTest_models_classes_import_TestImport::METADATA_FIELD, + $this->options[taoQtiTest_models_classes_import_TestImport::DISABLED_FIELDS] + ); + } + + private function addMetadataImportElement() + { + if (!$this->isMetadataDisabled()) { + $metadataImport = tao_helpers_form_FormFactory::getElement(self::METADATA_FORM_ELEMENT_NAME, 'Checkbox'); + $metadataImport->setOptions([self::METADATA_FORM_ELEMENT_NAME => __('Import metadata or fail')]); + $metadataImport->setDescription(__('Metadata import')); + $metadataImport->setLevel(1); + $this->form->addElement($metadataImport); + $this->form->addToGroup('file', self::METADATA_FORM_ELEMENT_NAME); + } + } } diff --git a/models/classes/metadata/MetadataLomService.php b/models/classes/metadata/MetadataLomService.php index 4d0c829f9..cc60b2fe5 100644 --- a/models/classes/metadata/MetadataLomService.php +++ b/models/classes/metadata/MetadataLomService.php @@ -27,6 +27,8 @@ class MetadataLomService { + public const FEATURE_FLAG = 'FEATURE_FLAG_METADATA_LOM_SERVICE'; + public function addPropertiesToMetadataBlock(array $properties, DOMDocument $manifest): void { /** @var DOMNodeList $metadataBlock */ From b516f30390ad3cfbe9dbcbb69ca1937b2528f065 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Thu, 2 May 2024 14:33:47 +0200 Subject: [PATCH 23/30] feat: phpcbf automatic fix --- models/classes/class.QtiTestService.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/models/classes/class.QtiTestService.php b/models/classes/class.QtiTestService.php index 0745ae7e7..8e98f7b15 100644 --- a/models/classes/class.QtiTestService.php +++ b/models/classes/class.QtiTestService.php @@ -1529,9 +1529,9 @@ private function getMetaMetadataImporter(): MetaMetadataImportMapper } private function getMappedProperties( - bool $importMetadata, - DOMDocument $domManifest, - stdClass $reportCtx, + bool $importMetadata, + DOMDocument $domManifest, + stdClass $reportCtx, core_kernel_classes_Class $testClass, core_kernel_classes_Class $targetItemClass ): array { From 361b66d1fb0a8aa7eb7c43af7f8abfea413e28ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Thu, 2 May 2024 17:15:33 +0200 Subject: [PATCH 24/30] feat: When importing a test, item metadata values are imported --- models/classes/class.QtiTestService.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/models/classes/class.QtiTestService.php b/models/classes/class.QtiTestService.php index 8e98f7b15..aaf94949c 100644 --- a/models/classes/class.QtiTestService.php +++ b/models/classes/class.QtiTestService.php @@ -717,7 +717,8 @@ protected function importTest( $this->itemMustExist, $this->itemMustBeOverwritten, $reportCtx->overwrittenItems, - $mappedProperties['itemProperties'] + $mappedProperties['itemProperties'], + $importMetadata ); $reportCtx->createdClasses = array_merge( From ad82c29abbfc90460228fcce0c340b9316d1590d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Fri, 3 May 2024 09:47:19 +0200 Subject: [PATCH 25/30] feat: Export using this same FF --- models/classes/export/AbstractQtiTestExporter.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/models/classes/export/AbstractQtiTestExporter.php b/models/classes/export/AbstractQtiTestExporter.php index 31989f1e3..fbac75b30 100644 --- a/models/classes/export/AbstractQtiTestExporter.php +++ b/models/classes/export/AbstractQtiTestExporter.php @@ -34,6 +34,7 @@ use oat\oatbox\reporting\ReportInterface; use oat\tao\model\featureFlag\FeatureFlagChecker; use oat\taoQtiTest\models\classes\metadata\GenericLomOntologyExtractor; +use oat\taoQtiTest\models\classes\metadata\MetadataLomService; use qtism\data\storage\xml\marshalling\MarshallingException; use qtism\data\storage\xml\XmlDocument; use oat\oatbox\filesystem\Directory; @@ -50,8 +51,6 @@ abstract class AbstractQtiTestExporter extends ItemExporter implements QtiTestExporterInterface { - public const FEATURE_FLAG_LOM_ONTOLOGY_EXTRACTION = 'FEATURE_FLAG_LOM_ONTOLOGY_EXTRACTION'; - /** The QTISM XmlDocument representing the Test to be exported. */ private XmlDocument $testDocument; @@ -178,7 +177,7 @@ public function export(array $options = []): Report // 3. Export test metadata to manifest $this->getMetadataExporter()->export($this->getItem(), $this->getManifest()); - if ($this->getFeatureFlagChecker()->isEnabled(self::FEATURE_FLAG_LOM_ONTOLOGY_EXTRACTION)) { + if ($this->getFeatureFlagChecker()->isEnabled(MetadataLomService::FEATURE_FLAG)) { $this->genericLomOntologyExtractor()->extract( array_merge([$this->getItem()], $this->getItems()), $this->getManifest() From 16ee3c9e7f99f1ea9c0baa95b38ab515b51cbf3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Mon, 6 May 2024 17:42:12 +0200 Subject: [PATCH 26/30] fix: define default array --- models/classes/class.QtiTestService.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/classes/class.QtiTestService.php b/models/classes/class.QtiTestService.php index aaf94949c..793bd9dbd 100644 --- a/models/classes/class.QtiTestService.php +++ b/models/classes/class.QtiTestService.php @@ -717,7 +717,7 @@ protected function importTest( $this->itemMustExist, $this->itemMustBeOverwritten, $reportCtx->overwrittenItems, - $mappedProperties['itemProperties'], + $mappedProperties['itemProperties'] ?? [], $importMetadata ); @@ -802,7 +802,7 @@ protected function importTest( // Metadata will be set as property values. $this->getMetadataImporter()->inject($qtiTestResource->getIdentifier(), $testResource); $this->getServiceManager()->getContainer()->get(MappedMetadataInjector::class)->inject( - $mappedProperties['testProperties'], + $mappedProperties['testProperties'] ?? [], $metadataValues[$qtiTestResourceIdentifier], $testResource ); From cd9a9a11e4d143e27b39db3e2e6ad3521a4408fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Tue, 7 May 2024 08:56:07 +0200 Subject: [PATCH 27/30] fix: Connect test import form --- models/classes/class.QtiTestService.php | 4 ++-- models/classes/import/class.TestImportForm.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/models/classes/class.QtiTestService.php b/models/classes/class.QtiTestService.php index 793bd9dbd..a7841f830 100644 --- a/models/classes/class.QtiTestService.php +++ b/models/classes/class.QtiTestService.php @@ -27,7 +27,6 @@ use oat\tao\model\resources\SecureResourceServiceInterface; use oat\tao\model\TaoOntology; use oat\taoItems\model\Command\DeleteItemCommand; -use oat\taoQtiItem\model\import\QtiPackageImport; use oat\taoQtiItem\model\qti\ImportService; use oat\taoQtiItem\model\qti\metadata\importer\MetadataImporter; use oat\taoQtiItem\model\qti\metadata\imsManifest\MetaMetadataExtractor; @@ -56,6 +55,7 @@ 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; /** * the QTI TestModel service. @@ -425,7 +425,7 @@ public function importMultipleTests( $alreadyImportedQtiResources, $overwriteTest, $itemClassUri, - $form[QtiPackageImport::METADATA_IMPORT_ELEMENT_NAME] ?? false + !empty($form[TestImportForm::METADATA_FORM_ELEMENT_NAME]) ?? false ); $report->add($importTestReport); diff --git a/models/classes/import/class.TestImportForm.php b/models/classes/import/class.TestImportForm.php index 97d8f9874..a35e355bc 100755 --- a/models/classes/import/class.TestImportForm.php +++ b/models/classes/import/class.TestImportForm.php @@ -31,7 +31,7 @@ */ class taoQtiTest_models_classes_import_TestImportForm extends tao_helpers_form_FormContainer { - private const METADATA_FORM_ELEMENT_NAME = 'metadata'; + public const METADATA_FORM_ELEMENT_NAME = 'metadata'; /** * (non-PHPdoc) From 431778fb2b63b4cfebf237826313c62314c7b742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Tue, 7 May 2024 09:36:51 +0200 Subject: [PATCH 28/30] fix: Decorate form with metadata. Overload getTaskParams --- models/classes/import/class.TestImport.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/models/classes/import/class.TestImport.php b/models/classes/import/class.TestImport.php index 6380c074b..418f9d509 100755 --- a/models/classes/import/class.TestImport.php +++ b/models/classes/import/class.TestImport.php @@ -25,9 +25,11 @@ use oat\tao\model\featureFlag\FeatureFlagChecker; use oat\tao\model\import\ImportHandlerHelperTrait; use oat\tao\model\import\TaskParameterProviderInterface; +use oat\tao\model\upload\UploadService; use oat\taoQtiTest\models\classes\metadata\MetadataLomService; use oat\taoQtiTest\models\event\QtiTestImportEvent; use Zend\ServiceManager\ServiceLocatorAwareInterface; +use taoQtiTest_models_classes_import_TestImportForm as TestImportForm; /** * Import handler for QTI packages @@ -99,6 +101,16 @@ public function import($class, $form, $userId = null) return common_report_Report::createFailure($e->getMessage()); } } + public function getTaskParameters(tao_helpers_form_Form $importForm) + { + $file = $this->getUploadService()->getUploadedFlyFile($importForm->getValue('importFile') + ?: $importForm->getValue('source')['uploaded_file']); + + return [ + 'uploaded_file' => $file->getPrefix(), // because of Async, we need the full path of the uploaded file + TestImportForm::METADATA_FORM_ELEMENT_NAME => $importForm->getValue('metadata'), + ]; + } private function getFeatureFlagChecker(): FeatureFlagChecker { @@ -113,4 +125,8 @@ private function getFormOptions(): array } return $options; } + private function getUploadService() + { + return $this->serviceLocator->get(UploadService::SERVICE_ID); + } } From effe9f70e7330e3073c211231005fce515c3addd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Tue, 7 May 2024 15:46:06 +0200 Subject: [PATCH 29/30] fix: oat-sa/extension-tao-itemqti - 30.10.0 dependency --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3424a60be..786884acc 100644 --- a/composer.json +++ b/composer.json @@ -66,7 +66,7 @@ "oat-sa/generis" : ">=15.22", "oat-sa/tao-core": ">=54.0.0", "oat-sa/extension-tao-item" : ">=12.1.0", - "oat-sa/extension-tao-itemqti" : "dev-feat/aut-3590-add-metadata-check-on-import", + "oat-sa/extension-tao-itemqti" : ">=30.10.0", "oat-sa/extension-tao-test" : ">=16.0.0", "oat-sa/extension-tao-delivery" : ">=15.0.0", "oat-sa/extension-tao-outcome" : ">=13.0.0", From d069b863d5739d5a6120049e5e4a122c84a1e974 Mon Sep 17 00:00:00 2001 From: github-actions Date: Tue, 7 May 2024 16:01:22 +0200 Subject: [PATCH 30/30] chore: bundle assets --- views/css/test-runner.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/css/test-runner.css b/views/css/test-runner.css index 10e25f133..14742d835 100644 --- a/views/css/test-runner.css +++ b/views/css/test-runner.css @@ -1,3 +1,3 @@ -@-o-keyframes loadingbar{0%{left:-10%}50%{left:90%}100%{left:-10%}}@-moz-keyframes loadingbar{0%{left:-10%}50%{left:90%}100%{left:-10%}}@-webkit-keyframes loadingbar{0%{left:-10%}50%{left:90%}100%{left:-10%}}@keyframes loadingbar{0%{left:-10%}50%{left:90%}100%{left:-10%}}.loading-bar{height:6px;position:absolute;width:100%;top:0px;display:none;z-index:10000;cursor:progress}.loading-bar.fixed{position:fixed;width:100%}.loading-bar.fixed:before{top:0 !important}.loading-bar.loading{display:block;overflow:hidden;top:58px}.loading-bar.loading:before{position:absolute;content:"";height:6px;width:20%;display:block;transform:translateZ(0);background:-webkit-linear-gradient(to right, transparent 0%, rgb(195, 90, 19) 20%, rgb(195, 90, 19) 80%, transparent 100%);background:-moz-linear-gradient(to right, transparent 0%, rgb(195, 90, 19) 20%, rgb(195, 90, 19) 80%, transparent 100%);background:-ms-linear-gradient(to right, transparent 0%, rgb(195, 90, 19) 20%, rgb(195, 90, 19) 80%, transparent 100%);background:-o-linear-gradient(to right, transparent 0%, rgb(195, 90, 19) 20%, rgb(195, 90, 19) 80%, transparent 100%);background:linear-gradient(to right, transparent 0%, rgb(195, 90, 19) 20%, rgb(195, 90, 19) 80%, transparent 100%);-webkit-animation:loadingbar 5s linear infinite;-moz-animation:loadingbar 5s linear infinite;-ms-animation:loadingbar 5s linear infinite;-o-animation:loadingbar 5s linear infinite;animation:loadingbar 5s linear infinite}.loading-bar.loading.loadingbar-covered{top:0px;overflow-y:visible}.loading-bar.loading.loadingbar-covered:before{top:86px}.no-version-warning .loading-bar.loadingbar-covered:before{top:58px}.section-container{top:0 !important}.section-container .flex-container-full{-ms-order:0;-webkit-order:0;order:0;flex-item-align:stretch;-ms-flex-item-align:stretch;-webkit-align-self:stretch;align-self:stretch;-ms-flex:0 0 100%;-webkit-flex:0 0 100%;flex:0 0 100%}.section-container .flex-container-half{-ms-order:0;-webkit-order:0;order:0;flex-item-align:stretch;-ms-flex-item-align:stretch;-webkit-align-self:stretch;align-self:stretch;-ms-flex:0 0 50%;-webkit-flex:0 0 50%;flex:0 0 50%}.section-container .flex-container-third{-ms-order:0;-webkit-order:0;order:0;flex-item-align:stretch;-ms-flex-item-align:stretch;-webkit-align-self:stretch;align-self:stretch;-ms-flex:0 0 33.3333333333%;-webkit-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%}.section-container .flex-container-quarter{-ms-order:0;-webkit-order:0;order:0;flex-item-align:stretch;-ms-flex-item-align:stretch;-webkit-align-self:stretch;align-self:stretch;-ms-flex:0 0 25%;-webkit-flex:0 0 25%;flex:0 0 25%}.section-container .flex-container-remaining{-ms-order:0;-webkit-order:0;order:0;flex-item-align:stretch;-ms-flex-item-align:stretch;-webkit-align-self:stretch;align-self:stretch;-ms-flex:1 1 480px;-webkit-flex:1 1 480px;flex:1 1 480px}.section-container .flex-container-main-form{-ms-order:0;-webkit-order:0;order:0;flex-item-align:stretch;-ms-flex-item-align:stretch;-webkit-align-self:stretch;align-self:stretch;-ms-flex:0 0 500px;-webkit-flex:0 0 500px;flex:0 0 500px;margin:0 20px 20px 0;width:100%}.section-container .flex-container-main-form .form-content{max-width:100%}.section-container .flex-container-navi{-ms-order:0;-webkit-order:0;order:0;flex-item-align:stretch;-ms-flex-item-align:stretch;-webkit-align-self:stretch;align-self:stretch;-ms-flex:0 0 280px;-webkit-flex:0 0 280px;flex:0 0 280px}.section-container .section-header{border:none}.section-container .content-panel{width:100%;height:100%;margin:0;padding:0;border:none !important;display:-ms-flex;display:-webkit-flex;display:flex;-ms-flex-direction:row;-webkit-flex-direction:row;flex-direction:row;-ms-flex-wrap:nowrap;-webkit-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-justify-content:flex-start;justify-content:flex-start;-webkit-align-content:flex-start;align-content:flex-start;-webkit-align-items:stretch;align-items:stretch}.section-container .tab-container{border:none;display:none;list-style-type:none;padding:0;margin:0}.section-container .tab-container li{float:left;position:relative;top:0;padding:0;margin:0 1px 0px 0;border-top:1px solid #f3f1ef !important;border-bottom:1px solid #f3f1ef !important;background:#f3f1ef !important}.section-container .tab-container li a{top:0 !important;margin-bottom:0 !important;padding:6px 16px;text-decoration:none;min-height:32px;color:#222;float:left}.section-container .tab-container li.active,.section-container .tab-container li:hover{border-bottom-color:#4a86ad !important;border-top-color:#6e9ebd !important;background:#266d9c !important}.section-container .tab-container li.active a,.section-container .tab-container li:hover a{background:rgba(0,0,0,0) !important;border-color:rgba(0,0,0,0) !important;color:#fff !important;text-shadow:1px 1px 0 rgba(0,0,0,.2)}.section-container .tab-container li.disabled:hover{background:#f3f1ef !important}.section-container .tab-container li.disabled:hover a{cursor:not-allowed !important;color:#222 !important}.section-container .navi-container{display:none;background:#f3f1ef;-ms-order:0;-webkit-order:0;order:0;flex-item-align:stretch;-ms-flex-item-align:stretch;-webkit-align-self:stretch;align-self:stretch;-ms-flex:0 0 280px;-webkit-flex:0 0 280px;flex:0 0 280px;border-right:1px #ddd solid}.section-container .navi-container .block-title{font-size:14px;font-size:1.4rem;padding:2px 8px;margin:0}.section-container .navi-container .tree-action-bar-box{margin:10px 0;opacity:0}.section-container .navi-container .tree-action-bar-box.active{opacity:1;-webkit-opacity:0.25s ease-in-out;-moz-opacity:0.25s ease-in-out;opacity:0.25s ease-in-out}.section-container .content-container{border:none;-ms-order:0;-webkit-order:0;order:0;flex-item-align:stretch;-ms-flex-item-align:stretch;-webkit-align-self:stretch;align-self:stretch;-ms-flex:1 1 auto;-webkit-flex:1 1 auto;flex:1 1 auto;-ms-flex:1 1;-webkit-flex:1 1;flex:1 1;max-width:100%}.section-container .navi-container+.content-container{max-width:calc(100% - 280px)}.section-container .content-block{padding:20px;overflow-y:auto;display:-ms-flex;display:-webkit-flex;display:flex;-ms-flex-direction:row;-webkit-flex-direction:row;flex-direction:row;-ms-flex-wrap:wrap;-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-justify-content:flex-start;justify-content:flex-start;-webkit-align-content:flex-start;align-content:flex-start;-webkit-align-items:stretch;align-items:stretch}.section-container .content-block>.grid-container{width:100%}.section-container .content-block .data-container-wrapper{padding:0px 20px 0 0}.section-container .content-block .data-container-wrapper:before,.section-container .content-block .data-container-wrapper:after{content:" ";display:table}.section-container .content-block .data-container-wrapper:after{clear:both}.section-container .content-block .data-container-wrapper>section,.section-container .content-block .data-container-wrapper .data-container{width:260px;margin:0 20px 20px 0;float:left;border:1px solid #ddd;border-radius:2px;-webkit-border-radius:2px}.section-container .content-block .data-container-wrapper>section.double,.section-container .content-block .data-container-wrapper .data-container.double{width:540px}.section-container .content-block .data-container-wrapper>section .emptyContentFooter,.section-container .content-block .data-container-wrapper .data-container .emptyContentFooter{display:none}.section-container .content-block .data-container-wrapper>section .tree,.section-container .content-block .data-container-wrapper .data-container .tree{border:none;max-width:none;max-height:none}.section-container .content-block .data-container-wrapper>section form,.section-container .content-block .data-container-wrapper .data-container form{background:none;border:none;margin:0;padding:0}.section-container .content-block .data-container-wrapper>section>header,.section-container .content-block .data-container-wrapper>section .ui-widget-header,.section-container .content-block .data-container-wrapper .data-container>header,.section-container .content-block .data-container-wrapper .data-container .ui-widget-header{background:#f3f1ef;border-width:0px !important;border-bottom:1px #ddd solid !important}.section-container .content-block .data-container-wrapper>section>header h1,.section-container .content-block .data-container-wrapper>section>header h6,.section-container .content-block .data-container-wrapper>section .ui-widget-header h1,.section-container .content-block .data-container-wrapper>section .ui-widget-header h6,.section-container .content-block .data-container-wrapper .data-container>header h1,.section-container .content-block .data-container-wrapper .data-container>header h6,.section-container .content-block .data-container-wrapper .data-container .ui-widget-header h1,.section-container .content-block .data-container-wrapper .data-container .ui-widget-header h6{padding:4px;margin:0;font-size:14px;font-size:1.4rem}.section-container .content-block .data-container-wrapper>section>div,.section-container .content-block .data-container-wrapper>section .ui-widget-content,.section-container .content-block .data-container-wrapper>section .container-content,.section-container .content-block .data-container-wrapper .data-container>div,.section-container .content-block .data-container-wrapper .data-container .ui-widget-content,.section-container .content-block .data-container-wrapper .data-container .container-content{border-width:0px !important;overflow-y:auto;min-height:250px;padding:5px}.section-container .content-block .data-container-wrapper>section>div .icon-grip,.section-container .content-block .data-container-wrapper>section .ui-widget-content .icon-grip,.section-container .content-block .data-container-wrapper>section .container-content .icon-grip,.section-container .content-block .data-container-wrapper .data-container>div .icon-grip,.section-container .content-block .data-container-wrapper .data-container .ui-widget-content .icon-grip,.section-container .content-block .data-container-wrapper .data-container .container-content .icon-grip{cursor:move}.section-container .content-block .data-container-wrapper>section>footer,.section-container .content-block .data-container-wrapper .data-container>footer{min-height:33px}.section-container .content-block .data-container-wrapper>section>footer,.section-container .content-block .data-container-wrapper>section .data-container-footer,.section-container .content-block .data-container-wrapper .data-container>footer,.section-container .content-block .data-container-wrapper .data-container .data-container-footer{background:#f3f1ef;text-align:right !important;padding:4px;border-width:0px !important;border-top:1px #ddd solid !important}.section-container .content-block .data-container-wrapper>section>footer .square,.section-container .content-block .data-container-wrapper>section .data-container-footer .square,.section-container .content-block .data-container-wrapper .data-container>footer .square,.section-container .content-block .data-container-wrapper .data-container .data-container-footer .square{width:28px}.section-container .content-block .data-container-wrapper>section>footer .square span,.section-container .content-block .data-container-wrapper>section .data-container-footer .square span,.section-container .content-block .data-container-wrapper .data-container>footer .square span,.section-container .content-block .data-container-wrapper .data-container .data-container-footer .square span{padding:0;left:0}.section-container .content-block .data-container-wrapper>section ol,.section-container .content-block .data-container-wrapper .data-container ol{margin:0 0 0 15px;padding:10px}.section-container .content-block #form-container.ui-widget-content{border:none !important}.section-container .content-block form:not(.list-container){border:1px #ddd solid;background:#f3f1ef;padding:30px;border:1px solid #ddd;border-radius:2px;-webkit-border-radius:2px}.section-container .content-block [class^=btn-],.section-container .content-block [class*=" btn-"]{margin:0 2px}.qti-navigator-default{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none;display:-ms-flex;display:-webkit-flex;display:flex;-ms-flex-direction:row;-webkit-flex-direction:row;flex-direction:row;-ms-flex-wrap:nowrap;-webkit-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-justify-content:flex-start;justify-content:flex-start;-webkit-align-content:flex-start;align-content:flex-start;-webkit-align-items:stretch;align-items:stretch;-webkit-flex-direction:column;-moz-flex-direction:column;-ms-flex-direction:column;-o-flex-direction:column;flex-direction:column;padding:0;cursor:default;min-width:calc(18rem - 8px);height:100%;position:relative}.qti-navigator-default span{display:inline-block}.qti-navigator-default .collapsed .collapsible-panel{display:none !important}.qti-navigator-default .collapsed .qti-navigator-label .icon-up{display:none}.qti-navigator-default .collapsed .qti-navigator-label .icon-down{display:inline-block}.qti-navigator-default .collapsible>.qti-navigator-label,.qti-navigator-default .qti-navigator-item>.qti-navigator-label{cursor:pointer}.qti-navigator-default.scope-test-section .qti-navigator-part>.qti-navigator-label{display:none !important}.qti-navigator-default .qti-navigator-label{display:-ms-flex;display:-webkit-flex;display:flex;-ms-flex-direction:row;-webkit-flex-direction:row;flex-direction:row;-ms-flex-wrap:nowrap;-webkit-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-justify-content:flex-start;justify-content:flex-start;-webkit-align-content:flex-start;align-content:flex-start;-webkit-align-items:stretch;align-items:stretch;min-width:calc(100% - 12px);padding:0 6px;line-height:3rem}.qti-navigator-default .qti-navigator-label .icon-up,.qti-navigator-default .qti-navigator-label .icon-down{line-height:3rem;margin-left:auto}.qti-navigator-default .qti-navigator-label .icon-down{display:none}.qti-navigator-default .qti-navigator-label .qti-navigator-number{display:none}.qti-navigator-default .qti-navigator-icon,.qti-navigator-default .icon{position:relative;top:1px;display:inline-block;line-height:2.8rem;margin-right:.5rem}.qti-navigator-default .unseen .qti-navigator-icon{cursor:default}.qti-navigator-default.prevents-unseen:not(.skipahead-enabled) .unseen .qti-navigator-icon,.qti-navigator-default.prevents-unseen:not(.skipahead-enabled) .unseen .qti-navigator-label{cursor:not-allowed !important}.qti-navigator-default .icon-answered:before{content:""}.qti-navigator-default .icon-viewed:before{content:""}.qti-navigator-default .icon-flagged:before{content:""}.qti-navigator-default .icon-unanswered:before,.qti-navigator-default .icon-unseen:before{content:""}.qti-navigator-default .qti-navigator-counter{text-align:right;margin-left:auto;font-size:12px;font-size:1.2rem}.qti-navigator-default .qti-navigator-actions{text-align:center}.qti-navigator-default .qti-navigator-info.collapsed{height:calc(3rem + 1px)}.qti-navigator-default .qti-navigator-info{height:calc(5*(3rem + 1px));overflow:hidden}.qti-navigator-default .qti-navigator-info>.qti-navigator-label{min-width:calc(100% - 16px);padding:0 8px}.qti-navigator-default .qti-navigator-info ul{padding:0 4px}.qti-navigator-default .qti-navigator-info ul .qti-navigator-label span.qti-navigator-text{padding:0 6px;min-width:10rem}.qti-navigator-default .qti-navigator-info ul .qti-navigator-label span.qti-navigator-icon{min-width:1.5rem}.qti-navigator-default .qti-navigator-info ul .qti-navigator-label span.qti-navigator-counter{min-width:5rem}.qti-navigator-default .qti-navigator-filters{margin-top:1rem;text-align:center;width:15rem;height:calc(3rem + 2*1px)}.qti-navigator-default .qti-navigator-filters ul{display:-ms-flex;display:-webkit-flex;display:flex;-ms-flex-direction:row;-webkit-flex-direction:row;flex-direction:row;-ms-flex-wrap:nowrap;-webkit-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-justify-content:flex-start;justify-content:flex-start;-webkit-align-content:flex-start;align-content:flex-start;-webkit-align-items:stretch;align-items:stretch}.qti-navigator-default .qti-navigator-filters li{display:block}.qti-navigator-default .qti-navigator-filters li .qti-navigator-tab{border:1px solid #ddd;border-radius:2px;-webkit-border-radius:2px;border-left:none;line-height:3rem;min-width:5rem;cursor:pointer;white-space:nowrap}.qti-navigator-default .qti-navigator-tree{-webkit-flex:1;-moz-flex:1;-ms-flex:1;-o-flex:1;flex:1;overflow-y:auto}.qti-navigator-default .qti-navigator-linear,.qti-navigator-default .qti-navigator-linear-part{padding:8px}.qti-navigator-default .qti-navigator-linear .icon,.qti-navigator-default .qti-navigator-linear-part .icon{display:none}.qti-navigator-default .qti-navigator-linear .qti-navigator-label,.qti-navigator-default .qti-navigator-linear-part .qti-navigator-label{font-size:14px;font-size:1.4rem}.qti-navigator-default .qti-navigator-linear .qti-navigator-title,.qti-navigator-default .qti-navigator-linear-part .qti-navigator-title{font-size:14px;font-size:1.4rem;margin:8px 0}.qti-navigator-default .qti-navigator-linear .qti-navigator-message,.qti-navigator-default .qti-navigator-linear-part .qti-navigator-message{font-size:14px;font-size:1.4rem}.qti-navigator-default .qti-navigator-part>.qti-navigator-label{padding:0 8px}.qti-navigator-default .qti-navigator-part:not(:first-child){margin-top:1px}.qti-navigator-default .qti-navigator-section>.qti-navigator-label{padding:0 8px}.qti-navigator-default .qti-navigator-item{margin:1px 0;padding-left:10px}.qti-navigator-default .qti-navigator-item:first-child{margin-top:0}.qti-navigator-default .qti-navigator-item.disabled>.qti-navigator-label{cursor:not-allowed}.qti-navigator-default .qti-navigator-collapsible{cursor:pointer;text-align:center;display:none;position:absolute;top:0;bottom:0;right:0;padding-top:50%}.qti-navigator-default .qti-navigator-collapsible .icon{font-size:20px;font-size:2rem;width:1rem !important;height:2rem !important}.qti-navigator-default .qti-navigator-collapsible .qti-navigator-expand{display:none}.qti-navigator-default.collapsible{padding-right:calc(1rem + 10px) !important}.qti-navigator-default.collapsible .qti-navigator-collapsible{display:block}.qti-navigator-default.collapsed{width:calc(8rem + 1rem + 10px);min-width:8rem}.qti-navigator-default.collapsed ul{padding:0 !important}.qti-navigator-default.collapsed .qti-navigator-text,.qti-navigator-default.collapsed .qti-navigator-info>.qti-navigator-label,.qti-navigator-default.collapsed .qti-navigator-part>.qti-navigator-label,.qti-navigator-default.collapsed .qti-navigator-section>.qti-navigator-label,.qti-navigator-default.collapsed .qti-navigator-message{display:none !important}.qti-navigator-default.collapsed .qti-navigator-label{padding:0 2px !important;width:calc(8rem - 4px);min-width:calc(8rem - 4px)}.qti-navigator-default.collapsed .qti-navigator-icon,.qti-navigator-default.collapsed .icon{width:auto}.qti-navigator-default.collapsed .qti-navigator-counter{margin-left:0;min-width:4rem !important}.qti-navigator-default.collapsed .qti-navigator-collapsible .qti-navigator-collapse{display:none}.qti-navigator-default.collapsed .qti-navigator-collapsible .qti-navigator-expand{display:block}.qti-navigator-default.collapsed .qti-navigator-info{height:calc(4*(3rem + 1px))}.qti-navigator-default.collapsed .qti-navigator-info.collapsed .collapsible-panel{display:block !important}.qti-navigator-default.collapsed .qti-navigator-filters{width:calc(8rem - 16px)}.qti-navigator-default.collapsed .qti-navigator-filter span{display:none}.qti-navigator-default.collapsed .qti-navigator-filter.active span{display:block;border:0 none;width:calc(8rem - 16px)}.qti-navigator-default.collapsed .qti-navigator-item,.qti-navigator-default.collapsed .qti-navigator-linear,.qti-navigator-default.collapsed .qti-navigator-linear-part{padding-left:2px;text-align:center}.qti-navigator-default.collapsed .qti-navigator-item{overflow:hidden}.qti-navigator-default.collapsed .qti-navigator-item .qti-navigator-icon{padding-left:6px;width:2rem}.qti-navigator-default.collapsed .qti-navigator-item .qti-navigator-number{display:inline-block;margin-left:6px;margin-right:8rem}.qti-navigator-default.collapsed .qti-navigator-linear,.qti-navigator-default.collapsed .qti-navigator-linear-part{padding:0 0 8px 0}.qti-navigator-default.collapsed .qti-navigator-linear .icon,.qti-navigator-default.collapsed .qti-navigator-linear-part .icon{display:block}.qti-navigator-default.collapsed .qti-navigator-actions button{padding:0 9px 0 5px}.qti-navigator-default .qti-navigator-info>.qti-navigator-label{background-color:#d4d5d7;color:#222;border-top:1px solid #d4d5d7}.qti-navigator-default .qti-navigator-info li{border-bottom:1px solid #fff}.qti-navigator-default .qti-navigator-filter .qti-navigator-tab{background-color:#fff}.qti-navigator-default .qti-navigator-filter .qti-navigator-tab:hover{background-color:#3e7da7;color:#fff}.qti-navigator-default .qti-navigator-filter.active .qti-navigator-tab{background-color:#a4a9b1;color:#fff}.qti-navigator-default .qti-navigator-linear,.qti-navigator-default .qti-navigator-linear-part{background:#fff}.qti-navigator-default .qti-navigator-part>.qti-navigator-label{background-color:#dddfe2}.qti-navigator-default .qti-navigator-part>.qti-navigator-label:hover{background-color:#c6cacf}.qti-navigator-default .qti-navigator-part.active>.qti-navigator-label{background-color:#c0c4ca}.qti-navigator-default .qti-navigator-section>.qti-navigator-label{border-bottom:1px solid #fff}.qti-navigator-default .qti-navigator-section>.qti-navigator-label:hover{background-color:#ebe8e4}.qti-navigator-default .qti-navigator-section.active>.qti-navigator-label{background-color:#ded9d4}.qti-navigator-default .qti-navigator-item{background:#fff}.qti-navigator-default .qti-navigator-item.active{background:#0e5d91;color:#fff}.qti-navigator-default .qti-navigator-item:hover{background:#0a3f62;color:#fff}.qti-navigator-default .qti-navigator-item.disabled{background-color:#e2deda !important}.qti-navigator-default .qti-navigator-collapsible{background-color:#dfe1e4;color:#222}.qti-navigator-default .qti-navigator-collapsible .icon{color:#fff}.qti-test-scope .action-bar li{margin:0 5px}.qti-test-scope .action-bar li.btn-info{border-color:rgba(255,255,255,.3)}.qti-test-scope .action-bar li.btn-info.btn-group{border:none !important;overflow:hidden;padding:0}.qti-test-scope .action-bar li.btn-info.btn-group a{float:left;margin:0 2px;padding:0 15px;border:1px solid rgba(255,255,255,.3);border-radius:0px;display:inline-block;height:inherit}.qti-test-scope .action-bar li.btn-info.btn-group a:first-of-type{border-top-left-radius:3px;border-bottom-left-radius:3px;margin-left:0}.qti-test-scope .action-bar li.btn-info.btn-group a:last-of-type{border-top-right-radius:3px;border-bottom-right-radius:3px;margin-right:0}.qti-test-scope .action-bar li.btn-info.btn-group a:hover,.qti-test-scope .action-bar li.btn-info.btn-group a.active{border-color:rgba(255,255,255,.8)}.qti-test-scope .action-bar li.btn-info.btn-group a .no-label{padding-right:0}.qti-test-scope .action-bar li.btn-info:hover,.qti-test-scope .action-bar li.btn-info.active{border-color:rgba(255,255,255,.8)}.qti-test-scope .action-bar.horizontal-action-bar{opacity:0}.qti-test-scope .action-bar.horizontal-action-bar .title-box{padding-top:4px}.qti-test-scope .action-bar.horizontal-action-bar .progress-box,.qti-test-scope .action-bar.horizontal-action-bar .timer-box,.qti-test-scope .action-bar.horizontal-action-bar .item-number-box{padding-top:4px;display:inline-block;white-space:nowrap;-webkit-flex:0 0 auto;flex:0 1 auto}.qti-test-scope .action-bar.horizontal-action-bar .progress-box .qti-controls,.qti-test-scope .action-bar.horizontal-action-bar .timer-box .qti-controls,.qti-test-scope .action-bar.horizontal-action-bar .item-number-box .qti-controls{display:inline-block;margin-left:20px;white-space:nowrap}.qti-test-scope .action-bar.horizontal-action-bar .progressbar{margin-top:5px;min-width:150px;max-width:200px;height:.6em}.qti-test-scope .action-bar.horizontal-action-bar.top-action-bar>.control-box{display:-webkit-flex;-webkit-justify-content:space-between;-webkit-flex-flow:row nowrap;display:flex;justify-content:space-between;flex-flow:row nowrap}.qti-test-scope .action-bar.horizontal-action-bar>.control-box{color:rgba(255,255,255,.9);text-shadow:1px 1px 0 #000}.qti-test-scope .action-bar.horizontal-action-bar>.control-box .lft,.qti-test-scope .action-bar.horizontal-action-bar>.control-box .rgt{padding-left:20px}.qti-test-scope .action-bar.horizontal-action-bar>.control-box .lft:first-child,.qti-test-scope .action-bar.horizontal-action-bar>.control-box .rgt:first-child{padding-left:0}.qti-test-scope .action-bar.horizontal-action-bar>.control-box .lft:last-child ul,.qti-test-scope .action-bar.horizontal-action-bar>.control-box .rgt:last-child ul{display:inline-block}.qti-test-scope .action-bar.horizontal-action-bar>.control-box [class^=btn-],.qti-test-scope .action-bar.horizontal-action-bar>.control-box [class*=" btn-"]{white-space:nowrap}.qti-test-scope .action-bar.horizontal-action-bar .tools-box .action{position:relative;overflow:visible}.qti-test-scope .action-bar.horizontal-action-bar .tools-box .menu{color:#222;background:#f3f1ef;overflow:auto;list-style:none;min-width:150px;margin:0;padding:0;position:absolute;bottom:30px;left:0}.qti-test-scope .action-bar.horizontal-action-bar .tools-box .menu .action{display:inline-block;text-align:left;width:100%;white-space:nowrap;overflow:hidden;color:#222;margin:0;-moz-border-radius:0px;-webkit-border-radius:0px;border-radius:0px;height:32px;padding:6px 15px;line-height:1}.qti-test-scope .action-bar.horizontal-action-bar .tools-box .menu .action.selected{background-color:#3e7da7;color:#fff}.qti-test-scope .action-bar.horizontal-action-bar .tools-box .menu .action.selected .label,.qti-test-scope .action-bar.horizontal-action-bar .tools-box .menu .action.selected .icon{color:#fff}.qti-test-scope .action-bar.horizontal-action-bar .tools-box .menu .action:hover{background-color:#0e5d91;color:#fff}.qti-test-scope .action-bar.horizontal-action-bar .tools-box .menu .action:hover .label,.qti-test-scope .action-bar.horizontal-action-bar .tools-box .menu .action:hover .icon{color:#fff}.qti-test-scope .action-bar.horizontal-action-bar .tools-box .menu .action .label,.qti-test-scope .action-bar.horizontal-action-bar .tools-box .menu .action .icon{font-size:14px;font-size:1.4rem;text-shadow:none;color:#222}.qti-test-scope .action-bar.horizontal-action-bar.bottom-action-bar{overflow:visible}.qti-test-scope .action-bar.horizontal-action-bar.bottom-action-bar .action{line-height:1.6}.qti-test-scope .action-bar.horizontal-action-bar.has-timers{height:47px}.qti-test-scope .action-bar.horizontal-action-bar.has-timers .progress-box,.qti-test-scope .action-bar.horizontal-action-bar.has-timers .title-box{padding-top:10px}.qti-test-scope .action-bar.horizontal-action-bar .bottom-action-bar .action{display:none}.qti-test-scope .test-sidebar{background:#f3f1ef;overflow:auto}.qti-test-scope .test-sidebar-left{border-right:1px #ddd solid}.qti-test-scope .test-sidebar-right{border-left:1px #ddd solid}.qti-test-scope .content-panel{height:auto !important}.qti-test-scope .content-panel #qti-content{-webkit-overflow-scrolling:touch;overflow-y:auto;font-size:0}.qti-test-scope .content-panel #qti-content #qti-rubrics{font-size:14px}.qti-test-scope #qti-item{width:100%;min-width:100%;height:auto;overflow:visible}.qti-test-scope .size-wrapper{max-width:1280px;margin:auto;width:100%}.qti-test-scope .tools-box{position:relative;overflow:visible}.qti-test-scope [data-control=qti-comment]{background-color:#f3f1ef;position:absolute;bottom:33px;left:8px;z-index:9999;text-align:right;padding:5px;border:1px solid #ddd;border-radius:2px;-webkit-border-radius:2px;-webkit-box-shadow:0 0 15px 1px rgba(0, 0, 0, 0.2);-moz-box-shadow:0 0 15px 1px rgba(0, 0, 0, 0.2);-ms-box-shadow:0 0 15px 1px rgba(0, 0, 0, 0.2);-o-box-shadow:0 0 15px 1px rgba(0, 0, 0, 0.2);box-shadow:0 0 15px 1px rgba(0, 0, 0, 0.2)}.qti-test-scope [data-control=qti-comment] textarea{display:block;height:100px;resize:none;width:350px;padding:3px;margin:0 0 10px 0;border:none;font-size:13px;font-size:1.3rem;border:1px solid #ddd;border-radius:2px;-webkit-border-radius:2px}.qti-test-scope #qti-timers{display:none}.qti-test-scope [data-control=exit]{margin-left:20px}.qti-test-scope [data-control=comment-toggle]{display:none}.qti-test-scope .qti-timer{display:inline-block;text-align:center;vertical-align:top;line-height:1.2;position:relative;padding:0 20px}.qti-test-scope .qti-timer .qti-timer_label{max-width:130px;font-size:12px;font-size:1.2rem}.qti-test-scope .qti-timer::before{content:" ";background:rgba(255,255,255,.3);width:1px;height:20px;position:absolute;left:0;top:5px}.qti-test-scope .qti-timer:first-child::before{content:none}.qti-test-scope.non-lti-context .title-box{display:none}.qti-test-scope #qti-rubrics{margin:auto;max-width:1024px;width:100%;padding:15px}.qti-test-scope #qti-rubrics .qti-rubricBlock{margin:20px 0}.qti-test-scope #qti-rubrics .hidden{display:none} +@-o-keyframes loadingbar{0%{left:-10%}50%{left:90%}100%{left:-10%}}@-moz-keyframes loadingbar{0%{left:-10%}50%{left:90%}100%{left:-10%}}@-webkit-keyframes loadingbar{0%{left:-10%}50%{left:90%}100%{left:-10%}}@keyframes loadingbar{0%{left:-10%}50%{left:90%}100%{left:-10%}}.loading-bar{height:6px;position:absolute;width:100%;top:0px;display:none;z-index:10000;cursor:progress}.loading-bar.fixed{position:fixed;width:100%}.loading-bar.fixed:before{top:0 !important}.loading-bar.loading{display:block;overflow:hidden;top:58px}.loading-bar.loading:before{position:absolute;content:"";height:6px;width:20%;display:block;transform:translateZ(0);background:-webkit-linear-gradient(to right, transparent 0%, rgb(195, 90, 19) 20%, rgb(195, 90, 19) 80%, transparent 100%);background:-moz-linear-gradient(to right, transparent 0%, rgb(195, 90, 19) 20%, rgb(195, 90, 19) 80%, transparent 100%);background:-ms-linear-gradient(to right, transparent 0%, rgb(195, 90, 19) 20%, rgb(195, 90, 19) 80%, transparent 100%);background:-o-linear-gradient(to right, transparent 0%, rgb(195, 90, 19) 20%, rgb(195, 90, 19) 80%, transparent 100%);background:linear-gradient(to right, transparent 0%, rgb(195, 90, 19) 20%, rgb(195, 90, 19) 80%, transparent 100%);-webkit-animation:loadingbar 5s linear infinite;-moz-animation:loadingbar 5s linear infinite;-ms-animation:loadingbar 5s linear infinite;-o-animation:loadingbar 5s linear infinite;animation:loadingbar 5s linear infinite}.loading-bar.loading.loadingbar-covered{top:0px;overflow-y:visible}.loading-bar.loading.loadingbar-covered:before{top:86px}.no-version-warning .loading-bar.loadingbar-covered:before{top:58px}.section-container{top:0 !important}.section-container .flex-container-full{-ms-order:0;-webkit-order:0;order:0;flex-item-align:stretch;-ms-flex-item-align:stretch;-webkit-align-self:stretch;align-self:stretch;-ms-flex:0 0 100%;-webkit-flex:0 0 100%;flex:0 0 100%}.section-container .flex-container-half{-ms-order:0;-webkit-order:0;order:0;flex-item-align:stretch;-ms-flex-item-align:stretch;-webkit-align-self:stretch;align-self:stretch;-ms-flex:0 0 50%;-webkit-flex:0 0 50%;flex:0 0 50%}.section-container .flex-container-third{-ms-order:0;-webkit-order:0;order:0;flex-item-align:stretch;-ms-flex-item-align:stretch;-webkit-align-self:stretch;align-self:stretch;-ms-flex:0 0 33.3333333333%;-webkit-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%}.section-container .flex-container-quarter{-ms-order:0;-webkit-order:0;order:0;flex-item-align:stretch;-ms-flex-item-align:stretch;-webkit-align-self:stretch;align-self:stretch;-ms-flex:0 0 25%;-webkit-flex:0 0 25%;flex:0 0 25%}.section-container .flex-container-remaining{-ms-order:0;-webkit-order:0;order:0;flex-item-align:stretch;-ms-flex-item-align:stretch;-webkit-align-self:stretch;align-self:stretch;-ms-flex:1 1 480px;-webkit-flex:1 1 480px;flex:1 1 480px}.section-container .flex-container-main-form{-ms-order:0;-webkit-order:0;order:0;flex-item-align:stretch;-ms-flex-item-align:stretch;-webkit-align-self:stretch;align-self:stretch;-ms-flex:0 0 500px;-webkit-flex:0 0 500px;flex:0 0 500px;margin:0 20px 20px 0;width:100%}.section-container .flex-container-main-form .form-content{max-width:100%}.section-container .flex-container-navi{-ms-order:0;-webkit-order:0;order:0;flex-item-align:stretch;-ms-flex-item-align:stretch;-webkit-align-self:stretch;align-self:stretch;-ms-flex:0 0 380px;-webkit-flex:0 0 380px;flex:0 0 380px}.section-container .section-header{border:none}.section-container .content-panel{width:100%;height:100%;margin:0;padding:0;border:none !important;display:-ms-flex;display:-webkit-flex;display:flex;-ms-flex-direction:row;-webkit-flex-direction:row;flex-direction:row;-ms-flex-wrap:nowrap;-webkit-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-justify-content:flex-start;justify-content:flex-start;-webkit-align-content:flex-start;align-content:flex-start;-webkit-align-items:stretch;align-items:stretch}.section-container .tab-container{border:none;display:none;list-style-type:none;padding:0;margin:0}.section-container .tab-container li{float:left;position:relative;top:0;padding:0;margin:0 1px 0px 0;border-top:1px solid #f3f1ef !important;border-bottom:1px solid #f3f1ef !important;background:#f3f1ef !important}.section-container .tab-container li a{top:0 !important;margin-bottom:0 !important;padding:6px 16px;text-decoration:none;min-height:32px;color:#222;float:left}.section-container .tab-container li.active,.section-container .tab-container li:hover{border-bottom-color:#4a86ad !important;border-top-color:#6e9ebd !important;background:#266d9c !important}.section-container .tab-container li.active a,.section-container .tab-container li:hover a{background:rgba(0,0,0,0) !important;border-color:rgba(0,0,0,0) !important;color:#fff !important;text-shadow:1px 1px 0 rgba(0,0,0,.2)}.section-container .tab-container li.disabled:hover{background:#f3f1ef !important}.section-container .tab-container li.disabled:hover a{cursor:not-allowed !important;color:#222 !important}.section-container .navi-container{display:none;background:#f3f1ef;-ms-order:0;-webkit-order:0;order:0;flex-item-align:stretch;-ms-flex-item-align:stretch;-webkit-align-self:stretch;align-self:stretch;-ms-flex:0 0 380px;-webkit-flex:0 0 380px;flex:0 0 380px;border-right:1px #ddd solid}.section-container .navi-container .block-title{font-size:14px;font-size:1.4rem;padding:2px 8px;margin:0}.section-container .navi-container .tree-action-bar-box{margin:10px 0;opacity:0}.section-container .navi-container .tree-action-bar-box.active{opacity:1;-webkit-opacity:0.25s ease-in-out;-moz-opacity:0.25s ease-in-out;opacity:0.25s ease-in-out}.section-container .content-container{border:none;-ms-order:0;-webkit-order:0;order:0;flex-item-align:stretch;-ms-flex-item-align:stretch;-webkit-align-self:stretch;align-self:stretch;-ms-flex:1 1 auto;-webkit-flex:1 1 auto;flex:1 1 auto;-ms-flex:1 1;-webkit-flex:1 1;flex:1 1;max-width:100%}.section-container .navi-container+.content-container{max-width:calc(100% - 380px)}.section-container .content-block{padding:20px;overflow-y:auto;display:-ms-flex;display:-webkit-flex;display:flex;-ms-flex-direction:row;-webkit-flex-direction:row;flex-direction:row;-ms-flex-wrap:wrap;-webkit-flex-wrap:wrap;flex-wrap:wrap;-webkit-justify-content:flex-start;justify-content:flex-start;-webkit-align-content:flex-start;align-content:flex-start;-webkit-align-items:stretch;align-items:stretch}.section-container .content-block>.grid-container{width:100%}.section-container .content-block .data-container-wrapper{padding:0px 20px 0 0}.section-container .content-block .data-container-wrapper:before,.section-container .content-block .data-container-wrapper:after{content:" ";display:table}.section-container .content-block .data-container-wrapper:after{clear:both}.section-container .content-block .data-container-wrapper>section,.section-container .content-block .data-container-wrapper .data-container{width:260px;margin:0 20px 20px 0;float:left;border:1px solid #ddd;border-radius:2px;-webkit-border-radius:2px}.section-container .content-block .data-container-wrapper>section.double,.section-container .content-block .data-container-wrapper .data-container.double{width:540px}.section-container .content-block .data-container-wrapper>section .emptyContentFooter,.section-container .content-block .data-container-wrapper .data-container .emptyContentFooter{display:none}.section-container .content-block .data-container-wrapper>section .tree,.section-container .content-block .data-container-wrapper .data-container .tree{border:none;max-width:none;max-height:none}.section-container .content-block .data-container-wrapper>section form,.section-container .content-block .data-container-wrapper .data-container form{background:none;border:none;margin:0;padding:0}.section-container .content-block .data-container-wrapper>section>header,.section-container .content-block .data-container-wrapper>section .ui-widget-header,.section-container .content-block .data-container-wrapper .data-container>header,.section-container .content-block .data-container-wrapper .data-container .ui-widget-header{background:#f3f1ef;border-width:0px !important;border-bottom:1px #ddd solid !important}.section-container .content-block .data-container-wrapper>section>header h1,.section-container .content-block .data-container-wrapper>section>header h6,.section-container .content-block .data-container-wrapper>section .ui-widget-header h1,.section-container .content-block .data-container-wrapper>section .ui-widget-header h6,.section-container .content-block .data-container-wrapper .data-container>header h1,.section-container .content-block .data-container-wrapper .data-container>header h6,.section-container .content-block .data-container-wrapper .data-container .ui-widget-header h1,.section-container .content-block .data-container-wrapper .data-container .ui-widget-header h6{padding:4px;margin:0;font-size:14px;font-size:1.4rem}.section-container .content-block .data-container-wrapper>section>div,.section-container .content-block .data-container-wrapper>section .ui-widget-content,.section-container .content-block .data-container-wrapper>section .container-content,.section-container .content-block .data-container-wrapper .data-container>div,.section-container .content-block .data-container-wrapper .data-container .ui-widget-content,.section-container .content-block .data-container-wrapper .data-container .container-content{border-width:0px !important;overflow-y:auto;min-height:250px;padding:5px}.section-container .content-block .data-container-wrapper>section>div .icon-grip,.section-container .content-block .data-container-wrapper>section .ui-widget-content .icon-grip,.section-container .content-block .data-container-wrapper>section .container-content .icon-grip,.section-container .content-block .data-container-wrapper .data-container>div .icon-grip,.section-container .content-block .data-container-wrapper .data-container .ui-widget-content .icon-grip,.section-container .content-block .data-container-wrapper .data-container .container-content .icon-grip{cursor:move}.section-container .content-block .data-container-wrapper>section>footer,.section-container .content-block .data-container-wrapper .data-container>footer{min-height:33px}.section-container .content-block .data-container-wrapper>section>footer,.section-container .content-block .data-container-wrapper>section .data-container-footer,.section-container .content-block .data-container-wrapper .data-container>footer,.section-container .content-block .data-container-wrapper .data-container .data-container-footer{background:#f3f1ef;text-align:right !important;padding:4px;border-width:0px !important;border-top:1px #ddd solid !important}.section-container .content-block .data-container-wrapper>section>footer .square,.section-container .content-block .data-container-wrapper>section .data-container-footer .square,.section-container .content-block .data-container-wrapper .data-container>footer .square,.section-container .content-block .data-container-wrapper .data-container .data-container-footer .square{width:28px}.section-container .content-block .data-container-wrapper>section>footer .square span,.section-container .content-block .data-container-wrapper>section .data-container-footer .square span,.section-container .content-block .data-container-wrapper .data-container>footer .square span,.section-container .content-block .data-container-wrapper .data-container .data-container-footer .square span{padding:0;left:0}.section-container .content-block .data-container-wrapper>section ol,.section-container .content-block .data-container-wrapper .data-container ol{margin:0 0 0 15px;padding:10px}.section-container .content-block #form-container.ui-widget-content{border:none !important}.section-container .content-block form:not(.list-container){border:1px #ddd solid;background:#f3f1ef;padding:30px;border:1px solid #ddd;border-radius:2px;-webkit-border-radius:2px}.section-container .content-block [class^=btn-],.section-container .content-block [class*=" btn-"]{margin:0 2px}.qti-navigator-default{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none;display:-ms-flex;display:-webkit-flex;display:flex;-ms-flex-direction:row;-webkit-flex-direction:row;flex-direction:row;-ms-flex-wrap:nowrap;-webkit-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-justify-content:flex-start;justify-content:flex-start;-webkit-align-content:flex-start;align-content:flex-start;-webkit-align-items:stretch;align-items:stretch;-webkit-flex-direction:column;-moz-flex-direction:column;-ms-flex-direction:column;-o-flex-direction:column;flex-direction:column;padding:0;cursor:default;min-width:calc(18rem - 8px);height:100%;position:relative}.qti-navigator-default span{display:inline-block}.qti-navigator-default .collapsed .collapsible-panel{display:none !important}.qti-navigator-default .collapsed .qti-navigator-label .icon-up{display:none}.qti-navigator-default .collapsed .qti-navigator-label .icon-down{display:inline-block}.qti-navigator-default .collapsible>.qti-navigator-label,.qti-navigator-default .qti-navigator-item>.qti-navigator-label{cursor:pointer}.qti-navigator-default.scope-test-section .qti-navigator-part>.qti-navigator-label{display:none !important}.qti-navigator-default .qti-navigator-label{display:-ms-flex;display:-webkit-flex;display:flex;-ms-flex-direction:row;-webkit-flex-direction:row;flex-direction:row;-ms-flex-wrap:nowrap;-webkit-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-justify-content:flex-start;justify-content:flex-start;-webkit-align-content:flex-start;align-content:flex-start;-webkit-align-items:stretch;align-items:stretch;min-width:calc(100% - 12px);padding:0 6px;line-height:3rem}.qti-navigator-default .qti-navigator-label .icon-up,.qti-navigator-default .qti-navigator-label .icon-down{line-height:3rem;margin-left:auto}.qti-navigator-default .qti-navigator-label .icon-down{display:none}.qti-navigator-default .qti-navigator-label .qti-navigator-number{display:none}.qti-navigator-default .qti-navigator-icon,.qti-navigator-default .icon{position:relative;top:1px;display:inline-block;line-height:2.8rem;margin-right:.5rem}.qti-navigator-default .unseen .qti-navigator-icon{cursor:default}.qti-navigator-default.prevents-unseen:not(.skipahead-enabled) .unseen .qti-navigator-icon,.qti-navigator-default.prevents-unseen:not(.skipahead-enabled) .unseen .qti-navigator-label{cursor:not-allowed !important}.qti-navigator-default .icon-answered:before{content:""}.qti-navigator-default .icon-viewed:before{content:""}.qti-navigator-default .icon-flagged:before{content:""}.qti-navigator-default .icon-unanswered:before,.qti-navigator-default .icon-unseen:before{content:""}.qti-navigator-default .qti-navigator-counter{text-align:right;margin-left:auto;font-size:12px;font-size:1.2rem}.qti-navigator-default .qti-navigator-actions{text-align:center}.qti-navigator-default .qti-navigator-info.collapsed{height:calc(3rem + 1px)}.qti-navigator-default .qti-navigator-info{height:calc(5*(3rem + 1px));overflow:hidden}.qti-navigator-default .qti-navigator-info>.qti-navigator-label{min-width:calc(100% - 16px);padding:0 8px}.qti-navigator-default .qti-navigator-info ul{padding:0 4px}.qti-navigator-default .qti-navigator-info ul .qti-navigator-label span.qti-navigator-text{padding:0 6px;min-width:10rem}.qti-navigator-default .qti-navigator-info ul .qti-navigator-label span.qti-navigator-icon{min-width:1.5rem}.qti-navigator-default .qti-navigator-info ul .qti-navigator-label span.qti-navigator-counter{min-width:5rem}.qti-navigator-default .qti-navigator-filters{margin-top:1rem;text-align:center;width:15rem;height:calc(3rem + 2*1px)}.qti-navigator-default .qti-navigator-filters ul{display:-ms-flex;display:-webkit-flex;display:flex;-ms-flex-direction:row;-webkit-flex-direction:row;flex-direction:row;-ms-flex-wrap:nowrap;-webkit-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-justify-content:flex-start;justify-content:flex-start;-webkit-align-content:flex-start;align-content:flex-start;-webkit-align-items:stretch;align-items:stretch}.qti-navigator-default .qti-navigator-filters li{display:block}.qti-navigator-default .qti-navigator-filters li .qti-navigator-tab{border:1px solid #ddd;border-radius:2px;-webkit-border-radius:2px;border-left:none;line-height:3rem;min-width:5rem;cursor:pointer;white-space:nowrap}.qti-navigator-default .qti-navigator-tree{-webkit-flex:1;-moz-flex:1;-ms-flex:1;-o-flex:1;flex:1;overflow-y:auto}.qti-navigator-default .qti-navigator-linear,.qti-navigator-default .qti-navigator-linear-part{padding:8px}.qti-navigator-default .qti-navigator-linear .icon,.qti-navigator-default .qti-navigator-linear-part .icon{display:none}.qti-navigator-default .qti-navigator-linear .qti-navigator-label,.qti-navigator-default .qti-navigator-linear-part .qti-navigator-label{font-size:14px;font-size:1.4rem}.qti-navigator-default .qti-navigator-linear .qti-navigator-title,.qti-navigator-default .qti-navigator-linear-part .qti-navigator-title{font-size:14px;font-size:1.4rem;margin:8px 0}.qti-navigator-default .qti-navigator-linear .qti-navigator-message,.qti-navigator-default .qti-navigator-linear-part .qti-navigator-message{font-size:14px;font-size:1.4rem}.qti-navigator-default .qti-navigator-part>.qti-navigator-label{padding:0 8px}.qti-navigator-default .qti-navigator-part:not(:first-child){margin-top:1px}.qti-navigator-default .qti-navigator-section>.qti-navigator-label{padding:0 8px}.qti-navigator-default .qti-navigator-item{margin:1px 0;padding-left:10px}.qti-navigator-default .qti-navigator-item:first-child{margin-top:0}.qti-navigator-default .qti-navigator-item.disabled>.qti-navigator-label{cursor:not-allowed}.qti-navigator-default .qti-navigator-collapsible{cursor:pointer;text-align:center;display:none;position:absolute;top:0;bottom:0;right:0;padding-top:50%}.qti-navigator-default .qti-navigator-collapsible .icon{font-size:20px;font-size:2rem;width:1rem !important;height:2rem !important}.qti-navigator-default .qti-navigator-collapsible .qti-navigator-expand{display:none}.qti-navigator-default.collapsible{padding-right:calc(1rem + 10px) !important}.qti-navigator-default.collapsible .qti-navigator-collapsible{display:block}.qti-navigator-default.collapsed{width:calc(8rem + 1rem + 10px);min-width:8rem}.qti-navigator-default.collapsed ul{padding:0 !important}.qti-navigator-default.collapsed .qti-navigator-text,.qti-navigator-default.collapsed .qti-navigator-info>.qti-navigator-label,.qti-navigator-default.collapsed .qti-navigator-part>.qti-navigator-label,.qti-navigator-default.collapsed .qti-navigator-section>.qti-navigator-label,.qti-navigator-default.collapsed .qti-navigator-message{display:none !important}.qti-navigator-default.collapsed .qti-navigator-label{padding:0 2px !important;width:calc(8rem - 4px);min-width:calc(8rem - 4px)}.qti-navigator-default.collapsed .qti-navigator-icon,.qti-navigator-default.collapsed .icon{width:auto}.qti-navigator-default.collapsed .qti-navigator-counter{margin-left:0;min-width:4rem !important}.qti-navigator-default.collapsed .qti-navigator-collapsible .qti-navigator-collapse{display:none}.qti-navigator-default.collapsed .qti-navigator-collapsible .qti-navigator-expand{display:block}.qti-navigator-default.collapsed .qti-navigator-info{height:calc(4*(3rem + 1px))}.qti-navigator-default.collapsed .qti-navigator-info.collapsed .collapsible-panel{display:block !important}.qti-navigator-default.collapsed .qti-navigator-filters{width:calc(8rem - 16px)}.qti-navigator-default.collapsed .qti-navigator-filter span{display:none}.qti-navigator-default.collapsed .qti-navigator-filter.active span{display:block;border:0 none;width:calc(8rem - 16px)}.qti-navigator-default.collapsed .qti-navigator-item,.qti-navigator-default.collapsed .qti-navigator-linear,.qti-navigator-default.collapsed .qti-navigator-linear-part{padding-left:2px;text-align:center}.qti-navigator-default.collapsed .qti-navigator-item{overflow:hidden}.qti-navigator-default.collapsed .qti-navigator-item .qti-navigator-icon{padding-left:6px;width:2rem}.qti-navigator-default.collapsed .qti-navigator-item .qti-navigator-number{display:inline-block;margin-left:6px;margin-right:8rem}.qti-navigator-default.collapsed .qti-navigator-linear,.qti-navigator-default.collapsed .qti-navigator-linear-part{padding:0 0 8px 0}.qti-navigator-default.collapsed .qti-navigator-linear .icon,.qti-navigator-default.collapsed .qti-navigator-linear-part .icon{display:block}.qti-navigator-default.collapsed .qti-navigator-actions button{padding:0 9px 0 5px}.qti-navigator-default .qti-navigator-info>.qti-navigator-label{background-color:#d4d5d7;color:#222;border-top:1px solid #d4d5d7}.qti-navigator-default .qti-navigator-info li{border-bottom:1px solid #fff}.qti-navigator-default .qti-navigator-filter .qti-navigator-tab{background-color:#fff}.qti-navigator-default .qti-navigator-filter .qti-navigator-tab:hover{background-color:#3e7da7;color:#fff}.qti-navigator-default .qti-navigator-filter.active .qti-navigator-tab{background-color:#a4a9b1;color:#fff}.qti-navigator-default .qti-navigator-linear,.qti-navigator-default .qti-navigator-linear-part{background:#fff}.qti-navigator-default .qti-navigator-part>.qti-navigator-label{background-color:#dddfe2}.qti-navigator-default .qti-navigator-part>.qti-navigator-label:hover{background-color:#c6cacf}.qti-navigator-default .qti-navigator-part.active>.qti-navigator-label{background-color:#c0c4ca}.qti-navigator-default .qti-navigator-section>.qti-navigator-label{border-bottom:1px solid #fff}.qti-navigator-default .qti-navigator-section>.qti-navigator-label:hover{background-color:#ebe8e4}.qti-navigator-default .qti-navigator-section.active>.qti-navigator-label{background-color:#ded9d4}.qti-navigator-default .qti-navigator-item{background:#fff}.qti-navigator-default .qti-navigator-item.active{background:#0e5d91;color:#fff}.qti-navigator-default .qti-navigator-item:hover{background:#0a3f62;color:#fff}.qti-navigator-default .qti-navigator-item.disabled{background-color:#e2deda !important}.qti-navigator-default .qti-navigator-collapsible{background-color:#dfe1e4;color:#222}.qti-navigator-default .qti-navigator-collapsible .icon{color:#fff}.qti-test-scope .action-bar li{margin:0 5px}.qti-test-scope .action-bar li.btn-info{border-color:rgba(255,255,255,.3)}.qti-test-scope .action-bar li.btn-info.btn-group{border:none !important;overflow:hidden;padding:0}.qti-test-scope .action-bar li.btn-info.btn-group a{float:left;margin:0 2px;padding:0 15px;border:1px solid rgba(255,255,255,.3);border-radius:0px;display:inline-block;height:inherit}.qti-test-scope .action-bar li.btn-info.btn-group a:first-of-type{border-top-left-radius:3px;border-bottom-left-radius:3px;margin-left:0}.qti-test-scope .action-bar li.btn-info.btn-group a:last-of-type{border-top-right-radius:3px;border-bottom-right-radius:3px;margin-right:0}.qti-test-scope .action-bar li.btn-info.btn-group a:hover,.qti-test-scope .action-bar li.btn-info.btn-group a.active{border-color:rgba(255,255,255,.8)}.qti-test-scope .action-bar li.btn-info.btn-group a .no-label{padding-right:0}.qti-test-scope .action-bar li.btn-info:hover,.qti-test-scope .action-bar li.btn-info.active{border-color:rgba(255,255,255,.8)}.qti-test-scope .action-bar.horizontal-action-bar{opacity:0}.qti-test-scope .action-bar.horizontal-action-bar .title-box{padding-top:4px}.qti-test-scope .action-bar.horizontal-action-bar .progress-box,.qti-test-scope .action-bar.horizontal-action-bar .timer-box,.qti-test-scope .action-bar.horizontal-action-bar .item-number-box{padding-top:4px;display:inline-block;white-space:nowrap;-webkit-flex:0 0 auto;flex:0 1 auto}.qti-test-scope .action-bar.horizontal-action-bar .progress-box .qti-controls,.qti-test-scope .action-bar.horizontal-action-bar .timer-box .qti-controls,.qti-test-scope .action-bar.horizontal-action-bar .item-number-box .qti-controls{display:inline-block;margin-left:20px;white-space:nowrap}.qti-test-scope .action-bar.horizontal-action-bar .progressbar{margin-top:5px;min-width:150px;max-width:200px;height:.6em}.qti-test-scope .action-bar.horizontal-action-bar.top-action-bar>.control-box{display:-webkit-flex;-webkit-justify-content:space-between;-webkit-flex-flow:row nowrap;display:flex;justify-content:space-between;flex-flow:row nowrap}.qti-test-scope .action-bar.horizontal-action-bar>.control-box{color:rgba(255,255,255,.9);text-shadow:1px 1px 0 #000}.qti-test-scope .action-bar.horizontal-action-bar>.control-box .lft,.qti-test-scope .action-bar.horizontal-action-bar>.control-box .rgt{padding-left:20px}.qti-test-scope .action-bar.horizontal-action-bar>.control-box .lft:first-child,.qti-test-scope .action-bar.horizontal-action-bar>.control-box .rgt:first-child{padding-left:0}.qti-test-scope .action-bar.horizontal-action-bar>.control-box .lft:last-child ul,.qti-test-scope .action-bar.horizontal-action-bar>.control-box .rgt:last-child ul{display:inline-block}.qti-test-scope .action-bar.horizontal-action-bar>.control-box [class^=btn-],.qti-test-scope .action-bar.horizontal-action-bar>.control-box [class*=" btn-"]{white-space:nowrap}.qti-test-scope .action-bar.horizontal-action-bar .tools-box .action{position:relative;overflow:visible}.qti-test-scope .action-bar.horizontal-action-bar .tools-box .menu{color:#222;background:#f3f1ef;overflow:auto;list-style:none;min-width:150px;margin:0;padding:0;position:absolute;bottom:30px;left:0}.qti-test-scope .action-bar.horizontal-action-bar .tools-box .menu .action{display:inline-block;text-align:left;width:100%;white-space:nowrap;overflow:hidden;color:#222;margin:0;-moz-border-radius:0px;-webkit-border-radius:0px;border-radius:0px;height:32px;padding:6px 15px;line-height:1}.qti-test-scope .action-bar.horizontal-action-bar .tools-box .menu .action.selected{background-color:#3e7da7;color:#fff}.qti-test-scope .action-bar.horizontal-action-bar .tools-box .menu .action.selected .label,.qti-test-scope .action-bar.horizontal-action-bar .tools-box .menu .action.selected .icon{color:#fff}.qti-test-scope .action-bar.horizontal-action-bar .tools-box .menu .action:hover{background-color:#0e5d91;color:#fff}.qti-test-scope .action-bar.horizontal-action-bar .tools-box .menu .action:hover .label,.qti-test-scope .action-bar.horizontal-action-bar .tools-box .menu .action:hover .icon{color:#fff}.qti-test-scope .action-bar.horizontal-action-bar .tools-box .menu .action .label,.qti-test-scope .action-bar.horizontal-action-bar .tools-box .menu .action .icon{font-size:14px;font-size:1.4rem;text-shadow:none;color:#222}.qti-test-scope .action-bar.horizontal-action-bar.bottom-action-bar{overflow:visible}.qti-test-scope .action-bar.horizontal-action-bar.bottom-action-bar .action{line-height:1.6}.qti-test-scope .action-bar.horizontal-action-bar.has-timers{height:47px}.qti-test-scope .action-bar.horizontal-action-bar.has-timers .progress-box,.qti-test-scope .action-bar.horizontal-action-bar.has-timers .title-box{padding-top:10px}.qti-test-scope .action-bar.horizontal-action-bar .bottom-action-bar .action{display:none}.qti-test-scope .test-sidebar{background:#f3f1ef;overflow:auto}.qti-test-scope .test-sidebar-left{border-right:1px #ddd solid}.qti-test-scope .test-sidebar-right{border-left:1px #ddd solid}.qti-test-scope .content-panel{height:auto !important}.qti-test-scope .content-panel #qti-content{-webkit-overflow-scrolling:touch;overflow-y:auto;font-size:0}.qti-test-scope .content-panel #qti-content #qti-rubrics{font-size:14px}.qti-test-scope #qti-item{width:100%;min-width:100%;height:auto;overflow:visible}.qti-test-scope .size-wrapper{max-width:1280px;margin:auto;width:100%}.qti-test-scope .tools-box{position:relative;overflow:visible}.qti-test-scope [data-control=qti-comment]{background-color:#f3f1ef;position:absolute;bottom:33px;left:8px;z-index:9999;text-align:right;padding:5px;border:1px solid #ddd;border-radius:2px;-webkit-border-radius:2px;-webkit-box-shadow:0 0 15px 1px rgba(0, 0, 0, 0.2);-moz-box-shadow:0 0 15px 1px rgba(0, 0, 0, 0.2);-ms-box-shadow:0 0 15px 1px rgba(0, 0, 0, 0.2);-o-box-shadow:0 0 15px 1px rgba(0, 0, 0, 0.2);box-shadow:0 0 15px 1px rgba(0, 0, 0, 0.2)}.qti-test-scope [data-control=qti-comment] textarea{display:block;height:100px;resize:none;width:350px;padding:3px;margin:0 0 10px 0;border:none;font-size:13px;font-size:1.3rem;border:1px solid #ddd;border-radius:2px;-webkit-border-radius:2px}.qti-test-scope #qti-timers{display:none}.qti-test-scope [data-control=exit]{margin-left:20px}.qti-test-scope [data-control=comment-toggle]{display:none}.qti-test-scope .qti-timer{display:inline-block;text-align:center;vertical-align:top;line-height:1.2;position:relative;padding:0 20px}.qti-test-scope .qti-timer .qti-timer_label{max-width:130px;font-size:12px;font-size:1.2rem}.qti-test-scope .qti-timer::before{content:" ";background:rgba(255,255,255,.3);width:1px;height:20px;position:absolute;left:0;top:5px}.qti-test-scope .qti-timer:first-child::before{content:none}.qti-test-scope.non-lti-context .title-box{display:none}.qti-test-scope #qti-rubrics{margin:auto;max-width:1024px;width:100%;padding:15px}.qti-test-scope #qti-rubrics .qti-rubricBlock{margin:20px 0}.qti-test-scope #qti-rubrics .hidden{display:none} /*# sourceMappingURL=test-runner.css.map */ \ No newline at end of file