Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
5.10% |
32 / 628 |
|
1.56% |
1 / 64 |
CRAP | |
0.00% |
0 / 1 |
taoQtiTest_models_classes_QtiTestService | |
5.10% |
32 / 628 |
|
1.56% |
1 / 64 |
27875.22 | |
0.00% |
0 / 1 |
enableMetadataGuardians | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
disableMetadataGuardians | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
enableMetadataValidators | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
disableMetadataValidators | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
enableItemMustExist | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
disableItemMustExist | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
enableItemMustBeOverwritten | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
disableItemMustBeOverwritten | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getJsonTest | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
checkMissingClassProperties | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
setDefaultModel | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
saveJsonTest | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
fromJson | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getItems | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setItems | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
save | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
getIdentifierFor | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
isIdentifierUnique | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
generateIdentifier | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
importMultipleTests | |
0.00% |
0 / 74 |
|
0.00% |
0 / 1 |
272 | |||
clearRelatedResources | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
72 | |||
importTest | |
0.00% |
0 / 221 |
|
0.00% |
0 / 1 |
1406 | |||
deleteTestsFromClassByLabel | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
importTestDefinition | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
setQtiIndexFile | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getQtiDefinitionPath | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
importTestAuxiliaryFiles | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
getTestFile | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
getDoc | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getDocPath | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getDocItems | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
setItemsToDoc | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
30 | |||
getQtiTestDir | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
searchInTestDirectory | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
getQtiTestFile | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getRelTestPath | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
saveDoc | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
createContent | |
70.00% |
21 / 30 |
|
0.00% |
0 / 1 |
8.32 | |||
deleteContent | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
setQtiTestFileSystem | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getDefaultDir | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
setQtiTestAcceptableLatency | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getQtiTestAcceptableLatency | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
2.02 | |||
getQtiTestTemplateFileAsString | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
getMetadataImporter | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getMetaMetadataExtractor | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSecureResourceService | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
verifyItemPermissions | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
72 | |||
deleteItemSubclassesByLabel | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
getQtiPackageImportPreprocessing | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getItemTreeService | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTestService | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getPsrContainer | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getMetaMetadataImporter | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getMappedProperties | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
20 | |||
getIdentifierGenerator | |
33.33% |
1 / 3 |
|
0.00% |
0 / 1 |
3.19 | |||
createTestIdentifier | |
50.00% |
4 / 8 |
|
0.00% |
0 / 1 |
4.12 | |||
getTestLabel | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
syncUniqueId | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
getManifestConverter | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTestConverter | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSectionConverter | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
convertAssessmentSectionRefs | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
getFeatureFlagChecker | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | /** |
4 | * This program is free software; you can redistribute it and/or |
5 | * modify it under the terms of the GNU General Public License |
6 | * as published by the Free Software Foundation; under version 2 |
7 | * of the License (non-upgradable). |
8 | * |
9 | * This program is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | * GNU General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU General Public License |
15 | * along with this program; if not, write to the Free Software |
16 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
17 | * |
18 | * Copyright (c) 2013-2024 (original work) Open Assessment Technologies SA; |
19 | */ |
20 | |
21 | use oat\generis\model\data\event\ResourceCreated; |
22 | use oat\oatbox\filesystem\Directory; |
23 | use oat\oatbox\filesystem\File; |
24 | use oat\oatbox\filesystem\FileSystemService; |
25 | use oat\oatbox\reporting\Report; |
26 | use oat\tao\model\featureFlag\FeatureFlagChecker; |
27 | use oat\tao\model\featureFlag\FeatureFlagCheckerInterface; |
28 | use oat\tao\model\IdentifierGenerator\Generator\IdentifierGeneratorInterface; |
29 | use oat\tao\model\IdentifierGenerator\Generator\IdentifierGeneratorProxy; |
30 | use oat\tao\model\resources\ResourceAccessDeniedException; |
31 | use oat\tao\model\resources\SecureResourceServiceInterface; |
32 | use oat\tao\model\TaoOntology; |
33 | use oat\taoItems\model\Command\DeleteItemCommand; |
34 | use oat\taoQtiItem\model\qti\converter\ManifestConverter; |
35 | use oat\taoQtiItem\model\qti\ImportService; |
36 | use oat\taoQtiItem\model\qti\metadata\importer\MetadataImporter; |
37 | use oat\taoQtiItem\model\qti\metadata\importer\MetaMetadataImportMapper; |
38 | use oat\taoQtiItem\model\qti\metadata\importer\PropertyDoesNotExistException; |
39 | use oat\taoQtiItem\model\qti\metadata\imsManifest\MetaMetadataExtractor; |
40 | use oat\taoQtiItem\model\qti\metadata\MetadataGuardianResource; |
41 | use oat\taoQtiItem\model\qti\metadata\MetadataService; |
42 | use oat\taoQtiItem\model\qti\metadata\ontology\MappedMetadataInjector; |
43 | use oat\taoQtiItem\model\qti\PackageParser; |
44 | use oat\taoQtiItem\model\qti\Resource; |
45 | use oat\taoQtiItem\model\qti\Service; |
46 | use oat\taoQtiTest\models\cat\AdaptiveSectionInjectionException; |
47 | use oat\taoQtiTest\models\cat\CatEngineNotFoundException; |
48 | use oat\taoQtiTest\models\cat\CatService; |
49 | use oat\taoQtiTest\models\classes\event\TestImportedEvent; |
50 | use oat\taoQtiTest\models\metadata\MetadataTestContextAware; |
51 | use oat\taoQtiTest\models\Qti\Converter\AssessmentSectionConverter; |
52 | use oat\taoQtiTest\models\Qti\Converter\TestConverter; |
53 | use oat\taoQtiTest\models\render\QtiPackageImportPreprocessing; |
54 | use oat\taoQtiTest\models\test\AssessmentTestXmlFactory; |
55 | use oat\taoTests\models\event\TestUpdatedEvent; |
56 | use Psr\Container\ContainerInterface; |
57 | use qtism\common\utils\Format; |
58 | use qtism\data\AssessmentItemRef; |
59 | use qtism\data\AssessmentSectionRef; |
60 | use qtism\data\QtiComponentCollection; |
61 | use qtism\data\SectionPartCollection; |
62 | use qtism\data\storage\StorageException; |
63 | use qtism\data\storage\xml\marshalling\UnmarshallingException; |
64 | use qtism\data\storage\xml\XmlDocument; |
65 | use qtism\data\storage\xml\XmlStorageException; |
66 | use taoQtiTest_models_classes_import_TestImportForm as TestImportForm; |
67 | use taoTests_models_classes_TestsService as TestService; |
68 | |
69 | /** |
70 | * the QTI TestModel service. |
71 | * |
72 | * @author Joel Bout <joel@taotesting.com> |
73 | * @author Bertrand Chevrier <bertrand@taotesting.com> |
74 | * @author Jerome Bogaerts <jerome@taotesting.com> |
75 | * @package taoQtiTest |
76 | */ |
77 | class taoQtiTest_models_classes_QtiTestService extends TestService |
78 | { |
79 | public const CONFIG_QTITEST_FILESYSTEM = 'qtiTestFolder'; |
80 | |
81 | public const CONFIG_QTITEST_ACCEPTABLE_LATENCY = 'qtiAcceptableLatency'; |
82 | |
83 | public const QTI_TEST_DEFINITION_INDEX = '.index/qti-test.txt'; |
84 | |
85 | public const PROPERTY_QTI_TEST_IDENTIFIER = 'http://www.tao.lu/Ontologies/TAOTest.rdf#QtiTestIdentifier'; |
86 | |
87 | public const INSTANCE_TEST_MODEL_QTI = 'http://www.tao.lu/Ontologies/TAOTest.rdf#QtiTestModel'; |
88 | |
89 | public const TAOQTITEST_FILENAME = 'tao-qtitest-testdefinition.xml'; |
90 | |
91 | public const METADATA_GUARDIAN_CONTEXT_NAME = 'tao-qtitest'; |
92 | |
93 | // phpcs:disable Generic.Files.LineLength |
94 | public const INSTANCE_FORMAL_PARAM_TEST_DEFINITION = 'http://www.tao.lu/Ontologies/TAOTest.rdf#FormalParamQtiTestDefinition'; |
95 | public const INSTANCE_FORMAL_PARAM_TEST_COMPILATION = 'http://www.tao.lu/Ontologies/TAOTest.rdf#FormalParamQtiTestCompilation'; |
96 | // phpcs:enable Generic.Files.LineLength |
97 | |
98 | public const TEST_COMPILED_FILENAME = 'compact-test'; |
99 | public const TEST_COMPILED_META_FILENAME = 'test-meta'; |
100 | public const TEST_COMPILED_METADATA_FILENAME = 'test-metadata.json'; |
101 | public const TEST_COMPILED_INDEX = 'test-index.json'; |
102 | public const TEST_COMPILED_HREF_INDEX_FILE_PREFIX = 'assessment-item-ref-href-index-'; |
103 | public const TEST_COMPILED_HREF_INDEX_FILE_EXTENSION = '.idx'; |
104 | |
105 | public const TEST_REMOTE_FOLDER = 'tao-qtitest-remote'; |
106 | public const TEST_RENDERING_STATE_NAME = 'taoQtiTestState'; |
107 | public const TEST_BASE_PATH_NAME = 'taoQtiBasePath'; |
108 | public const TEST_PLACEHOLDER_BASE_URI = 'tao://qti-directory'; |
109 | public const TEST_VIEWS_NAME = 'taoQtiViews'; |
110 | |
111 | public const XML_TEST_PART = 'testPart'; |
112 | public const XML_ASSESSMENT_SECTION = 'assessmentSection'; |
113 | public const XML_ASSESSMENT_ITEM_REF = 'assessmentItemRef'; |
114 | |
115 | private const IN_PROGRESS_LABEL = 'in progress'; |
116 | |
117 | /** |
118 | * @var MetadataImporter Service to manage Lom metadata during package import |
119 | */ |
120 | protected $metadataImporter; |
121 | |
122 | /** |
123 | * @var bool If true, it will guard and check metadata that comes from package. |
124 | */ |
125 | protected $useMetadataGuardians = true; |
126 | |
127 | /** |
128 | * @var bool If true, items contained in the test must be all found by one metadata guardian. |
129 | */ |
130 | protected $itemMustExist = false; |
131 | |
132 | /** |
133 | * @var bool If true, items found by metadata guardians will be overwritten. |
134 | */ |
135 | protected $itemMustBeOverwritten = false; |
136 | |
137 | /** |
138 | * @var bool If true, registered validators will be invoked for each test item to be imported. |
139 | */ |
140 | protected $useMetadataValidators = true; |
141 | |
142 | public function enableMetadataGuardians() |
143 | { |
144 | $this->useMetadataGuardians = true; |
145 | } |
146 | |
147 | public function disableMetadataGuardians() |
148 | { |
149 | $this->useMetadataGuardians = false; |
150 | } |
151 | |
152 | public function enableMetadataValidators() |
153 | { |
154 | $this->useMetadataValidators = true; |
155 | } |
156 | |
157 | public function disableMetadataValidators() |
158 | { |
159 | $this->useMetadataValidators = false; |
160 | } |
161 | |
162 | public function enableItemMustExist() |
163 | { |
164 | $this->itemMustExist = true; |
165 | } |
166 | |
167 | public function disableItemMustExist() |
168 | { |
169 | $this->itemMustExist = false; |
170 | } |
171 | |
172 | public function enableItemMustBeOverwritten() |
173 | { |
174 | $this->itemMustBeOverwritten = true; |
175 | } |
176 | |
177 | public function disableItemMustBeOverwritten() |
178 | { |
179 | $this->itemMustBeOverwritten = false; |
180 | } |
181 | |
182 | /** |
183 | * Get the QTI Test document formated in JSON. |
184 | * |
185 | * @param core_kernel_classes_Resource $test |
186 | * |
187 | * @return string the json |
188 | * |
189 | * @throws taoQtiTest_models_classes_QtiTestServiceException |
190 | */ |
191 | public function getJsonTest(core_kernel_classes_Resource $test): string |
192 | { |
193 | $doc = $this->getDoc($test); |
194 | $converter = new taoQtiTest_models_classes_QtiTestConverter($doc); |
195 | |
196 | return $converter->toJson(); |
197 | } |
198 | |
199 | /** |
200 | * Checks if target class has all the properties needed to import the metadata. |
201 | * @param array $metadataValues |
202 | * @param array $mappedMetadataValues |
203 | * @return array |
204 | */ |
205 | private function checkMissingClassProperties(array $metadataValues, array $mappedMetadataValues): array |
206 | { |
207 | $metadataValueUris = $this->getMetadataImporter()->metadataValueUris($metadataValues); |
208 | return array_diff( |
209 | $metadataValueUris, |
210 | array_keys(array_merge($mappedMetadataValues['testProperties'], $mappedMetadataValues['itemProperties'])) |
211 | ); |
212 | } |
213 | |
214 | /** |
215 | * @inheritDoc |
216 | */ |
217 | protected function setDefaultModel($test): void |
218 | { |
219 | $this->setTestModel($test, $this->getResource(self::INSTANCE_TEST_MODEL_QTI)); |
220 | } |
221 | |
222 | /** |
223 | * Save the json formated test into the test resource. |
224 | * |
225 | * @param core_kernel_classes_Resource $test |
226 | * @param string $json |
227 | * |
228 | * @return bool true if saved |
229 | * |
230 | * @throws taoQtiTest_models_classes_QtiTestServiceException |
231 | * @throws taoQtiTest_models_classes_QtiTestConverterException |
232 | */ |
233 | public function saveJsonTest(core_kernel_classes_Resource $test, $json): bool |
234 | { |
235 | $saved = false; |
236 | |
237 | if (!empty($json)) { |
238 | $this->verifyItemPermissions($test, $json); |
239 | $this->syncUniqueId($test, $json); |
240 | |
241 | $doc = $this->getDoc($test); |
242 | |
243 | $converter = new taoQtiTest_models_classes_QtiTestConverter($doc); |
244 | $converter->fromJson($json); |
245 | |
246 | $saved = $this->saveDoc($test, $doc); |
247 | |
248 | # Update the test's updated_at property |
249 | $now = microtime(true); |
250 | $property = $this->getProperty(TaoOntology::PROPERTY_UPDATED_AT); |
251 | $test->editPropertyValues($property, $now); |
252 | |
253 | $this->getEventManager()->trigger(new TestUpdatedEvent($test->getUri())); |
254 | } |
255 | return $saved; |
256 | } |
257 | |
258 | public function fromJson($json) |
259 | { |
260 | $doc = new XmlDocument('2.2'); |
261 | $converter = new taoQtiTest_models_classes_QtiTestConverter($doc); |
262 | $converter->fromJson($json); |
263 | return $doc; |
264 | } |
265 | |
266 | /** |
267 | * Get the items that are part of a given $test. |
268 | * |
269 | * @param core_kernel_classes_Resource $test A Resource describing a QTI Assessment Test. |
270 | * @return array An array of core_kernel_classes_Resource objects. The array is associative. Its keys are actually |
271 | * the assessmentItemRef identifiers. |
272 | */ |
273 | public function getItems(core_kernel_classes_Resource $test) |
274 | { |
275 | return $this->getDocItems($this->getDoc($test)); |
276 | } |
277 | |
278 | /** |
279 | * Assign items to a test and save it. |
280 | * @param core_kernel_classes_Resource $test |
281 | * @param array $items |
282 | * @return boolean true if set |
283 | * @throws taoQtiTest_models_classes_QtiTestServiceException |
284 | */ |
285 | public function setItems(core_kernel_classes_Resource $test, array $items) |
286 | { |
287 | $doc = $this->getDoc($test); |
288 | $bound = $this->setItemsToDoc($doc, $items); |
289 | |
290 | if ($this->saveDoc($test, $doc)) { |
291 | return $bound == count($items); |
292 | } |
293 | |
294 | return false; |
295 | } |
296 | |
297 | /** |
298 | * Save the QTI test : set the items sequence and some options. |
299 | * |
300 | * @param core_kernel_classes_Resource $test A Resource describing a QTI Assessment Test. |
301 | * @param array $items the items sequence |
302 | * @param array $options the test's options |
303 | * @return boolean if nothing goes wrong |
304 | * @throws StorageException If an error occurs while serializing/unserializing QTI-XML content. |
305 | */ |
306 | public function save(core_kernel_classes_Resource $test, array $items) |
307 | { |
308 | try { |
309 | $doc = $this->getDoc($test); |
310 | $this->setItemsToDoc($doc, $items); |
311 | $saved = $this->saveDoc($test, $doc); |
312 | } catch (StorageException $e) { |
313 | throw new taoQtiTest_models_classes_QtiTestServiceException( |
314 | "An error occured while dealing with the QTI-XML test: " . $e->getMessage(), |
315 | taoQtiTest_models_classes_QtiTestServiceException::TEST_WRITE_ERROR |
316 | ); |
317 | } |
318 | |
319 | return $saved; |
320 | } |
321 | |
322 | /** |
323 | * Get an identifier for a component of $qtiType. |
324 | * This identifier must be unique across the whole document. |
325 | * |
326 | * @param XmlDocument $doc |
327 | * @param string $qtiType the type name |
328 | * @return string the identifier |
329 | */ |
330 | public function getIdentifierFor(XmlDocument $doc, $qtiType) |
331 | { |
332 | $components = $doc->getDocumentComponent()->getIdentifiableComponents(); |
333 | $index = 1; |
334 | do { |
335 | $identifier = $this->generateIdentifier($doc, $qtiType, $index); |
336 | $index++; |
337 | } while (!$this->isIdentifierUnique($components, $identifier)); |
338 | |
339 | return $identifier; |
340 | } |
341 | |
342 | /** |
343 | * Check whether an identifier is unique against a list of components |
344 | * |
345 | * @param QtiComponentCollection $components |
346 | * @param string $identifier |
347 | * @return boolean |
348 | */ |
349 | private function isIdentifierUnique(QtiComponentCollection $components, $identifier) |
350 | { |
351 | foreach ($components as $component) { |
352 | if ($component->getIdentifier() == $identifier) { |
353 | return false; |
354 | } |
355 | } |
356 | return true; |
357 | } |
358 | |
359 | /** |
360 | * Generate an identifier from a qti type, using the syntax "qtitype-index" |
361 | * |
362 | * @param XmlDocument $doc |
363 | * @param string $qtiType |
364 | * @param int $offset |
365 | * @return string the identifier |
366 | */ |
367 | private function generateIdentifier(XmlDocument $doc, $qtiType, $offset = 1) |
368 | { |
369 | $typeList = $doc->getDocumentComponent()->getComponentsByClassName($qtiType); |
370 | return $qtiType . '-' . (count($typeList) + $offset); |
371 | } |
372 | |
373 | /** |
374 | * Import a QTI Test Package containing one or more QTI Test definitions. |
375 | * |
376 | * @param core_kernel_classes_Class $targetClass The Target RDFS class where you want the Test Resources to be |
377 | * created. |
378 | * @param string|File $file The path to the IMS archive you want to import tests from. |
379 | * @return common_report_Report An import report. |
380 | * @throws common_exception |
381 | * @throws common_exception_Error |
382 | * @throws common_exception_FileSystemError |
383 | */ |
384 | public function importMultipleTests( |
385 | core_kernel_classes_Class $targetClass, |
386 | $file, |
387 | bool $overwriteTest = false, |
388 | ?string $itemClassUri = null, |
389 | array $form = [], |
390 | ?string $overwriteTestUri = null, |
391 | ?string $packageLabel = null |
392 | ) { |
393 | $testClass = $targetClass; |
394 | $report = new Report(Report::TYPE_INFO); |
395 | $validPackage = false; |
396 | $validManifest = false; |
397 | $testsFound = false; |
398 | |
399 | $qtiPackageImportPreprocessingService = $this->getQtiPackageImportPreprocessing(); |
400 | $preprocessingReport = $qtiPackageImportPreprocessingService->run($file); |
401 | |
402 | if ($preprocessingReport) { |
403 | $report->add($preprocessingReport); |
404 | } |
405 | |
406 | // Validate the given IMS Package itself (ZIP integrity, presence of an 'imsmanifest.xml' file. |
407 | // phpcs:disable Generic.Files.LineLength |
408 | $invalidArchiveMsg = __("The provided archive is invalid. Make sure it is not corrupted and that it contains an 'imsmanifest.xml' file."); |
409 | // phpcs:enable Generic.Files.LineLength |
410 | |
411 | try { |
412 | $qtiPackageParser = new PackageParser($file); |
413 | $qtiPackageParser->validate(); |
414 | $validPackage = true; |
415 | } catch (Exception $e) { |
416 | $report->add(Report::createError($invalidArchiveMsg)); |
417 | } |
418 | |
419 | // Validate the manifest (well formed XML, valid against the schema). |
420 | if ($validPackage === true) { |
421 | $folder = $qtiPackageParser->extract(); |
422 | if (is_dir($folder) === false) { |
423 | $report->add(Report::createError($invalidArchiveMsg)); |
424 | } else { |
425 | $file = $folder . 'imsmanifest.xml'; |
426 | $qtiManifestParser = new taoQtiTest_models_classes_ManifestParser($file); |
427 | $this->propagate($qtiManifestParser); |
428 | // For taoSetup PsrContainer is not available |
429 | // It is not required to perform manifest conversion in this process |
430 | // therefore we can skip it during taoSetup |
431 | if ($this->getPsrContainer()->has(ManifestConverter::class)) { |
432 | $this->getManifestConverter()->convertToQti2($file, $qtiManifestParser); |
433 | } |
434 | // We validate manifest file against QTI 3.0 |
435 | $qtiManifestParser->validate(); |
436 | if ($qtiManifestParser->isValid() === true) { |
437 | $validManifest = true; |
438 | |
439 | $tests = []; |
440 | foreach (Resource::getTestTypes() as $type) { |
441 | $tests = array_merge($tests, $qtiManifestParser->getResources($type)); |
442 | } |
443 | |
444 | $testsFound = (count($tests) !== 0); |
445 | |
446 | if ($testsFound !== true) { |
447 | $report->add( |
448 | Report::createError( |
449 | // phpcs:disable Generic.Files.LineLength |
450 | __("Package is valid but no tests were found. Make sure that it contains valid QTI tests.") |
451 | // phpcs:enable Generic.Files.LineLength |
452 | ) |
453 | ); |
454 | } else { |
455 | $alreadyImportedQtiResources = []; |
456 | |
457 | foreach ($tests as $qtiTestResource) { |
458 | $importTestReport = $this->importTest( |
459 | $testClass, |
460 | $qtiTestResource, |
461 | $qtiManifestParser, |
462 | $folder, |
463 | $alreadyImportedQtiResources, |
464 | $overwriteTest, |
465 | $itemClassUri, |
466 | !empty($form[TestImportForm::METADATA_FORM_ELEMENT_NAME]) ?? false, |
467 | $overwriteTestUri, |
468 | $packageLabel |
469 | ); |
470 | $report->add($importTestReport); |
471 | |
472 | if ($data = $importTestReport->getData()) { |
473 | $alreadyImportedQtiResources = array_unique( |
474 | array_merge( |
475 | $alreadyImportedQtiResources, |
476 | $data->itemQtiResources |
477 | ) |
478 | ); |
479 | } |
480 | } |
481 | } |
482 | } else { |
483 | $msg = __("The 'imsmanifest.xml' file found in the archive is not valid."); |
484 | $report->add(Report::createError($msg)); |
485 | } |
486 | |
487 | // Cleanup the folder where the archive was extracted. |
488 | tao_helpers_File::deltree($folder); |
489 | } |
490 | } |
491 | |
492 | if ($report->containsError() === true) { |
493 | $report->setMessage(__('The IMS QTI Test Package could not be imported.')); |
494 | $report->setType(Report::TYPE_ERROR); |
495 | } else { |
496 | $report->setMessage(__('IMS QTI Test Package successfully imported.')); |
497 | $report->setType(Report::TYPE_SUCCESS); |
498 | } |
499 | |
500 | if ( |
501 | $report->containsError() === true |
502 | && $validPackage === true |
503 | && $validManifest === true |
504 | && $testsFound === true |
505 | ) { |
506 | $this->clearRelatedResources($report); |
507 | } |
508 | |
509 | return $report; |
510 | } |
511 | |
512 | /** |
513 | * @param common_report_Report $report |
514 | * |
515 | * @throws common_exception_Error |
516 | * @throws common_exception_FileSystemError |
517 | */ |
518 | public function clearRelatedResources(common_report_Report $report): void |
519 | { |
520 | // We consider a test package as an atomic component, we then rollback it. |
521 | $itemService = $this->getServiceLocator()->get(taoItems_models_classes_ItemsService::class); |
522 | |
523 | foreach ($report as $r) { |
524 | $data = $r->getData(); |
525 | |
526 | // -- Rollback all items. |
527 | // 1. Simply delete items that were not involved in overwriting. |
528 | foreach ($data->newItems as $item) { |
529 | if ( |
530 | !$item instanceof MetadataGuardianResource |
531 | && !array_key_exists($item->getUri(), $data->overwrittenItems) |
532 | ) { |
533 | common_Logger::d("Rollbacking new item '" . $item->getUri() . "'..."); |
534 | @$itemService->deleteResource($item); |
535 | } |
536 | } |
537 | |
538 | // 2. Restore overwritten item contents. |
539 | foreach ($data->overwrittenItems as $overwrittenItemId => $backupName) { |
540 | common_Logger::d("Restoring content for item '{$overwrittenItemId}'..."); |
541 | @Service::singleton()->restoreContentByRdfItem( |
542 | new core_kernel_classes_Resource($overwrittenItemId), |
543 | $backupName |
544 | ); |
545 | } |
546 | |
547 | // Delete all created classes (by registered class lookups). |
548 | foreach ($data->createdClasses as $createdClass) { |
549 | @$createdClass->delete(); |
550 | } |
551 | |
552 | // Delete the target Item RDFS class. |
553 | common_Logger::t("Rollbacking Items target RDFS class '" . $data->itemClass->getLabel() . "'..."); |
554 | @$data->itemClass->delete(); |
555 | |
556 | // Delete test definition. |
557 | common_Logger::t("Rollbacking test '" . $data->rdfsResource->getLabel() . "..."); |
558 | @$this->deleteTest($data->rdfsResource); |
559 | |
560 | if (count($data->newItems) > 0) { |
561 | // phpcs:disable Generic.Files.LineLength |
562 | $msg = __("The resources related to the IMS QTI Test referenced as \"%s\" in the IMS Manifest file were rolled back.", $data->manifestResource->getIdentifier()); |
563 | // phpcs:enable Generic.Files.LineLength |
564 | $report->add(new common_report_Report(common_report_Report::TYPE_WARNING, $msg)); |
565 | } |
566 | } |
567 | } |
568 | |
569 | /** |
570 | * Import a QTI Test and its dependent Items into the TAO Platform. |
571 | * |
572 | * @param core_kernel_classes_Class $testClass The RDFS Class where Ontology resources must be created. |
573 | * @param oat\taoQtiItem\model\qti\Resource $qtiTestResource The QTI Test Resource representing the IMS QTI Test to |
574 | * be imported. |
575 | * @param taoQtiTest_models_classes_ManifestParser $manifestParser The parser used to retrieve the IMS Manifest. |
576 | * @param string $folder The absolute path to the folder where the IMS archive containing the test content |
577 | * @param oat\taoQtiItem\model\qti\Resource[] $ignoreQtiResources An array of QTI Manifest Resources to be ignored |
578 | * at import time. |
579 | * @return common_report_Report A report about how the importation behaved. |
580 | */ |
581 | protected function importTest( |
582 | core_kernel_classes_Class $testClass, |
583 | Resource $qtiTestResource, |
584 | taoQtiTest_models_classes_ManifestParser $manifestParser, |
585 | $folder, |
586 | array $ignoreQtiResources = [], |
587 | bool $overwriteTest = false, |
588 | ?string $itemClassUri = null, |
589 | bool $importMetadata = false, |
590 | ?string $overwriteTestUri = null, |
591 | ?string $packageLabel = null |
592 | ) { |
593 | /** @var ImportService $itemImportService */ |
594 | $itemImportService = $this->getServiceLocator()->get(ImportService::SERVICE_ID); |
595 | $qtiTestResourceIdentifier = $qtiTestResource->getIdentifier(); |
596 | |
597 | // The class where the items that belong to the test will be imported. |
598 | $itemParentClass = $this->getClass($itemClassUri ?: TaoOntology::CLASS_URI_ITEM); |
599 | |
600 | // Create an RDFS resource in the knowledge base that will hold |
601 | // the information about the imported QTI Test. |
602 | |
603 | $testResource = $this->createInstance($testClass, self::IN_PROGRESS_LABEL); |
604 | $qtiTestModelResource = $this->getResource(self::INSTANCE_TEST_MODEL_QTI); |
605 | $modelProperty = $this->getProperty(TestService::PROPERTY_TEST_TESTMODEL); |
606 | $testResource->editPropertyValues($modelProperty, $qtiTestModelResource); |
607 | |
608 | // Setting qtiIdentifier property |
609 | $qtiIdentifierProperty = $this->getProperty(self::PROPERTY_QTI_TEST_IDENTIFIER); |
610 | $testResource->editPropertyValues($qtiIdentifierProperty, $qtiTestResourceIdentifier); |
611 | |
612 | // Create the report that will hold information about the import |
613 | // of $qtiTestResource in TAO. |
614 | $report = new Report(Report::TYPE_INFO); |
615 | |
616 | // Load and validate the manifest |
617 | $qtiManifestParser = new taoQtiTest_models_classes_ManifestParser($folder . 'imsmanifest.xml'); |
618 | $this->propagate($qtiManifestParser); |
619 | $qtiManifestParser->validate(); |
620 | |
621 | $domManifest = new DOMDocument('1.0', 'UTF-8'); |
622 | $domManifest->load($folder . 'imsmanifest.xml'); |
623 | |
624 | $metadataValues = $this->getMetadataImporter()->extract($domManifest); |
625 | |
626 | // Note: without this fix, metadata guardians do not work. |
627 | $this->getMetadataImporter()->setMetadataValues($metadataValues); |
628 | |
629 | // Set up $report with useful information for client code (especially for rollback). |
630 | $reportCtx = new stdClass(); |
631 | $reportCtx->manifestResource = $qtiTestResource; |
632 | $reportCtx->rdfsResource = $testResource; |
633 | $reportCtx->items = []; |
634 | $reportCtx->newItems = []; |
635 | $reportCtx->overwrittenItems = []; |
636 | $reportCtx->itemQtiResources = []; |
637 | $reportCtx->testMetadata = $metadataValues[$qtiTestResourceIdentifier] ?? []; |
638 | $reportCtx->createdClasses = []; |
639 | |
640 | // 'uriResource' key is needed by javascript in tao/views/templates/form/import.tpl |
641 | $reportCtx->uriResource = $testResource->getUri(); |
642 | |
643 | // Expected test.xml file location. |
644 | $expectedTestFile = $folder . str_replace('/', DIRECTORY_SEPARATOR, $qtiTestResource->getFile()); |
645 | |
646 | // Already imported test items (qti xml file paths). |
647 | $alreadyImportedTestItemFiles = []; |
648 | |
649 | // -- Check if the file referenced by the test QTI resource exists. |
650 | if (is_readable($expectedTestFile) === false) { |
651 | $report->add( |
652 | Report::createError( |
653 | __('No file found at location "%s".', $qtiTestResource->getFile()) |
654 | ) |
655 | ); |
656 | } else { |
657 | //Convert to QTI 2.2 |
658 | if ($this->getPsrContainer()->has(TestConverter::class)) { |
659 | $this->getTestConverter()->convertToQti2($expectedTestFile); |
660 | } |
661 | // -- Load the test in a QTISM flavour. |
662 | $testDefinition = new XmlDocument(); |
663 | |
664 | try { |
665 | $testDefinition->load($expectedTestFile, true); |
666 | $this->convertAssessmentSectionRefs( |
667 | $testDefinition->getDocumentComponent() |
668 | ->getComponentsByClassName('assessmentSectionRef'), |
669 | $folder |
670 | ); |
671 | // If any, assessmentSectionRefs will be resolved and included as part of the main test definition. |
672 | $testDefinition->includeAssessmentSectionRefs(true); |
673 | $testLabel = $packageLabel ?? $this->getTestLabel($reportCtx->testMetadata, $testDefinition); |
674 | |
675 | if ($overwriteTestUri || $overwriteTest) { |
676 | $itemsClassLabel = $testLabel; |
677 | |
678 | /** @var oat\taoQtiItem\model\qti\metadata\simple\SimpleMetadataValue $singleMetadata */ |
679 | foreach ($reportCtx->testMetadata as $singleMetadata) { |
680 | if (($singleMetadata->getPath()[1] ?? '') === RDFS_LABEL) { |
681 | $testLabel = $singleMetadata->getValue(); |
682 | } |
683 | } |
684 | |
685 | $this->deleteItemSubclassesByLabel($itemParentClass, $itemsClassLabel); |
686 | $overwriteTestUri |
687 | ? $this->getTestService()->deleteTest(new core_kernel_classes_Resource($overwriteTestUri)) |
688 | : $this->deleteTestsFromClassByLabel($testLabel, $testClass); |
689 | } |
690 | |
691 | $targetItemClass = $itemParentClass->createSubClass(self::IN_PROGRESS_LABEL); |
692 | |
693 | // add real label without saving (to not pass it separately to item importer) |
694 | $targetItemClass->label = $testLabel; |
695 | |
696 | $reportCtx->itemClass = $targetItemClass; |
697 | |
698 | $mappedProperties = $this->getMappedProperties( |
699 | $importMetadata, |
700 | $domManifest, |
701 | $reportCtx, |
702 | $testClass, |
703 | $targetItemClass |
704 | ); |
705 | |
706 | // -- Load all items related to test. |
707 | $itemError = false; |
708 | |
709 | // discover test's base path. |
710 | $dependencies = taoQtiTest_helpers_Utils::buildAssessmentItemRefsTestMap( |
711 | $testDefinition, |
712 | $manifestParser, |
713 | $folder |
714 | ); |
715 | |
716 | // Build a DOM version of the fully resolved AssessmentTest for later usage. |
717 | $transitionalDoc = new DOMDocument('1.0', 'UTF-8'); |
718 | $transitionalDoc->loadXML($testDefinition->saveToString()); |
719 | |
720 | /** @var CatService $service */ |
721 | $service = $this->getServiceLocator()->get(CatService::SERVICE_ID); |
722 | $service->importCatSectionIdsToRdfTest( |
723 | $testResource, |
724 | $testDefinition->getDocumentComponent(), |
725 | $expectedTestFile |
726 | ); |
727 | |
728 | if (count($dependencies['items']) > 0) { |
729 | // Stores shared files across multiple items to avoid duplicates. |
730 | $sharedFiles = []; |
731 | |
732 | foreach ($dependencies['items'] as $assessmentItemRefId => $qtiDependency) { |
733 | if ($qtiDependency !== false) { |
734 | if (Resource::isAssessmentItem($qtiDependency->getType())) { |
735 | $resourceIdentifier = $qtiDependency->getIdentifier(); |
736 | |
737 | if (!array_key_exists($resourceIdentifier, $ignoreQtiResources)) { |
738 | $qtiFile = $folder |
739 | . str_replace('/', DIRECTORY_SEPARATOR, $qtiDependency->getFile()); |
740 | |
741 | // If metadata should be aware of the test context... |
742 | foreach ($this->getMetadataImporter()->getExtractors() as $extractor) { |
743 | if ($extractor instanceof MetadataTestContextAware) { |
744 | $metadataValues = array_merge( |
745 | $metadataValues, |
746 | $extractor->contextualizeWithTest( |
747 | $qtiTestResource->getIdentifier(), |
748 | $transitionalDoc, |
749 | $resourceIdentifier, |
750 | $metadataValues |
751 | ) |
752 | ); |
753 | } |
754 | } |
755 | // Skip if $qtiFile already imported (multiple assessmentItemRef "hrefing" the same |
756 | // file). |
757 | if (array_key_exists($qtiFile, $alreadyImportedTestItemFiles) === false) { |
758 | $createdClasses = []; |
759 | |
760 | $itemReport = $itemImportService->importQtiItem( |
761 | $folder, |
762 | $qtiDependency, |
763 | $targetItemClass, |
764 | $sharedFiles, |
765 | $dependencies['dependencies'], |
766 | $metadataValues, |
767 | $createdClasses, |
768 | $this->useMetadataGuardians, |
769 | $this->useMetadataValidators, |
770 | $this->itemMustExist, |
771 | $this->itemMustBeOverwritten, |
772 | $reportCtx->overwrittenItems, |
773 | $mappedProperties['itemProperties'] ?? [], |
774 | $importMetadata |
775 | ); |
776 | |
777 | $reportCtx->createdClasses = array_merge( |
778 | $reportCtx->createdClasses, |
779 | $createdClasses |
780 | ); |
781 | |
782 | $rdfItem = $itemReport->getData(); |
783 | |
784 | if ($rdfItem) { |
785 | $reportCtx->items[$assessmentItemRefId] = $rdfItem; |
786 | $reportCtx->newItems[$assessmentItemRefId] = $rdfItem; |
787 | $reportCtx->itemQtiResources[$resourceIdentifier] = $rdfItem; |
788 | $alreadyImportedTestItemFiles[$qtiFile] = $rdfItem; |
789 | } else { |
790 | if (!$itemReport->getMessage()) { |
791 | $itemReport->setMessage( |
792 | // phpcs:disable Generic.Files.LineLength |
793 | __('IMS QTI Item referenced as "%s" in the IMS Manifest file could not be imported.', $resourceIdentifier) |
794 | // phpcs:enable Generic.Files.LineLength |
795 | ); |
796 | } |
797 | |
798 | $itemReport->setType(common_report_Report::TYPE_ERROR); |
799 | $itemError = ($itemError === false) ? true : $itemError; |
800 | } |
801 | |
802 | $report->add($itemReport); |
803 | } else { |
804 | // phpcs:disable Generic.Files.LineLength |
805 | $reportCtx->items[$assessmentItemRefId] = $alreadyImportedTestItemFiles[$qtiFile]; |
806 | // phpcs:enable Generic.Files.LineLength |
807 | } |
808 | } else { |
809 | // Ignored (possibily because imported in another test of the same package). |
810 | $reportCtx->items[$assessmentItemRefId] = $ignoreQtiResources[$resourceIdentifier]; |
811 | $report->add( |
812 | new common_report_Report( |
813 | common_report_Report::TYPE_SUCCESS, |
814 | // phpcs:disable Generic.Files.LineLength |
815 | __('IMS QTI Item referenced as "%s" in the IMS Manifest file successfully imported.', $resourceIdentifier) |
816 | // phpcs:enable Generic.Files.LineLength |
817 | ) |
818 | ); |
819 | } |
820 | } |
821 | } else { |
822 | // phpcs:disable Generic.Files.LineLength |
823 | $msg = __('The dependency to the IMS QTI AssessmentItemRef "%s" in the IMS Manifest file could not be resolved.', $assessmentItemRefId); |
824 | // phpcs:enable Generic.Files.LineLength |
825 | $report->add(common_report_Report::createFailure($msg)); |
826 | $itemError = ($itemError === false) ? true : $itemError; |
827 | } |
828 | } |
829 | |
830 | // If items did not produce errors, we import the test definition. |
831 | if ($itemError === false) { |
832 | common_Logger::i("Importing test with manifest identifier '{$qtiTestResourceIdentifier}'..."); |
833 | |
834 | // Second step is to take care of the test definition and the related media (auxiliary files). |
835 | |
836 | // 1. Import test definition (i.e. the QTI-XML Test file). |
837 | $testContent = $this->importTestDefinition( |
838 | $testResource, |
839 | $testDefinition, |
840 | $qtiTestResource, |
841 | $reportCtx->items, |
842 | $folder, |
843 | $report |
844 | ); |
845 | |
846 | if ($testContent !== false) { |
847 | // 2. Import test auxilliary files (e.g. stylesheets, images, ...). |
848 | $this->importTestAuxiliaryFiles($testContent, $qtiTestResource, $folder, $report); |
849 | |
850 | // 3. Give meaningful names to resources. |
851 | $testResource->setLabel($packageLabel ?? $testLabel); |
852 | $targetItemClass->setLabel($packageLabel ?? $testLabel); |
853 | |
854 | // 4. Import metadata for the resource (use same mechanics as item resources). |
855 | // Metadata will be set as property values. |
856 | //todo: fix taoSetup to be aware of containers. This is only workaround. |
857 | if ( |
858 | $this->getServiceManager()->getContainer()->has(MappedMetadataInjector::class) |
859 | && $importMetadata |
860 | ) { |
861 | $this->getServiceManager()->getContainer()->get(MappedMetadataInjector::class)->inject( |
862 | $mappedProperties['testProperties'] ?? [], |
863 | $metadataValues[$qtiTestResourceIdentifier] ?? [], |
864 | $testResource |
865 | ); |
866 | } |
867 | |
868 | |
869 | // 5. if $targetClass does not contain any instances |
870 | // (because everything resolved by class lookups), |
871 | // Just delete it. |
872 | if ($targetItemClass->countInstances() == 0) { |
873 | $targetItemClass->delete(); |
874 | } |
875 | } |
876 | } else { |
877 | $msg = __("One or more dependent IMS QTI Items could not be imported."); |
878 | $report->add(common_report_Report::createFailure($msg)); |
879 | } |
880 | } else { |
881 | // No depencies found (i.e. no item resources bound to the test). |
882 | $msg = __("No reference to any IMS QTI Item found."); |
883 | $report->add(common_report_Report::createFailure($msg)); |
884 | } |
885 | } catch (StorageException $e) { |
886 | // Source of the exception = $testDefinition->load() |
887 | // What is the reason ? |
888 | $eStrs = []; |
889 | |
890 | if (($libXmlErrors = $e->getErrors()) !== null) { |
891 | foreach ($libXmlErrors as $libXmlError) { |
892 | // phpcs:disable Generic.Files.LineLength |
893 | $eStrs[] = __('XML error at line %1$d column %2$d "%3$s".', $libXmlError->line, $libXmlError->column, trim($libXmlError->message)); |
894 | // phpcs:enable Generic.Files.LineLength |
895 | } |
896 | } |
897 | |
898 | $finalErrorString = implode("\n", $eStrs); |
899 | if (empty($finalErrorString) === true) { |
900 | common_Logger::e($e->getMessage()); |
901 | // Not XML malformation related. No info from LibXmlErrors extracted. |
902 | if (($previous = $e->getPrevious()) != null) { |
903 | // Useful information could be found here. |
904 | $finalErrorString = $previous->getMessage(); |
905 | |
906 | if ($previous instanceof UnmarshallingException) { |
907 | $domElement = $previous->getDOMElement(); |
908 | // phpcs:disable Generic.Files.LineLength |
909 | $finalErrorString = __('Inconsistency at line %1d:', $domElement->getLineNo()) . ' ' . $previous->getMessage(); |
910 | // phpcs:enable Generic.Files.LineLength |
911 | } |
912 | } elseif ($e->getMessage() !== '') { |
913 | $finalErrorString = $e->getMessage(); |
914 | } else { |
915 | $finalErrorString = __("Unknown error."); |
916 | } |
917 | } |
918 | |
919 | $msg = __("Error found in the IMS QTI Test:\n%s", $finalErrorString); |
920 | $report->add(common_report_Report::createFailure($msg)); |
921 | } catch (PropertyDoesNotExistException $e) { |
922 | $reportCtx->itemClass = $targetItemClass; |
923 | $report->add(Report::createError($e->getMessage())); |
924 | } catch (CatEngineNotFoundException $e) { |
925 | $report->add( |
926 | new common_report_Report( |
927 | common_report_Report::TYPE_ERROR, |
928 | __('No CAT Engine configured for CAT Endpoint "%s".', $e->getRequestedEndpoint()) |
929 | ) |
930 | ); |
931 | } catch (AdaptiveSectionInjectionException $e) { |
932 | $report->add( |
933 | new common_report_Report( |
934 | common_report_Report::TYPE_ERROR, |
935 | // phpcs:disable Generic.Files.LineLength |
936 | __("Items with assessmentItemRef identifiers \"%s\" are not registered in the related CAT endpoint.", implode(', ', $e->getInvalidItemIdentifiers())) |
937 | // phpcs:enable Generic.Files.LineLength |
938 | ) |
939 | ); |
940 | } |
941 | } |
942 | |
943 | $report->setData($reportCtx); |
944 | |
945 | if ($report->containsError() === false) { |
946 | $report->setType(common_report_Report::TYPE_SUCCESS); |
947 | // phpcs:disable Generic.Files.LineLength |
948 | $msg = __("IMS QTI Test referenced as \"%s\" in the IMS Manifest file successfully imported.", $qtiTestResource->getIdentifier()); |
949 | // phpcs:enable Generic.Files.LineLength |
950 | $report->setMessage($msg); |
951 | $eventManager = $this->getEventManager(); |
952 | |
953 | $eventManager->trigger(new ResourceCreated($testResource)); |
954 | $eventManager->trigger(new TestImportedEvent($testResource->getUri())); |
955 | } else { |
956 | $report->setType(common_report_Report::TYPE_ERROR); |
957 | // phpcs:disable Generic.Files.LineLength |
958 | $msg = __("The IMS QTI Test referenced as \"%s\" in the IMS Manifest file could not be imported.", $qtiTestResource->getIdentifier()); |
959 | // phpcs:enable Generic.Files.LineLength |
960 | $report->setMessage($msg); |
961 | } |
962 | |
963 | return $report; |
964 | } |
965 | |
966 | /** |
967 | * @throws common_Exception |
968 | */ |
969 | private function deleteTestsFromClassByLabel(string $testLabel, core_kernel_classes_Resource $testClass): void |
970 | { |
971 | $testService = $this->getTestService(); |
972 | |
973 | foreach ($testClass->getInstances() as $testInstance) { |
974 | if ($testInstance->getLabel() === $testLabel) { |
975 | $testService->deleteTest($testInstance); |
976 | } |
977 | } |
978 | } |
979 | |
980 | /** |
981 | * Import the Test itself by importing its QTI-XML definition into the system, after |
982 | * the QTI Items composing the test were also imported. |
983 | * |
984 | * The $itemMapping argument makes the implementation of this method able to know |
985 | * what are the items that were imported. The $itemMapping is an associative array |
986 | * where keys are the assessmentItemRef's identifiers and the values are the core_kernel_classes_Resources of |
987 | * the items that are now stored in the system. |
988 | * |
989 | * When this method returns false, it means that an error occured at the level of the content of the imported test |
990 | * itself e.g. an item referenced by the test is not present in the content package. In this case, $report might |
991 | * contain useful information to return to the client. |
992 | * |
993 | * @param core_kernel_classes_Resource $testResource A Test Resource the new content must be bind to. |
994 | * @param XmlDocument $testDefinition An XmlAssessmentTestDocument object. |
995 | * @param Resource $qtiResource The manifest resource describing the test to be imported. |
996 | * @param array $itemMapping An associative array that represents the mapping between assessmentItemRef elements |
997 | * and the imported items. |
998 | * @param string $extractionFolder The absolute path to the temporary folder containing the content of the imported |
999 | * IMS QTI Package Archive. |
1000 | * @param common_report_Report $report A Report object to be filled during the import. |
1001 | * @return Directory The newly created test content. |
1002 | * @throws taoQtiTest_models_classes_QtiTestServiceException If an unexpected runtime error occurs. |
1003 | */ |
1004 | protected function importTestDefinition( |
1005 | core_kernel_classes_Resource $testResource, |
1006 | XmlDocument $testDefinition, |
1007 | Resource $qtiResource, |
1008 | array $itemMapping, |
1009 | $extractionFolder, |
1010 | common_report_Report $report |
1011 | ) { |
1012 | |
1013 | foreach ($itemMapping as $itemRefId => $itemResource) { |
1014 | $itemRef = $testDefinition->getDocumentComponent()->getComponentByIdentifier($itemRefId); |
1015 | $itemRef->setHref($itemResource->getUri()); |
1016 | } |
1017 | |
1018 | $oldFile = $this->getQtiTestFile($testResource); |
1019 | $oldFile->delete(); |
1020 | |
1021 | $ds = DIRECTORY_SEPARATOR; |
1022 | $path = dirname($qtiResource->getFile()) . $ds . self::TAOQTITEST_FILENAME; |
1023 | $dir = $this->getQtiTestDir($testResource); |
1024 | $newFile = $dir->getFile($path); |
1025 | $newFile->write($testDefinition->saveToString()); |
1026 | $this->setQtiIndexFile($dir, $path); |
1027 | return $this->getQtiTestDir($testResource); |
1028 | } |
1029 | |
1030 | /** |
1031 | * |
1032 | * @param Directory $dir |
1033 | * @param $path |
1034 | * @return bool |
1035 | */ |
1036 | protected function setQtiIndexFile(Directory $dir, $path) |
1037 | { |
1038 | $newFile = $dir->getFile(self::QTI_TEST_DEFINITION_INDEX); |
1039 | return $newFile->put($path); |
1040 | } |
1041 | |
1042 | /** |
1043 | * @param Directory $dir |
1044 | * @return false|string |
1045 | */ |
1046 | protected function getQtiDefinitionPath(Directory $dir) |
1047 | { |
1048 | $index = $dir->getFile(self::QTI_TEST_DEFINITION_INDEX); |
1049 | if ($index->exists()) { |
1050 | return $index->read(); |
1051 | } |
1052 | return false; |
1053 | } |
1054 | |
1055 | /** |
1056 | * Imports the auxiliary files (file elements contained in the resource test element to be imported) into |
1057 | * the TAO Test Content directory. |
1058 | * |
1059 | * If some file cannot be copied, warnings will be committed. |
1060 | * |
1061 | * @param Directory $testContent The pointer to the TAO Test Content directory where auxilliary files will be |
1062 | * stored. |
1063 | * @param Resource $qtiResource The manifest resource describing the test to be imported. |
1064 | * @param string $extractionFolder The absolute path to the temporary folder containing the content of the imported |
1065 | * IMS QTI Package Archive. |
1066 | * @param common_report_Report A report about how the importation behaved. |
1067 | */ |
1068 | protected function importTestAuxiliaryFiles( |
1069 | Directory $testContent, |
1070 | Resource $qtiResource, |
1071 | $extractionFolder, |
1072 | common_report_Report $report |
1073 | ) { |
1074 | |
1075 | foreach ($qtiResource->getAuxiliaryFiles() as $aux) { |
1076 | try { |
1077 | taoQtiTest_helpers_Utils::storeQtiResource($testContent, $aux, $extractionFolder); |
1078 | } catch (common_Exception $e) { |
1079 | $report->add( |
1080 | new common_report_Report( |
1081 | common_report_Report::TYPE_WARNING, |
1082 | __('Auxiliary file not found at location "%s".', $aux) |
1083 | ) |
1084 | ); |
1085 | } |
1086 | } |
1087 | } |
1088 | |
1089 | /** |
1090 | * Get the File object corresponding to the location |
1091 | * of the test content (a directory!) on the file system. |
1092 | * |
1093 | * @param core_kernel_classes_Resource $test |
1094 | * @return null|File |
1095 | * @throws taoQtiTest_models_classes_QtiTestServiceException |
1096 | */ |
1097 | public function getTestFile(core_kernel_classes_Resource $test) |
1098 | { |
1099 | $testModel = $test->getOnePropertyValue($this->getProperty(TestService::PROPERTY_TEST_TESTMODEL)); |
1100 | if (is_null($testModel) || $testModel->getUri() != self::INSTANCE_TEST_MODEL_QTI) { |
1101 | throw new taoQtiTest_models_classes_QtiTestServiceException( |
1102 | 'The selected test is not a QTI test', |
1103 | taoQtiTest_models_classes_QtiTestServiceException::TEST_READ_ERROR |
1104 | ); |
1105 | } |
1106 | $file = $test->getOnePropertyValue($this->getProperty(TestService::PROPERTY_TEST_CONTENT)); |
1107 | |
1108 | if (!is_null($file)) { |
1109 | return $this->getFileReferenceSerializer()->unserializeFile($file->getUri()); |
1110 | } |
1111 | |
1112 | return null; |
1113 | } |
1114 | |
1115 | /** |
1116 | * Get the QTI reprensentation of a test content. |
1117 | * |
1118 | * @param core_kernel_classes_Resource $test the test to get the content from |
1119 | * @return XmlDocument the QTI representation from the test content |
1120 | * @throws taoQtiTest_models_classes_QtiTestServiceException |
1121 | */ |
1122 | public function getDoc(core_kernel_classes_Resource $test) |
1123 | { |
1124 | $doc = new XmlDocument('2.2'); |
1125 | $doc->loadFromString($this->getQtiTestFile($test)->read()); |
1126 | return $doc; |
1127 | } |
1128 | |
1129 | /** |
1130 | * Get the path of the QTI XML test definition of a given $test resource. |
1131 | * |
1132 | * @param core_kernel_classes_Resource $test |
1133 | * @throws Exception If no QTI-XML or multiple QTI-XML test definition were found. |
1134 | * @return string The absolute path to the QTI XML Test definition related to $test. |
1135 | */ |
1136 | public function getDocPath(core_kernel_classes_Resource $test) |
1137 | { |
1138 | $file = $this->getQtiTestFile($test); |
1139 | return $file->getBasename(); |
1140 | } |
1141 | |
1142 | /** |
1143 | * Get the items from a QTI test document. |
1144 | * |
1145 | * @param \qtism\data\storage\xml\XmlDocument $doc The QTI XML document to be inspected to retrieve the items. |
1146 | * @return core_kernel_classes_Resource[] An array of core_kernel_classes_Resource object indexed by |
1147 | * assessmentItemRef->identifier (string). |
1148 | */ |
1149 | private function getDocItems(XmlDocument $doc) |
1150 | { |
1151 | $itemArray = []; |
1152 | foreach ($doc->getDocumentComponent()->getComponentsByClassName('assessmentItemRef') as $itemRef) { |
1153 | $itemArray[$itemRef->getIdentifier()] = $this->getResource($itemRef->getHref()); |
1154 | } |
1155 | return $itemArray; |
1156 | } |
1157 | |
1158 | /** |
1159 | * Assign items to a QTI test. |
1160 | * @param XmlDocument $doc |
1161 | * @param array $items |
1162 | * @return int |
1163 | * @throws taoQtiTest_models_classes_QtiTestServiceException |
1164 | */ |
1165 | private function setItemsToDoc(XmlDocument $doc, array $items, $sectionIndex = 0) |
1166 | { |
1167 | |
1168 | $sections = $doc->getDocumentComponent()->getComponentsByClassName('assessmentSection'); |
1169 | if (!isset($sections[$sectionIndex])) { |
1170 | throw new taoQtiTest_models_classes_QtiTestServiceException( |
1171 | 'No section found in test at index : ' . $sectionIndex, |
1172 | taoQtiTest_models_classes_QtiTestServiceException::TEST_READ_ERROR |
1173 | ); |
1174 | } |
1175 | $section = $sections[$sectionIndex]; |
1176 | |
1177 | $itemRefs = new SectionPartCollection(); |
1178 | $itemRefIdentifiers = []; |
1179 | foreach ($items as $itemResource) { |
1180 | $itemDoc = new XmlDocument(); |
1181 | |
1182 | try { |
1183 | $itemDoc->loadFromString(Service::singleton()->getXmlByRdfItem($itemResource)); |
1184 | } catch (StorageException $e) { |
1185 | // We consider the item not compliant with QTI, let's try the next one. |
1186 | continue; |
1187 | } |
1188 | |
1189 | $itemRefIdentifier = $itemDoc->getDocumentComponent()->getIdentifier(); |
1190 | |
1191 | //enable more than one reference |
1192 | if (array_key_exists($itemRefIdentifier, $itemRefIdentifiers)) { |
1193 | $itemRefIdentifiers[$itemRefIdentifier] += 1; |
1194 | $itemRefIdentifier .= '-' . $itemRefIdentifiers[$itemRefIdentifier]; |
1195 | } else { |
1196 | $itemRefIdentifiers[$itemRefIdentifier] = 0; |
1197 | } |
1198 | $itemRefs[] = new AssessmentItemRef($itemRefIdentifier, $itemResource->getUri()); |
1199 | } |
1200 | $section->setSectionParts($itemRefs); |
1201 | |
1202 | return count($itemRefs); |
1203 | } |
1204 | |
1205 | /** |
1206 | * Get root qti test directory or crate if not exists |
1207 | * |
1208 | * @param core_kernel_classes_Resource $test |
1209 | * @param boolean $createTestFile Whether or not create an empty QTI XML test file. Default is |
1210 | * (boolean) true. |
1211 | * |
1212 | * @return Directory |
1213 | * |
1214 | * @throws taoQtiTest_models_classes_QtiTestServiceException |
1215 | * @throws common_exception_InconsistentData |
1216 | * @throws core_kernel_persistence_Exception |
1217 | */ |
1218 | public function getQtiTestDir(core_kernel_classes_Resource $test, $createTestFile = true) |
1219 | { |
1220 | $testModel = $this->getServiceLocator()->get(TestService::class)->getTestModel($test); |
1221 | |
1222 | if ($testModel->getUri() !== self::INSTANCE_TEST_MODEL_QTI) { |
1223 | throw new taoQtiTest_models_classes_QtiTestServiceException( |
1224 | 'The selected test is not a QTI test', |
1225 | taoQtiTest_models_classes_QtiTestServiceException::TEST_READ_ERROR |
1226 | ); |
1227 | } |
1228 | |
1229 | $dir = $test->getOnePropertyValue($this->getProperty(TestService::PROPERTY_TEST_CONTENT)); |
1230 | |
1231 | if (null !== $dir) { |
1232 | /** @noinspection PhpIncompatibleReturnTypeInspection */ |
1233 | return $this->getFileReferenceSerializer()->unserialize($dir); |
1234 | } |
1235 | |
1236 | return $this->createContent($test, $createTestFile); |
1237 | } |
1238 | |
1239 | protected function searchInTestDirectory(Directory $dir) |
1240 | { |
1241 | $iterator = $dir->getFlyIterator(Directory::ITERATOR_RECURSIVE | Directory::ITERATOR_FILE); |
1242 | $files = []; |
1243 | |
1244 | /** |
1245 | * @var File $file |
1246 | */ |
1247 | foreach ($iterator as $file) { |
1248 | if ($file->getBasename() === self::TAOQTITEST_FILENAME) { |
1249 | $files[] = $file; |
1250 | break; |
1251 | } |
1252 | } |
1253 | |
1254 | if (empty($files)) { |
1255 | throw new Exception('No QTI-XML test file found.'); |
1256 | } |
1257 | |
1258 | $file = current($files); |
1259 | $fileName = str_replace($dir->getPrefix() . '/', '', $file->getPrefix()); |
1260 | $this->setQtiIndexFile($dir, $fileName); |
1261 | |
1262 | return $file; |
1263 | } |
1264 | |
1265 | /** |
1266 | * Return the File containing the test definition |
1267 | * If it doesn't exist, it will be created |
1268 | * |
1269 | * @param core_kernel_classes_Resource $test |
1270 | * @throws \Exception If file is not found. |
1271 | * @return File |
1272 | */ |
1273 | public function getQtiTestFile(core_kernel_classes_Resource $test) |
1274 | { |
1275 | |
1276 | $dir = $this->getQtiTestDir($test); |
1277 | |
1278 | $file = $this->getQtiDefinitionPath($dir); |
1279 | |
1280 | if (!empty($file)) { |
1281 | return $dir->getFile($file); |
1282 | } |
1283 | return $this->searchInTestDirectory($dir); |
1284 | } |
1285 | |
1286 | /** |
1287 | * |
1288 | * @param core_kernel_classes_Resource $test |
1289 | * @throws Exception |
1290 | * @return string |
1291 | */ |
1292 | public function getRelTestPath(core_kernel_classes_Resource $test) |
1293 | { |
1294 | $testRootDir = $this->getQtiTestDir($test); |
1295 | return $testRootDir->getRelPath($this->getQtiTestFile($test)); |
1296 | } |
1297 | |
1298 | /** |
1299 | * Save the content of test from a QTI Document |
1300 | * @param core_kernel_classes_Resource $test |
1301 | * @param qtism\data\storage\xml\XmlDocument $doc |
1302 | * @return boolean true if saved |
1303 | * @throws taoQtiTest_models_classes_QtiTestServiceException |
1304 | */ |
1305 | private function saveDoc(core_kernel_classes_Resource $test, XmlDocument $doc) |
1306 | { |
1307 | $file = $this->getQtiTestFile($test); |
1308 | return $file->update($doc->saveToString()); |
1309 | } |
1310 | |
1311 | /** |
1312 | * Create the default content directory of a QTI test. |
1313 | * |
1314 | * @param core_kernel_classes_Resource $test |
1315 | * @param boolean $createTestFile Whether or not create an empty QTI XML test file. Default is (boolean) true. |
1316 | * @param boolean $preventOverride Prevent data to be overriden Default is (boolean) true. |
1317 | * |
1318 | * @return Directory the content directory |
1319 | * @throws XmlStorageException |
1320 | * @throws common_Exception |
1321 | * @throws common_exception_Error |
1322 | * @throws common_exception_InconsistentData In case of trying to override existing data. |
1323 | * @throws common_ext_ExtensionException |
1324 | * @throws taoQtiTest_models_classes_QtiTestServiceException If a runtime error occurs while creating the |
1325 | * test content. |
1326 | */ |
1327 | public function createContent(core_kernel_classes_Resource $test, $createTestFile = true, $preventOverride = true) |
1328 | { |
1329 | $dir = $this->getDefaultDir()->getDirectory(md5($test->getUri())); |
1330 | if ($dir->exists() && $preventOverride === true) { |
1331 | throw new common_exception_InconsistentData( |
1332 | 'Data directory for test ' . $test->getUri() . ' already exists.' |
1333 | ); |
1334 | } |
1335 | |
1336 | $file = $dir->getFile(self::TAOQTITEST_FILENAME); |
1337 | |
1338 | if ($createTestFile === true) { |
1339 | /** @var AssessmentTestXmlFactory $xmlBuilder */ |
1340 | $xmlBuilder = $this->getServiceLocator()->get(AssessmentTestXmlFactory::class); |
1341 | |
1342 | $testLabel = $test->getLabel(); |
1343 | $identifier = $this->createTestIdentifier($test); |
1344 | $xml = $xmlBuilder->create($identifier, $testLabel); |
1345 | |
1346 | if (!$file->write($xml)) { |
1347 | throw new taoQtiTest_models_classes_QtiTestServiceException( |
1348 | 'Unable to write raw QTI Test template.', |
1349 | taoQtiTest_models_classes_QtiTestServiceException::TEST_WRITE_ERROR |
1350 | ); |
1351 | } |
1352 | |
1353 | common_Logger::t("Created QTI Test content for test '" . $test->getUri() . "'."); |
1354 | } elseif ($file->exists()) { |
1355 | $doc = new DOMDocument('1.0', 'UTF-8'); |
1356 | $doc->loadXML($file->read()); |
1357 | |
1358 | // Label update only. |
1359 | $doc->documentElement->setAttribute('title', $test->getLabel()); |
1360 | |
1361 | if (!$file->update($doc->saveXML())) { |
1362 | $msg = 'Unable to update QTI Test file.'; |
1363 | throw new taoQtiTest_models_classes_QtiTestServiceException( |
1364 | $msg, |
1365 | taoQtiTest_models_classes_QtiTestServiceException::TEST_WRITE_ERROR |
1366 | ); |
1367 | } |
1368 | } |
1369 | |
1370 | $directory = $this->getFileReferenceSerializer()->serialize($dir); |
1371 | $test->editPropertyValues($this->getProperty(TestService::PROPERTY_TEST_CONTENT), $directory); |
1372 | |
1373 | return $dir; |
1374 | } |
1375 | |
1376 | /** |
1377 | * Delete the content of a QTI test |
1378 | * @param core_kernel_classes_Resource $test |
1379 | * @throws common_exception_Error |
1380 | */ |
1381 | public function deleteContent(core_kernel_classes_Resource $test) |
1382 | { |
1383 | $content = $test->getOnePropertyValue($this->getProperty(TestService::PROPERTY_TEST_CONTENT)); |
1384 | |
1385 | if (!is_null($content)) { |
1386 | $dir = $this->getFileReferenceSerializer()->unserialize($content); |
1387 | $dir->deleteSelf(); |
1388 | $this->getFileReferenceSerializer()->cleanUp($content); |
1389 | $test->removePropertyValue($this->getProperty(TestService::PROPERTY_TEST_CONTENT), $content); |
1390 | } |
1391 | } |
1392 | |
1393 | /** |
1394 | * Set the directory where the tests' contents are stored. |
1395 | * @param string $fsId |
1396 | */ |
1397 | public function setQtiTestFileSystem($fsId) |
1398 | { |
1399 | $ext = common_ext_ExtensionsManager::singleton()->getExtensionById('taoQtiTest'); |
1400 | $ext->setConfig(self::CONFIG_QTITEST_FILESYSTEM, $fsId); |
1401 | } |
1402 | |
1403 | /** |
1404 | * Get the default directory where the tests' contents are stored. |
1405 | * replaces getQtiTestFileSystem |
1406 | * |
1407 | * @return Directory |
1408 | */ |
1409 | public function getDefaultDir() |
1410 | { |
1411 | $ext = $this |
1412 | ->getServiceLocator() |
1413 | ->get(common_ext_ExtensionsManager::SERVICE_ID) |
1414 | ->getExtensionById('taoQtiTest'); |
1415 | $fsId = $ext->getConfig(self::CONFIG_QTITEST_FILESYSTEM); |
1416 | return $this->getServiceLocator()->get(FileSystemService::SERVICE_ID)->getDirectory($fsId); |
1417 | } |
1418 | |
1419 | /** |
1420 | * Set the acceptable latency time (applied on qti:timeLimits->minTime, qti:timeLimits:maxTime). |
1421 | * |
1422 | * @param string $duration An ISO 8601 Duration. |
1423 | * @see http://www.php.net/manual/en/dateinterval.construct.php PHP's interval_spec format (based on ISO 8601). |
1424 | */ |
1425 | public function setQtiTestAcceptableLatency($duration) |
1426 | { |
1427 | $ext = common_ext_ExtensionsManager::singleton()->getExtensionById('taoQtiTest'); |
1428 | $ext->setConfig(self::CONFIG_QTITEST_ACCEPTABLE_LATENCY, $duration); |
1429 | } |
1430 | |
1431 | /** |
1432 | * Get the acceptable latency time (applied on qti:timeLimits->minTime, qti:timeLimits->maxTime). |
1433 | * |
1434 | * @throws common_Exception If no value can be found as the acceptable latency in the extension's configuration |
1435 | * file. |
1436 | * @return string An ISO 8601 Duration. |
1437 | * @see http://www.php.net/manual/en/dateinterval.construct.php PHP's interval_spec format (based on ISO 8601). |
1438 | */ |
1439 | public function getQtiTestAcceptableLatency() |
1440 | { |
1441 | $ext = $this->getServiceLocator()->get(common_ext_ExtensionsManager::SERVICE_ID) |
1442 | ->getExtensionById('taoQtiTest'); |
1443 | $latency = $ext->getConfig(self::CONFIG_QTITEST_ACCEPTABLE_LATENCY); |
1444 | if (empty($latency)) { |
1445 | // Default duration for legacy code or missing config. |
1446 | return 'PT5S'; |
1447 | } |
1448 | return $latency; |
1449 | } |
1450 | |
1451 | /** |
1452 | * |
1453 | * @deprecated |
1454 | * |
1455 | * Get the content of the QTI Test template file as an XML string. |
1456 | * |
1457 | * @return string|boolean The QTI Test template file content or false if it could not be read. |
1458 | */ |
1459 | public function getQtiTestTemplateFileAsString() |
1460 | { |
1461 | $ext = $this |
1462 | ->getServiceLocator() |
1463 | ->get(common_ext_ExtensionsManager::SERVICE_ID) |
1464 | ->getExtensionById('taoQtiTest'); |
1465 | return file_get_contents( |
1466 | $ext->getDir() . 'models' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . 'qtiTest.xml' |
1467 | ); |
1468 | } |
1469 | |
1470 | /** |
1471 | * Get the lom metadata importer |
1472 | * |
1473 | * @return MetadataImporter |
1474 | */ |
1475 | protected function getMetadataImporter() |
1476 | { |
1477 | if (!$this->metadataImporter) { |
1478 | $this->metadataImporter = $this->getServiceLocator()->get(MetadataService::SERVICE_ID)->getImporter(); |
1479 | } |
1480 | return $this->metadataImporter; |
1481 | } |
1482 | |
1483 | private function getMetaMetadataExtractor(): MetaMetadataExtractor |
1484 | { |
1485 | return $this->getPsrContainer()->get(MetaMetadataExtractor::class); |
1486 | } |
1487 | |
1488 | private function getSecureResourceService(): SecureResourceServiceInterface |
1489 | { |
1490 | return $this->getServiceLocator()->get(SecureResourceServiceInterface::SERVICE_ID); |
1491 | } |
1492 | |
1493 | /** |
1494 | * @param core_kernel_classes_Resource $oldTest |
1495 | * @param string $json |
1496 | * |
1497 | * @throws ResourceAccessDeniedException |
1498 | */ |
1499 | private function verifyItemPermissions(core_kernel_classes_Resource $oldTest, string $json): void |
1500 | { |
1501 | $array = json_decode($json, true); |
1502 | |
1503 | $ids = []; |
1504 | |
1505 | $oldItemIds = []; |
1506 | foreach ($this->getTestItems($oldTest) as $item) { |
1507 | $oldItemIds[] = $item->getUri(); |
1508 | } |
1509 | |
1510 | foreach ($array['testParts'] ?? [] as $testPart) { |
1511 | foreach ($testPart['assessmentSections'] ?? [] as $assessmentSection) { |
1512 | foreach ($assessmentSection['sectionParts'] ?? [] as $item) { |
1513 | if ( |
1514 | isset($item['href']) |
1515 | && !in_array($item['href'], $oldItemIds) |
1516 | && $item['qti-type'] ?? '' === self::XML_ASSESSMENT_ITEM_REF |
1517 | ) { |
1518 | $ids[] = $item['href']; |
1519 | } |
1520 | } |
1521 | } |
1522 | } |
1523 | |
1524 | $this->getSecureResourceService()->validatePermissions($ids, ['READ']); |
1525 | } |
1526 | |
1527 | private function deleteItemSubclassesByLabel(core_kernel_classes_Class $root, string $label): void |
1528 | { |
1529 | $itemTreeService = $this->getItemTreeService(); |
1530 | |
1531 | foreach ($root->getSubClasses() as $subClass) { |
1532 | if ($subClass->getLabel() !== $label) { |
1533 | continue; |
1534 | } |
1535 | |
1536 | foreach ($subClass->getInstances(true) as $instance) { |
1537 | $itemTreeService->delete(new DeleteItemCommand($instance, true)); |
1538 | } |
1539 | |
1540 | $itemTreeService->deleteClass($subClass); |
1541 | } |
1542 | } |
1543 | |
1544 | private function getQtiPackageImportPreprocessing(): QtiPackageImportPreprocessing |
1545 | { |
1546 | return $this->getPsrContainer()->get(QtiPackageImportPreprocessing::SERVICE_ID); |
1547 | } |
1548 | |
1549 | private function getItemTreeService(): taoItems_models_classes_ItemsService |
1550 | { |
1551 | return $this->getPsrContainer()->get(taoItems_models_classes_ItemsService::class); |
1552 | } |
1553 | |
1554 | private function getTestService(): taoTests_models_classes_TestsService |
1555 | { |
1556 | return $this->getPsrContainer()->get(taoTests_models_classes_TestsService::class); |
1557 | } |
1558 | |
1559 | private function getPsrContainer(): ContainerInterface |
1560 | { |
1561 | return $this->getServiceLocator()->getContainer(); |
1562 | } |
1563 | |
1564 | private function getMetaMetadataImporter(): MetaMetadataImportMapper |
1565 | { |
1566 | return $this->getServiceManager()->getContainer()->get(MetaMetadataImportMapper::class); |
1567 | } |
1568 | |
1569 | /** |
1570 | * @throws PropertyDoesNotExistException |
1571 | * @throws \oat\tao\model\metadata\exception\MetadataImportException |
1572 | */ |
1573 | private function getMappedProperties( |
1574 | bool $importMetadata, |
1575 | DOMDocument $domManifest, |
1576 | stdClass $reportCtx, |
1577 | core_kernel_classes_Class $testClass, |
1578 | core_kernel_classes_Class $targetItemClass |
1579 | ): array { |
1580 | if ($importMetadata === true) { |
1581 | $metaMetadataValues = $this->getMetaMetadataExtractor()->extract($domManifest); |
1582 | $reportCtx->metaMetadata = $metaMetadataValues; |
1583 | |
1584 | $mappedMetadataValues = $this->getMetaMetadataImporter() |
1585 | ->mapMetaMetadataToProperties($metaMetadataValues, $targetItemClass, $testClass); |
1586 | |
1587 | $metadataValues = $this->getMetadataImporter()->extract($domManifest); |
1588 | $notMatchingProperties = $this->checkMissingClassProperties($metadataValues, $mappedMetadataValues); |
1589 | if (!empty($notMatchingProperties)) { |
1590 | $message['checksum_result'] = false; |
1591 | $message['label'] = implode(', ', $notMatchingProperties); |
1592 | throw new PropertyDoesNotExistException($message); |
1593 | } |
1594 | if (empty($mappedMetadataValues)) { |
1595 | $mappedMetadataValues = $this->getMetaMetadataImporter()->mapMetadataToProperties( |
1596 | $metadataValues, |
1597 | $targetItemClass, |
1598 | $testClass |
1599 | ); |
1600 | } |
1601 | return $mappedMetadataValues; |
1602 | } |
1603 | |
1604 | return []; |
1605 | } |
1606 | |
1607 | private function getIdentifierGenerator(): ?IdentifierGeneratorInterface |
1608 | { |
1609 | try { |
1610 | return $this->getPsrContainer()->get(IdentifierGeneratorProxy::class); |
1611 | } catch (Throwable $exception) { |
1612 | return null; |
1613 | } |
1614 | } |
1615 | |
1616 | private function createTestIdentifier(core_kernel_classes_Resource $test): string |
1617 | { |
1618 | $generator = $this->getIdentifierGenerator(); |
1619 | $testLabel = $test->getLabel(); |
1620 | |
1621 | if ($generator) { |
1622 | return $generator->generate([IdentifierGeneratorInterface::OPTION_RESOURCE => $test]); |
1623 | } |
1624 | |
1625 | $identifier = null; |
1626 | |
1627 | if (preg_match('/^\d/', $testLabel)) { |
1628 | $identifier = 't_' . $testLabel; |
1629 | } |
1630 | |
1631 | return str_replace('_', '-', Format::sanitizeIdentifier($identifier)); |
1632 | } |
1633 | |
1634 | private function getTestLabel(array $testMetadata, XmlDocument $testDefinition): string |
1635 | { |
1636 | $labelMetadata = array_filter($testMetadata, function ($metadata) { |
1637 | return in_array(RDFS_LABEL, $metadata->getPath()); |
1638 | }); |
1639 | |
1640 | if (empty($labelMetadata)) { |
1641 | if ($testDefinition->getDocumentComponent() === null) { |
1642 | throw new Exception('No metadata label found for test and no title in the test definition.'); |
1643 | } |
1644 | |
1645 | common_Logger::w('No metadata label found for test. Using the title from the test definition.'); |
1646 | return $testDefinition->getDocumentComponent()->getTitle(); |
1647 | } |
1648 | |
1649 | if (count($labelMetadata) > 1) { |
1650 | common_Logger::w('Multiple labels found for test. Using the first one.'); |
1651 | } |
1652 | |
1653 | return reset($labelMetadata)->getValue(); |
1654 | } |
1655 | |
1656 | private function syncUniqueId(core_kernel_classes_Resource $test, string &$json): void |
1657 | { |
1658 | if (!$this->getFeatureFlagChecker()->isEnabled('FEATURE_FLAG_UNIQUE_NUMERIC_QTI_IDENTIFIER')) { |
1659 | return; |
1660 | } |
1661 | |
1662 | $uniqueId = (string) $test->getOnePropertyValue($this->getProperty(TaoOntology::PROPERTY_UNIQUE_IDENTIFIER)); |
1663 | $decodedJson = json_decode($json, true); |
1664 | |
1665 | if (!empty($uniqueId) && $uniqueId !== $decodedJson['identifier']) { |
1666 | $decodedJson['identifier'] = $uniqueId; |
1667 | $json = json_encode($decodedJson); |
1668 | } |
1669 | } |
1670 | |
1671 | private function getManifestConverter(): ManifestConverter |
1672 | { |
1673 | return $this->getPsrContainer()->get(ManifestConverter::class); |
1674 | } |
1675 | |
1676 | private function getTestConverter(): TestConverter |
1677 | { |
1678 | return $this->getPsrContainer()->get(TestConverter::class); |
1679 | } |
1680 | |
1681 | private function getSectionConverter(): AssessmentSectionConverter |
1682 | { |
1683 | return $this->getPsrContainer()->get(AssessmentSectionConverter::class); |
1684 | } |
1685 | |
1686 | /** |
1687 | * @param AssessmentSectionRef[] $testDefinition |
1688 | */ |
1689 | private function convertAssessmentSectionRefs(QtiComponentCollection $assessmentSectionRefs, string $folder): void |
1690 | { |
1691 | if (!$this->getPsrContainer()->has(AssessmentSectionConverter::class)) { |
1692 | return; |
1693 | } |
1694 | |
1695 | foreach ($assessmentSectionRefs as $assessmentSectionRef) { |
1696 | $file = $folder . $assessmentSectionRef->getHref(); |
1697 | $this->getSectionConverter()->convertToQti2($file); |
1698 | } |
1699 | } |
1700 | |
1701 | private function getFeatureFlagChecker(): FeatureFlagCheckerInterface |
1702 | { |
1703 | return $this->getPsrContainer()->get(FeatureFlagChecker::class); |
1704 | } |
1705 | } |