Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 363 |
|
0.00% |
0 / 53 |
CRAP | |
0.00% |
0 / 1 |
QtiRunnerServiceContext | |
0.00% |
0 / 363 |
|
0.00% |
0 / 53 |
11772 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
init | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
initCompilationDirectory | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
initTestDefinition | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
initStorage | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
initTestSession | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
retrieveTestMeta | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
retrieveItemIndex | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
setTestSession | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
getStorage | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getEventManager | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSessionManager | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getTestDefinition | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getCompilationDirectory | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getTestMeta | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getTestCompilationVersion | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTestDefinitionUri | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTestCompilationUri | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTestExecutionUri | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getItemIndex | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getUserUri | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
setUserUri | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getItemIndexValue | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
getCatEngine | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
getTestSession | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getCatSession | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
persistCatSession | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
persistSeenCatItemIds | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
12 | |||
getLastCatItemOutput | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
persistLastCatItemOutput | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
getCatSection | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
isAdaptive | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
containsAdaptive | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
selectAdaptiveNextItem | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
42 | |||
getCurrentAssessmentItemRef | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
getPreviouslySeenCatItemIds | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
getShadowTest | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
getCurrentCatItemId | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
persistCurrentCatItemId | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
getItemPositionInRoute | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
42 | |||
getCurrentPosition | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
30 | |||
getCatAttempts | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
persistCatAttempts | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
6 | |||
canMoveBackward | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
30 | |||
saveAdaptiveResults | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
storeResult | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
72 | |||
convertCatVariables | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
72 | |||
getItemUriFromRefId | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
isSyncingMode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setSyncingMode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTestTakerFromSessionOrRds | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
getSectionPauseService | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCatService | |
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) 2017-2023 (original work) Open Assessment Technologies SA. |
19 | */ |
20 | |
21 | namespace oat\taoQtiTest\models\runner; |
22 | |
23 | use common_exception_InvalidArgumentType; |
24 | use common_ext_ExtensionException; |
25 | use common_ext_ExtensionsManager; |
26 | use common_session_SessionManager; |
27 | use core_kernel_classes_Resource; |
28 | use oat\libCat\CatEngine; |
29 | use oat\libCat\CatSection; |
30 | use oat\libCat\CatSession; |
31 | use oat\libCat\exception\CatEngineException; |
32 | use oat\libCat\result\AbstractResult; |
33 | use oat\libCat\result\ItemResult; |
34 | use oat\libCat\result\ResultVariable; |
35 | use oat\oatbox\event\EventManager; |
36 | use oat\oatbox\service\ServiceNotFoundException; |
37 | use oat\oatbox\user\User; |
38 | use oat\tao\helpers\UserHelper; |
39 | use oat\taoDelivery\model\execution\DeliveryServerService; |
40 | use oat\taoQtiTest\helpers\TestSessionMemento; |
41 | use oat\taoQtiTest\models\cat\CatEngineNotFoundException; |
42 | use oat\taoQtiTest\models\CompilationDataService; |
43 | use oat\taoQtiTest\models\event\QtiTestChangeEvent; |
44 | use oat\taoQtiTest\models\QtiTestCompilerIndex; |
45 | use oat\taoQtiTest\models\runner\session\TestSession; |
46 | use oat\taoQtiTest\models\cat\CatService; |
47 | use oat\taoQtiTest\models\ExtendedStateService; |
48 | use oat\taoQtiTest\models\SectionPauseService; |
49 | use oat\taoQtiTest\models\event\SelectAdaptiveNextItemEvent; |
50 | use Psr\Container\ContainerInterface; |
51 | use qtism\data\AssessmentTest; |
52 | use qtism\data\AssessmentItemRef; |
53 | use qtism\data\ExtendedAssessmentItemRef; |
54 | use qtism\data\NavigationMode; |
55 | use qtism\runtime\storage\binary\AbstractQtiBinaryStorage; |
56 | use qtism\runtime\storage\binary\BinaryAssessmentTestSeeker; |
57 | use qtism\runtime\storage\common\StorageException; |
58 | use qtism\runtime\tests\AssessmentTestSession; |
59 | use qtism\runtime\tests\AssessmentTestSessionException; |
60 | use qtism\runtime\tests\RouteItem; |
61 | use tao_models_classes_service_FileStorage; |
62 | use tao_models_classes_service_StorageDirectory; |
63 | use taoQtiTest_helpers_SessionManager; |
64 | use taoQtiTest_helpers_TestCompilerUtils; |
65 | use taoQtiTest_helpers_TestRunnerUtils; |
66 | use taoQtiTest_models_classes_QtiTestService; |
67 | use taoResultServer_models_classes_Variable; |
68 | use common_Exception; |
69 | use common_exception_Error; |
70 | use common_exception_NotImplemented; |
71 | use common_Logger; |
72 | use Exception; |
73 | |
74 | /** |
75 | * Class QtiRunnerServiceContext |
76 | * |
77 | * Defines a container to store and to share runner service context of the QTI implementation |
78 | * |
79 | * @package oat\taoQtiTest\models |
80 | * |
81 | * @author Jean-Sébastien Conan <jean-sebastien.conan@vesperiagroup.com> |
82 | */ |
83 | class QtiRunnerServiceContext extends RunnerServiceContext |
84 | { |
85 | /** |
86 | * The session storage |
87 | */ |
88 | protected ?AbstractQtiBinaryStorage $storage = null; |
89 | |
90 | protected ?taoQtiTest_helpers_SessionManager $sessionManager = null; |
91 | |
92 | /** |
93 | * The assessment test definition |
94 | */ |
95 | protected ?AssessmentTest $testDefinition = null; |
96 | |
97 | /** |
98 | * The path of the compilation directory. |
99 | * |
100 | * @var tao_models_classes_service_StorageDirectory[] |
101 | */ |
102 | protected ?array $compilationDirectory = null; |
103 | |
104 | /** |
105 | * The metadata about the test definition being executed. |
106 | */ |
107 | private ?array $testMeta = null; |
108 | |
109 | /** |
110 | * The index of compiled items. |
111 | */ |
112 | private QtiTestCompilerIndex $itemIndex; |
113 | |
114 | /** |
115 | * The URI of the assessment test |
116 | */ |
117 | protected string $testDefinitionUri; |
118 | |
119 | /** |
120 | * The URI of the compiled delivery |
121 | */ |
122 | protected string $testCompilationUri; |
123 | |
124 | /** |
125 | * The URI of the delivery execution |
126 | */ |
127 | protected string $testExecutionUri; |
128 | |
129 | /** |
130 | * Whether we are in synchronization mode |
131 | */ |
132 | private bool $syncingMode = false; |
133 | |
134 | private ?string $userUri; |
135 | |
136 | private ?ContainerInterface $container = null; |
137 | |
138 | /** |
139 | * QtiRunnerServiceContext constructor. |
140 | * |
141 | * @param string $testDefinitionUri |
142 | * @param string $testCompilationUri |
143 | * @param string $testExecutionUri |
144 | */ |
145 | public function __construct( |
146 | string $testDefinitionUri, |
147 | string $testCompilationUri, |
148 | string $testExecutionUri |
149 | ) { |
150 | $this->testDefinitionUri = $testDefinitionUri; |
151 | $this->testCompilationUri = $testCompilationUri; |
152 | $this->testExecutionUri = $testExecutionUri; |
153 | } |
154 | |
155 | /** |
156 | * Starts the context |
157 | */ |
158 | public function init() |
159 | { |
160 | $this->retrieveItemIndex(); |
161 | } |
162 | |
163 | /** |
164 | * Extracts the path of the compilation directory |
165 | */ |
166 | protected function initCompilationDirectory() |
167 | { |
168 | $fileStorage = tao_models_classes_service_FileStorage::singleton(); |
169 | $directoryIds = explode('|', $this->getTestCompilationUri()); |
170 | $directories = [ |
171 | 'private' => $fileStorage->getDirectoryById($directoryIds[0]), |
172 | 'public' => $fileStorage->getDirectoryById($directoryIds[1]) |
173 | ]; |
174 | |
175 | $this->compilationDirectory = $directories; |
176 | } |
177 | |
178 | /** |
179 | * Loads the test definition |
180 | */ |
181 | protected function initTestDefinition() |
182 | { |
183 | $this->testDefinition = \taoQtiTest_helpers_Utils::getTestDefinition($this->getTestCompilationUri()); |
184 | } |
185 | |
186 | /** |
187 | * Loads the storage |
188 | * |
189 | * @throws ServiceNotFoundException |
190 | * @throws common_Exception |
191 | * @throws common_exception_Error |
192 | * @throws common_ext_ExtensionException |
193 | */ |
194 | protected function initStorage() |
195 | { |
196 | /** @var DeliveryServerService $deliveryServerService */ |
197 | $deliveryServerService = $this->getServiceManager()->get(DeliveryServerService::SERVICE_ID); |
198 | $resultStore = $deliveryServerService->getResultStoreWrapper($this->getTestExecutionUri()); |
199 | $testResource = new core_kernel_classes_Resource($this->getTestDefinitionUri()); |
200 | $sessionManager = new taoQtiTest_helpers_SessionManager($resultStore, $testResource); |
201 | |
202 | $seeker = new BinaryAssessmentTestSeeker($this->getTestDefinition()); |
203 | $userUri = $this->getUserUri(); |
204 | |
205 | $config = common_ext_ExtensionsManager::singleton()->getExtensionById('taoQtiTest')->getConfig('testRunner'); |
206 | $storageClassName = $config['test-session-storage']; |
207 | $this->storage = new $storageClassName($sessionManager, $seeker, $userUri); |
208 | $this->sessionManager = $sessionManager; |
209 | } |
210 | |
211 | /** |
212 | * Loads the test session |
213 | * |
214 | * @throws StorageException |
215 | * @throws common_exception_Error |
216 | * @throws common_exception_InvalidArgumentType |
217 | * @throws common_ext_ExtensionException |
218 | */ |
219 | protected function initTestSession() |
220 | { |
221 | $storage = $this->getStorage(); |
222 | $sessionId = $this->getTestExecutionUri(); |
223 | |
224 | if ($storage->exists($sessionId) === false) { |
225 | common_Logger::d("Instantiating QTI Assessment Test Session"); |
226 | $this->setTestSession($storage->instantiate($this->getTestDefinition(), $sessionId)); |
227 | |
228 | $testTaker = $this->getTestTakerFromSessionOrRds(); |
229 | taoQtiTest_helpers_TestRunnerUtils::setInitialOutcomes($this->getTestSession(), $testTaker); |
230 | } else { |
231 | common_Logger::d("Retrieving QTI Assessment Test Session '${sessionId}'..."); |
232 | $this->setTestSession($storage->retrieve($this->getTestDefinition(), $sessionId)); |
233 | } |
234 | |
235 | taoQtiTest_helpers_TestRunnerUtils::preserveOutcomes($this->getTestSession()); |
236 | } |
237 | |
238 | /** |
239 | * @deprecated |
240 | */ |
241 | protected function retrieveTestMeta() |
242 | { |
243 | } |
244 | |
245 | /** |
246 | * Retrieves the index of compiled items. |
247 | */ |
248 | protected function retrieveItemIndex() |
249 | { |
250 | $this->itemIndex = new QtiTestCompilerIndex(); |
251 | try { |
252 | $directories = $this->getCompilationDirectory(); |
253 | $data = $directories['private']->read(taoQtiTest_models_classes_QtiTestService::TEST_COMPILED_INDEX); |
254 | if ($data) { |
255 | $this->itemIndex->unserialize($data); |
256 | } |
257 | } catch (Exception $e) { |
258 | common_Logger::d('Ignoring file not found exception for Items Index'); |
259 | } |
260 | } |
261 | |
262 | /** |
263 | * Sets the test session |
264 | * @param mixed $testSession |
265 | * @throws common_exception_InvalidArgumentType |
266 | */ |
267 | public function setTestSession($testSession) |
268 | { |
269 | if ($testSession instanceof TestSession) { |
270 | parent::setTestSession($testSession); |
271 | } else { |
272 | throw new common_exception_InvalidArgumentType( |
273 | 'QtiRunnerServiceContext', |
274 | 'setTestSession', |
275 | 0, |
276 | TestSession::class, |
277 | $testSession |
278 | ); |
279 | } |
280 | } |
281 | |
282 | /** |
283 | * Gets the session storage |
284 | * @return AbstractQtiBinaryStorage |
285 | * @throws common_exception_Error |
286 | * @throws common_ext_ExtensionException |
287 | */ |
288 | public function getStorage() |
289 | { |
290 | if (!$this->storage) { |
291 | $this->initStorage(); |
292 | } |
293 | return $this->storage; |
294 | } |
295 | |
296 | /** |
297 | * @return EventManager |
298 | * @throws \Zend\ServiceManager\Exception\ServiceNotFoundException |
299 | */ |
300 | protected function getEventManager() |
301 | { |
302 | return $this->getServiceLocator()->get(EventManager::SERVICE_ID); |
303 | } |
304 | |
305 | /** |
306 | * @return taoQtiTest_helpers_SessionManager |
307 | * @throws common_exception_Error |
308 | * @throws common_ext_ExtensionException |
309 | */ |
310 | public function getSessionManager() |
311 | { |
312 | if (null === $this->sessionManager) { |
313 | $this->initStorage(); |
314 | } |
315 | return $this->sessionManager; |
316 | } |
317 | |
318 | /** |
319 | * Gets the assessment test definition |
320 | * @return AssessmentTest |
321 | */ |
322 | public function getTestDefinition() |
323 | { |
324 | if (null === $this->testDefinition) { |
325 | $this->initTestDefinition(); |
326 | } |
327 | return $this->testDefinition; |
328 | } |
329 | |
330 | /** |
331 | * Gets the path of the compilation directory |
332 | * @return tao_models_classes_service_StorageDirectory[] |
333 | */ |
334 | public function getCompilationDirectory() |
335 | { |
336 | if (null === $this->compilationDirectory) { |
337 | $this->initCompilationDirectory(); |
338 | } |
339 | return $this->compilationDirectory; |
340 | } |
341 | |
342 | /** |
343 | * Gets the meta data about the test definition being executed. |
344 | * @return array |
345 | * @throws common_Exception |
346 | */ |
347 | public function getTestMeta() |
348 | { |
349 | if (!isset($this->testMeta)) { |
350 | $directories = $this->getCompilationDirectory(); |
351 | |
352 | /** @var CompilationDataService $compilationDataService */ |
353 | $compilationDataService = $this->getServiceLocator()->get(CompilationDataService::SERVICE_ID); |
354 | $this->testMeta = $compilationDataService->readCompilationMetadata($directories['private']); |
355 | } |
356 | |
357 | return $this->testMeta; |
358 | } |
359 | |
360 | public function getTestCompilationVersion(): int |
361 | { |
362 | return $this->getTestMeta()[taoQtiTest_helpers_TestCompilerUtils::COMPILATION_VERSION] ?? 0; |
363 | } |
364 | |
365 | /** |
366 | * Gets the URI of the assessment test |
367 | * @return string |
368 | */ |
369 | public function getTestDefinitionUri() |
370 | { |
371 | return $this->testDefinitionUri; |
372 | } |
373 | |
374 | /** |
375 | * Gets the URI of the compiled delivery |
376 | * @return string |
377 | */ |
378 | public function getTestCompilationUri() |
379 | { |
380 | return $this->testCompilationUri; |
381 | } |
382 | |
383 | /** |
384 | * Gets the URI of the delivery execution |
385 | * @return string |
386 | */ |
387 | public function getTestExecutionUri() |
388 | { |
389 | return $this->testExecutionUri; |
390 | } |
391 | |
392 | /** |
393 | * Gets info from item index |
394 | * @param string $id |
395 | * @return mixed |
396 | * @throws common_exception_Error |
397 | */ |
398 | public function getItemIndex($id) |
399 | { |
400 | return $this->itemIndex->getItem($id, common_session_SessionManager::getSession()->getInterfaceLanguage()); |
401 | } |
402 | |
403 | |
404 | /** |
405 | * @return string |
406 | * @throws common_exception_Error |
407 | */ |
408 | public function getUserUri() |
409 | { |
410 | if ($this->userUri === null) { |
411 | $this->userUri = common_session_SessionManager::getSession()->getUserUri(); |
412 | } |
413 | return $this->userUri; |
414 | } |
415 | |
416 | /** |
417 | * @param string $userUri |
418 | */ |
419 | public function setUserUri($userUri) |
420 | { |
421 | $this->userUri = $userUri; |
422 | } |
423 | |
424 | /** |
425 | * Gets a particular value from item index |
426 | * @param string $id |
427 | * @param string $name |
428 | * @return mixed |
429 | * @throws common_exception_Error |
430 | */ |
431 | public function getItemIndexValue($id, $name) |
432 | { |
433 | return $this->itemIndex->getItemValue( |
434 | $id, |
435 | common_session_SessionManager::getSession()->getInterfaceLanguage(), |
436 | $name |
437 | ); |
438 | } |
439 | |
440 | /** |
441 | * Get Cat Engine Implementation |
442 | * |
443 | * Get the currently configured Cat Engine implementation. |
444 | * |
445 | * @param RouteItem|null $routeItem |
446 | * @return CatEngine |
447 | * |
448 | * @throws ServiceNotFoundException |
449 | * @throws CatEngineNotFoundException |
450 | * @throws common_exception_Error |
451 | */ |
452 | public function getCatEngine(RouteItem $routeItem = null) |
453 | { |
454 | $compiledDirectory = $this->getCompilationDirectory()['private']; |
455 | $adaptiveSectionMap = $this->getCatService()->getAdaptiveSectionMap($compiledDirectory); |
456 | $routeItem = $routeItem ? $routeItem : $this->getTestSession()->getRoute()->current(); |
457 | |
458 | $sectionId = $routeItem->getAssessmentSection()->getIdentifier(); |
459 | $catEngine = false; |
460 | |
461 | if (isset($adaptiveSectionMap[$sectionId])) { |
462 | $catEngine = $this->getCatService()->getEngine( |
463 | $adaptiveSectionMap[$sectionId]['endpoint'] |
464 | ); |
465 | } |
466 | |
467 | return $catEngine; |
468 | } |
469 | |
470 | /** |
471 | * @return AssessmentTestSession |
472 | * @throws common_exception_Error |
473 | */ |
474 | public function getTestSession() |
475 | { |
476 | if (!$this->testSession) { |
477 | $this->initTestSession(); |
478 | } |
479 | return parent::getTestSession(); |
480 | } |
481 | |
482 | |
483 | /** |
484 | * Get the current CAT Session Object. |
485 | * |
486 | * @param RouteItem|null $routeItem |
487 | * @return CatSession|false |
488 | * |
489 | * @throws ServiceNotFoundException |
490 | * @throws common_exception_Error |
491 | */ |
492 | public function getCatSession(RouteItem $routeItem = null) |
493 | { |
494 | return $this->getCatService()->getCatSession( |
495 | $this->getTestSession(), |
496 | $this->getCompilationDirectory()['private'], |
497 | $routeItem |
498 | ); |
499 | } |
500 | |
501 | /** |
502 | * Persist the CAT Session Data. |
503 | * |
504 | * Persist the current CAT Session Data in storage. |
505 | * |
506 | * @param string $catSession JSON encoded CAT Session data. |
507 | * @param RouteItem|null $routeItem |
508 | * @return mixed |
509 | * |
510 | * @throws ServiceNotFoundException |
511 | * @throws common_exception_Error |
512 | */ |
513 | public function persistCatSession($catSession, RouteItem $routeItem = null) |
514 | { |
515 | return $this->getCatService()->persistCatSession( |
516 | $catSession, |
517 | $this->getTestSession(), |
518 | $this->getCompilationDirectory()['private'], |
519 | $routeItem |
520 | ); |
521 | } |
522 | |
523 | /** |
524 | * Persist seen CAT Item identifiers. |
525 | * |
526 | * @param string $seenCatItemId |
527 | * |
528 | * @throws ServiceNotFoundException |
529 | * @throws common_exception_Error |
530 | */ |
531 | public function persistSeenCatItemIds($seenCatItemId) |
532 | { |
533 | $sessionId = $this->getTestSession()->getSessionId(); |
534 | $items = $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->getCatValue( |
535 | $sessionId, |
536 | $this->getCatSection()->getSectionId(), |
537 | 'cat-seen-item-ids' |
538 | ); |
539 | |
540 | if (!$items) { |
541 | $items = []; |
542 | } else { |
543 | $items = json_decode($items); |
544 | } |
545 | |
546 | if (!in_array($seenCatItemId, $items)) { |
547 | $items[] = $seenCatItemId; |
548 | } |
549 | |
550 | $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->setCatValue( |
551 | $sessionId, |
552 | $this->getCatSection()->getSectionId(), |
553 | 'cat-seen-item-ids', |
554 | json_encode($items) |
555 | ); |
556 | } |
557 | |
558 | /** |
559 | * Get Last CAT Item Output. |
560 | * |
561 | * Get the last CAT Item Result from memory. |
562 | */ |
563 | public function getLastCatItemOutput() |
564 | { |
565 | $sessionId = $this->getTestSession()->getSessionId(); |
566 | |
567 | $itemOutput = $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->getCatValue( |
568 | $sessionId, |
569 | $this->getCatSection()->getSectionId(), |
570 | 'cat-item-output' |
571 | ); |
572 | |
573 | $output = []; |
574 | |
575 | if (!is_null($itemOutput)) { |
576 | $rawData = json_decode($itemOutput, true); |
577 | |
578 | foreach ($rawData as $result) { |
579 | /** @var ItemResult $itemResult */ |
580 | $itemResult = ItemResult::restore($result); |
581 | $output[$itemResult->getItemRefId()] = $itemResult; |
582 | } |
583 | } |
584 | |
585 | return $output; |
586 | } |
587 | |
588 | /** |
589 | * Persist CAT Item Output. |
590 | * |
591 | * Persist the last CAT Item Result in memory. |
592 | * |
593 | * @throws common_exception_Error |
594 | * @throws ServiceNotFoundException |
595 | */ |
596 | public function persistLastCatItemOutput(array $lastCatItemOutput) |
597 | { |
598 | $sessionId = $this->getTestSession()->getSessionId(); |
599 | |
600 | $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->setCatValue( |
601 | $sessionId, |
602 | $this->getCatSection()->getSectionId(), |
603 | 'cat-item-output', |
604 | json_encode($lastCatItemOutput) |
605 | ); |
606 | } |
607 | |
608 | /** |
609 | * Get Current CAT Section. |
610 | * |
611 | * Returns the current CatSection object. In case of the current Assessment Section is not adaptive, the method |
612 | * returns the boolean false value. |
613 | * |
614 | * @param RouteItem|null $routeItem |
615 | * @return CatSection|boolean |
616 | * |
617 | * @throws ServiceNotFoundException |
618 | * @throws common_exception_Error |
619 | */ |
620 | public function getCatSection(RouteItem $routeItem = null) |
621 | { |
622 | return $this->getCatService()->getCatSection( |
623 | $this->getTestSession(), |
624 | $this->getCompilationDirectory()['private'], |
625 | $routeItem |
626 | ); |
627 | } |
628 | |
629 | /** |
630 | * Is the Assessment Test Session Context Adaptive. |
631 | * |
632 | * Determines whether the current Assessment Test Session is in an adaptive context. |
633 | * |
634 | * @param ?AssessmentItemRef $currentAssessmentItemRef An AssessmentItemRef object to be considered as |
635 | * the current assessmentItemRef. |
636 | * @return boolean |
637 | * |
638 | * @throws common_exception_Error |
639 | * @throws ServiceNotFoundException |
640 | */ |
641 | public function isAdaptive(AssessmentItemRef $currentAssessmentItemRef = null) |
642 | { |
643 | return $this->getCatService()->isAdaptive( |
644 | $this->getTestSession(), |
645 | $currentAssessmentItemRef |
646 | ); |
647 | } |
648 | |
649 | /** |
650 | * Contains Adaptive Content. |
651 | * |
652 | * Whether the current Assessment Test Session has some adaptive contents. |
653 | * |
654 | * @return boolean |
655 | * |
656 | * @throws ServiceNotFoundException |
657 | */ |
658 | public function containsAdaptive() |
659 | { |
660 | $adaptiveSectionMap = $this->getCatService()->getAdaptiveSectionMap( |
661 | $this->getCompilationDirectory()['private'] |
662 | ); |
663 | |
664 | return !empty($adaptiveSectionMap); |
665 | } |
666 | |
667 | /** |
668 | * Select the next Adaptive Item and store the retrieved results from CAT engine |
669 | * |
670 | * Ask the CAT Engine for the Next Item to be presented to the candidate, depending on the last |
671 | * CAT Item ID and last CAT Item Output currently stored. |
672 | * |
673 | * This method returns a CAT Item ID in case of the CAT Engine returned one. Otherwise, it returns |
674 | * null meaning that there is no CAT Item to be presented. |
675 | * |
676 | * @return mixed|null |
677 | * |
678 | * @throws common_Exception |
679 | * @throws ServiceNotFoundException |
680 | */ |
681 | public function selectAdaptiveNextItem() |
682 | { |
683 | $lastItemId = $this->getCurrentCatItemId(); |
684 | $lastOutput = $this->getLastCatItemOutput(); |
685 | $catSession = $this->getCatSession(); |
686 | |
687 | $preSelection = $catSession->getTestMap(); |
688 | |
689 | try { |
690 | if (!$this->syncingMode) { |
691 | $selection = $catSession->getTestMap(array_values($lastOutput)); |
692 | |
693 | if (!$this->saveAdaptiveResults($catSession)) { |
694 | common_Logger::w('Unable to save CatService results.'); |
695 | } |
696 | $isShadowItem = false; |
697 | } else { |
698 | $selection = $catSession->getTestMap(); |
699 | $isShadowItem = true; |
700 | } |
701 | } catch (CatEngineException $e) { |
702 | common_Logger::e('Error during CatEngine processing. ' . $e->getMessage()); |
703 | $selection = $catSession->getTestMap(); |
704 | $isShadowItem = true; |
705 | } |
706 | |
707 | $event = new SelectAdaptiveNextItemEvent( |
708 | $this->getTestSession(), |
709 | $lastItemId, |
710 | $preSelection, |
711 | $selection, |
712 | $isShadowItem |
713 | ); |
714 | $this->getServiceManager()->get(EventManager::SERVICE_ID)->trigger($event); |
715 | |
716 | $this->persistCatSession($catSession); |
717 | if (is_array($selection) && count($selection) > 0) { |
718 | common_Logger::d("New CAT item selection is '" . implode(', ', $selection) . "'."); |
719 | return $selection[0]; |
720 | } else { |
721 | common_Logger::d('No new CAT item selection.'); |
722 | return null; |
723 | } |
724 | } |
725 | |
726 | /** |
727 | * Get Current AssessmentItemRef object. |
728 | * |
729 | * This method returns the current AssessmentItemRef object depending on the test $context. |
730 | * |
731 | * @return ExtendedAssessmentItemRef|false |
732 | * |
733 | * @throws ServiceNotFoundException |
734 | * @throws common_exception_Error |
735 | */ |
736 | public function getCurrentAssessmentItemRef() |
737 | { |
738 | if ($this->isAdaptive()) { |
739 | return $this->getCatService()->getAssessmentItemRefByIdentifier( |
740 | $this->getCompilationDirectory()['private'], |
741 | $this->getCurrentCatItemId() |
742 | ); |
743 | } else { |
744 | return $this->getTestSession()->getCurrentAssessmentItemRef(); |
745 | } |
746 | } |
747 | |
748 | /** |
749 | * @return array |
750 | * |
751 | * @throws ServiceNotFoundException |
752 | * @throws common_exception_Error |
753 | */ |
754 | public function getPreviouslySeenCatItemIds(RouteItem $routeItem = null) |
755 | { |
756 | return $this->getCatService()->getPreviouslySeenCatItemIds( |
757 | $this->getTestSession(), |
758 | $this->getCompilationDirectory()['private'], |
759 | $routeItem |
760 | ); |
761 | } |
762 | |
763 | |
764 | /** |
765 | * @return array |
766 | * |
767 | * @throws ServiceNotFoundException |
768 | * @throws common_exception_Error |
769 | */ |
770 | public function getShadowTest(RouteItem $routeItem = null) |
771 | { |
772 | return $this->getCatService()->getShadowTest( |
773 | $this->getTestSession(), |
774 | $this->getCompilationDirectory()['private'], |
775 | $routeItem |
776 | ); |
777 | } |
778 | |
779 | /** |
780 | * @return mixed |
781 | * |
782 | * @throws ServiceNotFoundException |
783 | * @throws common_exception_Error |
784 | */ |
785 | public function getCurrentCatItemId(RouteItem $routeItem = null) |
786 | { |
787 | return $this->getCatService()->getCurrentCatItemId( |
788 | $this->getTestSession(), |
789 | $this->getCompilationDirectory()['private'], |
790 | $routeItem |
791 | ); |
792 | } |
793 | |
794 | /** |
795 | * @return void |
796 | * |
797 | * @throws ServiceNotFoundException |
798 | * @throws common_exception_Error |
799 | */ |
800 | public function persistCurrentCatItemId($catItemId) |
801 | { |
802 | $session = $this->getTestSession(); |
803 | $sessionId = $session->getSessionId(); |
804 | $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->setCatValue( |
805 | $sessionId, |
806 | $this->getCatSection()->getSectionId(), |
807 | 'current-cat-item-id', |
808 | $catItemId |
809 | ); |
810 | |
811 | $event = new QtiTestChangeEvent($session, new TestSessionMemento($session)); |
812 | $this->getServiceManager()->propagate($event); |
813 | $this->getEventManager()->trigger($event); |
814 | } |
815 | |
816 | /** |
817 | * @return int |
818 | * |
819 | * @throws ServiceNotFoundException |
820 | * @throws common_exception_Error |
821 | */ |
822 | public function getItemPositionInRoute($refId, &$catItemId = '') |
823 | { |
824 | $route = $this->getTestSession()->getRoute(); |
825 | $routeCount = $route->count(); |
826 | |
827 | $i = 0; |
828 | $j = 0; |
829 | |
830 | while ($i < $routeCount) { |
831 | $routeItem = $route->getRouteItemAt($i); |
832 | |
833 | if ($this->isAdaptive($routeItem->getAssessmentItemRef())) { |
834 | $shadow = $this->getShadowTest($routeItem); |
835 | |
836 | for ($k = 0; $k < count($shadow); $k++) { |
837 | if ($j == $refId) { |
838 | $catItemId = $shadow[$k]; |
839 | break 2; |
840 | } |
841 | |
842 | $j++; |
843 | } |
844 | } else { |
845 | if ($j == $refId) { |
846 | break; |
847 | } |
848 | |
849 | $j++; |
850 | } |
851 | |
852 | $i++; |
853 | } |
854 | |
855 | return $i; |
856 | } |
857 | |
858 | /** |
859 | * Get Real Current Position. |
860 | * |
861 | * This method returns the real position of the test taker within |
862 | * the item flow, by considering CAT sections. |
863 | * |
864 | * @return integer A zero-based index. |
865 | * |
866 | * @throws common_exception_Error |
867 | * @throws ServiceNotFoundException |
868 | */ |
869 | public function getCurrentPosition() |
870 | { |
871 | $route = $this->getTestSession()->getRoute(); |
872 | $routeCount = $route->count(); |
873 | $routeItemPosition = $route->getPosition(); |
874 | $currentRouteItem = $route->getRouteItemAt($routeItemPosition); |
875 | |
876 | $finalPosition = 0; |
877 | |
878 | for ($i = 0; $i < $routeCount; $i++) { |
879 | $routeItem = $route->getRouteItemAt($i); |
880 | |
881 | if ($routeItem !== $currentRouteItem) { |
882 | if (!$this->isAdaptive($routeItem->getAssessmentItemRef())) { |
883 | $finalPosition++; |
884 | } else { |
885 | $finalPosition += count($this->getShadowTest($routeItem)); |
886 | } |
887 | } else { |
888 | if ($this->isAdaptive($routeItem->getAssessmentItemRef())) { |
889 | $finalPosition += array_search( |
890 | $this->getCurrentCatItemId($routeItem), |
891 | $this->getShadowTest($routeItem) |
892 | ); |
893 | } |
894 | |
895 | break; |
896 | } |
897 | } |
898 | |
899 | return $finalPosition; |
900 | } |
901 | |
902 | /** |
903 | * @return int|mixed |
904 | * |
905 | * @throws ServiceNotFoundException |
906 | * @throws common_exception_Error |
907 | */ |
908 | public function getCatAttempts($identifier, RouteItem $routeItem = null) |
909 | { |
910 | return $this->getCatService()->getCatAttempts( |
911 | $this->getTestSession(), |
912 | $this->getCompilationDirectory()['private'], |
913 | $identifier, |
914 | $routeItem |
915 | ); |
916 | } |
917 | |
918 | /** |
919 | * @return void |
920 | * |
921 | * @throws ServiceNotFoundException |
922 | * @throws common_exception_Error |
923 | */ |
924 | public function persistCatAttempts($identifier, $attempts) |
925 | { |
926 | $sessionId = $this->getTestSession()->getSessionId(); |
927 | $sectionId = $this->getCatSection()->getSectionId(); |
928 | |
929 | $catAttempts = $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->getCatValue( |
930 | $sessionId, |
931 | $sectionId, |
932 | 'cat-attempts' |
933 | ); |
934 | |
935 | $catAttempts = ($catAttempts) ? $catAttempts : []; |
936 | $catAttempts[$identifier] = $attempts; |
937 | |
938 | $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->setCatValue( |
939 | $sessionId, |
940 | $sectionId, |
941 | 'cat-attempts', |
942 | $catAttempts |
943 | ); |
944 | } |
945 | |
946 | /** |
947 | * Can Move Backward |
948 | * |
949 | * Whether the Test Taker is able to navigate backward. |
950 | * This implementation takes the CAT sections into consideration. |
951 | * |
952 | * @return boolean |
953 | * @throws AssessmentTestSessionException |
954 | * |
955 | * @throws common_exception_Error |
956 | * @throws ServiceNotFoundException |
957 | */ |
958 | public function canMoveBackward() |
959 | { |
960 | $moveBack = false; |
961 | $session = $this->getTestSession(); |
962 | if ($this->isAdaptive()) { |
963 | $positionInCatSession = array_search( |
964 | $this->getCurrentCatItemId(), |
965 | $this->getShadowTest() |
966 | ); |
967 | |
968 | if ($positionInCatSession === 0) { |
969 | // First item in cat section. |
970 | if ($session->getRoute()->getPosition() !== 0) { |
971 | $testPart = $session->getPreviousRouteItem()->getTestPart(); |
972 | $moveBack = $testPart->getNavigationMode() === NavigationMode::NONLINEAR; |
973 | } |
974 | } else { |
975 | $testPart = $session->getRoute()->current()->getTestPart(); |
976 | $moveBack = $testPart->getNavigationMode() === NavigationMode::NONLINEAR; |
977 | } |
978 | } else { |
979 | $moveBack = $session->canMoveBackward(); |
980 | |
981 | // Check also if the sectionPause prevents you from moving backward |
982 | if ($moveBack) { |
983 | $moveBack = $this->getSectionPauseService()->canMoveBackward($session); |
984 | } |
985 | } |
986 | |
987 | return $moveBack; |
988 | } |
989 | |
990 | /** |
991 | * Save the Cat service result for tests and items |
992 | * |
993 | * @param CatSession $catSession |
994 | * @return bool |
995 | * |
996 | * @throws ServiceNotFoundException |
997 | */ |
998 | protected function saveAdaptiveResults(CatSession $catSession) |
999 | { |
1000 | $testResult = $catSession->getTestResult(); |
1001 | $testResult = empty($testResult) ? [] : [$testResult]; |
1002 | return $this->storeResult(array_merge($testResult, $catSession->getItemResults())); |
1003 | } |
1004 | |
1005 | /** |
1006 | * Store a Cat Result variable |
1007 | * |
1008 | * The result has to be an ItemResult and TestResult to embed CAT variables |
1009 | * After converted them to taoResultServer variables |
1010 | * Use the runner service to store the variables |
1011 | * |
1012 | * @param AbstractResult[] $results |
1013 | * @return bool |
1014 | * @throws ServiceNotFoundException |
1015 | */ |
1016 | protected function storeResult(array $results) |
1017 | { |
1018 | /** @var QtiRunnerService $runnerService */ |
1019 | $runnerService = $this->getServiceLocator()->get(QtiRunnerService::SERVICE_ID); |
1020 | |
1021 | $success = true; |
1022 | try { |
1023 | foreach ($results as $result) { |
1024 | if (!$result instanceof AbstractResult) { |
1025 | throw new common_Exception(__FUNCTION__ . ' requires a CAT result to store it.'); |
1026 | } |
1027 | |
1028 | $variables = $this->convertCatVariables($result->getVariables()); |
1029 | if (empty($variables)) { |
1030 | common_Logger::t('No Cat result variables to store.'); |
1031 | continue; |
1032 | } |
1033 | |
1034 | if ($result instanceof ItemResult) { |
1035 | $itemId = $result->getItemRefId(); |
1036 | $itemUri = $this->getItemUriFromRefId($itemId); |
1037 | } else { |
1038 | $itemUri = $itemId = null; |
1039 | $sectionId = $this |
1040 | ->getTestSession() |
1041 | ->getRoute() |
1042 | ->current() |
1043 | ->getAssessmentSection() |
1044 | ->getIdentifier(); |
1045 | foreach ($variables as $variable) { |
1046 | /** @var taoResultServer_models_classes_Variable $variable */ |
1047 | $variable->setIdentifier($sectionId . '-' . $variable->getIdentifier()); |
1048 | } |
1049 | } |
1050 | |
1051 | if (!$runnerService->storeVariables($this, $itemUri, $variables, $itemId)) { |
1052 | $success = false; |
1053 | } |
1054 | } |
1055 | } catch (Exception $e) { |
1056 | common_Logger::w('An error has occurred during CAT result storing: ' . $e->getMessage()); |
1057 | $success = false; |
1058 | } |
1059 | |
1060 | return $success; |
1061 | } |
1062 | |
1063 | /** |
1064 | * Convert CAT variables to taoResultServer variables |
1065 | * |
1066 | * Following the variable type, use the Runner service to get the appropriate variable |
1067 | * The method manage the trace, response and outcome variable |
1068 | * |
1069 | * @param array $variables |
1070 | * @return array |
1071 | * @throws common_exception_NotImplemented If variable type is not managed |
1072 | */ |
1073 | protected function convertCatVariables(array $variables) |
1074 | { |
1075 | /** @var QtiRunnerService $runnerService */ |
1076 | $runnerService = $this->getServiceLocator()->get(QtiRunnerService::SERVICE_ID); |
1077 | $convertedVariables = []; |
1078 | |
1079 | foreach ($variables as $variable) { |
1080 | switch ($variable->getVariableType()) { |
1081 | case ResultVariable::TRACE_VARIABLE: |
1082 | $getVariableMethod = 'getTraceVariable'; |
1083 | break; |
1084 | case ResultVariable::RESPONSE_VARIABLE: |
1085 | $getVariableMethod = 'getResponseVariable'; |
1086 | break; |
1087 | case ResultVariable::OUTCOME_VARIABLE: |
1088 | $getVariableMethod = 'getOutcomeVariable'; |
1089 | break; |
1090 | case ResultVariable::TEMPLATE_VARIABLE: |
1091 | default: |
1092 | $getVariableMethod = null; |
1093 | break; |
1094 | } |
1095 | |
1096 | if (is_null($getVariableMethod)) { |
1097 | common_Logger::w( |
1098 | 'Variable of type ' . $variable->getVariableType() . ' is not implemented in ' . __METHOD__ |
1099 | ); |
1100 | throw new common_exception_NotImplemented(); |
1101 | } |
1102 | |
1103 | $convertedVariables[] = call_user_func_array( |
1104 | [$runnerService, $getVariableMethod], |
1105 | [$variable->getId(), $variable->getValue()] |
1106 | ); |
1107 | } |
1108 | |
1109 | return $convertedVariables; |
1110 | } |
1111 | |
1112 | /** |
1113 | * Get item uri associated to the given $itemId. |
1114 | * |
1115 | * @return string The uri |
1116 | * @throws ServiceNotFoundException |
1117 | */ |
1118 | protected function getItemUriFromRefId($itemId) |
1119 | { |
1120 | $ref = $this->getCatService()->getAssessmentItemRefByIdentifier( |
1121 | $this->getCompilationDirectory()['private'], |
1122 | $itemId |
1123 | ); |
1124 | return explode('|', $ref->getHref())[0]; |
1125 | } |
1126 | |
1127 | /** |
1128 | * Are we in a synchronization mode |
1129 | * @return bool |
1130 | */ |
1131 | public function isSyncingMode() |
1132 | { |
1133 | return $this->syncingMode; |
1134 | } |
1135 | |
1136 | /** |
1137 | * Set/Unset the synchronization mode |
1138 | * @param bool $syncing |
1139 | */ |
1140 | public function setSyncingMode($syncing) |
1141 | { |
1142 | $this->syncingMode = (bool) $syncing; |
1143 | } |
1144 | |
1145 | /** |
1146 | * @return User |
1147 | * @throws common_exception_Error |
1148 | */ |
1149 | private function getTestTakerFromSessionOrRds() |
1150 | { |
1151 | try { |
1152 | $session = common_session_SessionManager::getSession(); |
1153 | } catch (common_exception_Error $exception) { |
1154 | $session = null; |
1155 | common_Logger::w($exception->getMessage()); |
1156 | } |
1157 | |
1158 | if ($session == null || $session->getUser() == null) { |
1159 | $testTaker = UserHelper::getUser($this->getUserUri()); |
1160 | } else { |
1161 | $testTaker = $session->getUser(); |
1162 | } |
1163 | |
1164 | return $testTaker; |
1165 | } |
1166 | |
1167 | /** |
1168 | * @throws ServiceNotFoundException |
1169 | */ |
1170 | private function getSectionPauseService(): SectionPauseService |
1171 | { |
1172 | return $this->getServiceManager()->get(SectionPauseService::SERVICE_ID); |
1173 | } |
1174 | |
1175 | /** |
1176 | * @throws ServiceNotFoundException |
1177 | */ |
1178 | private function getCatService(): CatService |
1179 | { |
1180 | return $this->getServiceManager()->get(CatService::SERVICE_ID); |
1181 | } |
1182 | } |