Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 276 |
|
0.00% |
0 / 24 |
CRAP | |
0.00% |
0 / 1 |
CatService | |
0.00% |
0 / 276 |
|
0.00% |
0 / 24 |
6480 | |
0.00% |
0 / 1 |
getEngine | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
56 | |||
getAssessmentItemRefByIdentifier | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
getAssessmentItemRefByIdentifiers | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getAssessmentItemRefsByPlaceholder | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
getAdaptiveAssessmentSectionInfo | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
42 | |||
getAdaptiveSectionMap | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
importCatSectionIdsToRdfTest | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
30 | |||
createAdaptiveSection | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
validateAdaptiveAssessmentSection | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
20 | |||
isAssessmentSectionAdaptive | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
isAdaptivePlaceholder | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
onQtiContinueInteraction | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
getCatEngineClient | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
72 | |||
getCatEngineVersion | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
isAdaptive | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getCatSection | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 | |||
getCatEngine | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
getPreviouslySeenCatItemIds | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
getShadowTest | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
getCatSession | |
0.00% |
0 / 39 |
|
0.00% |
0 / 1 |
30 | |||
persistCatSession | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
getCurrentCatItemId | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
getCatAttempts | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
alterTimeoutCallValue | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
30 |
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 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT); |
19 | * |
20 | */ |
21 | |
22 | namespace oat\taoQtiTest\models\cat; |
23 | |
24 | use GuzzleHttp\ClientInterface; |
25 | use oat\oatbox\service\ConfigurableService; |
26 | use oat\generis\model\OntologyAwareTrait; |
27 | use oat\libCat\CatEngine; |
28 | use oat\taoQtiTest\models\event\QtiContinueInteractionEvent; |
29 | use qtism\data\AssessmentTest; |
30 | use qtism\data\AssessmentSection; |
31 | use qtism\data\SectionPartCollection; |
32 | use qtism\data\AssessmentItemRef; |
33 | use qtism\data\storage\php\PhpDocument; |
34 | use qtism\runtime\tests\AssessmentTestSession; |
35 | use qtism\runtime\tests\RouteItem; |
36 | use oat\taoQtiTest\models\ExtendedStateService; |
37 | use oat\oatbox\event\EventManager; |
38 | use oat\taoQtiTest\models\event\InitializeAdaptiveSessionEvent; |
39 | use oat\taoQtiTest\models\CompilationDataService; |
40 | |
41 | /** |
42 | * Computerized Adaptive Testing Service |
43 | * |
44 | * This Service gives you access to a CatEngine object in addition |
45 | * with relevant services to deal with CAT in TAO. |
46 | * |
47 | * @access public |
48 | * @author Joel Bout, <joel@taotesting.com> |
49 | * @package taoDelivery |
50 | */ |
51 | class CatService extends ConfigurableService |
52 | { |
53 | use OntologyAwareTrait; |
54 | |
55 | public const SERVICE_ID = 'taoQtiTest/CatService'; |
56 | |
57 | public const OPTION_ENGINE_ENDPOINTS = 'endpoints'; |
58 | |
59 | public const OPTION_ENGINE_URL = 'url'; |
60 | |
61 | public const OPTION_ENGINE_CLASS = 'class'; |
62 | |
63 | public const OPTION_ENGINE_ARGS = 'args'; |
64 | |
65 | public const OPTION_ENGINE_VERSION = 'version'; |
66 | |
67 | public const OPTION_ENGINE_CLIENT = 'client'; |
68 | |
69 | public const OPTION_INITIAL_CALL_TIMEOUT = 'initialCallTimeout'; |
70 | |
71 | public const OPTION_NEXT_ITEM_CALL_TIMEOUT = 'nextItemCallTimeout'; |
72 | |
73 | public const QTI_2X_ADAPTIVE_XML_NAMESPACE = 'http://www.taotesting.com/xsd/ais_v1p0p0'; |
74 | |
75 | public const CAT_ADAPTIVE_IDS_PROPERTY = 'http://www.tao.lu/Ontologies/TAOTest.rdf#QtiCatAdaptiveSections'; |
76 | |
77 | public const IS_CAT_ADAPTIVE = 'is-cat-adaptive'; |
78 | |
79 | public const IS_SHADOW_ITEM = 'is-shadow-item'; |
80 | |
81 | private $engines = []; |
82 | |
83 | private $sectionMapCache = []; |
84 | |
85 | private $catSection = []; |
86 | |
87 | private $catSession = []; |
88 | |
89 | protected $isInitialCall = false; |
90 | |
91 | /** |
92 | * Returns the Adaptive Engine |
93 | * |
94 | * Returns an CatEngine implementation object. |
95 | * If it is the initial call, change endpoint name to differentiate it from nextItem call |
96 | * |
97 | * @param string $endpoint |
98 | * @return CatEngine |
99 | * @throws CatEngineNotFoundException |
100 | */ |
101 | public function getEngine($endpoint) |
102 | { |
103 | if ($this->isInitialCall == true) { |
104 | $endpointCached = $endpoint . '-init'; |
105 | } else { |
106 | $endpointCached = $endpoint; |
107 | } |
108 | |
109 | if (!isset($this->engines[$endpointCached])) { |
110 | $endPoints = $this->getOption(self::OPTION_ENGINE_ENDPOINTS); |
111 | |
112 | if (!empty($endPoints[$endpoint])) { |
113 | $engineOptions = $endPoints[$endpoint]; |
114 | |
115 | $class = $engineOptions[self::OPTION_ENGINE_CLASS]; |
116 | $args = $engineOptions[self::OPTION_ENGINE_ARGS]; |
117 | $args = $this->alterTimeoutCallValue($args); |
118 | $url = isset($engineOptions[self::OPTION_ENGINE_URL]) |
119 | ? $engineOptions[self::OPTION_ENGINE_URL] |
120 | : $endpoint; |
121 | array_unshift($args, $endpoint); |
122 | |
123 | try { |
124 | $this->engines[$endpointCached] = new $class( |
125 | $url, |
126 | $this->getCatEngineVersion($args), |
127 | $this->getCatEngineClient($args) |
128 | ); |
129 | } catch (\Exception $e) { |
130 | \common_Logger::e('Fail to connect to CAT endpoint : ' . $e->getMessage()); |
131 | throw new CatEngineNotFoundException( |
132 | 'CAT Engine for endpoint "' . $endpoint . '" is misconfigured.', |
133 | $endpoint, |
134 | 0, |
135 | $e |
136 | ); |
137 | } |
138 | } |
139 | } |
140 | |
141 | if (empty($this->engines[$endpointCached])) { |
142 | // No configured endpoint found. |
143 | throw new CatEngineNotFoundException("CAT Engine for endpoint '${endpoint}' is not configured.", $endpoint); |
144 | } |
145 | |
146 | return $this->engines[$endpointCached]; |
147 | } |
148 | |
149 | /** |
150 | * Get AssessmentItemRef by Identifier |
151 | * |
152 | * This method enables you to access to a pre-compiled version of a stand alone AssessmentItemRef, that can be run |
153 | * with a stand alone AssessmentItemSession. |
154 | * |
155 | * @return \qtism\data\ExtendedAssessmentItemRef |
156 | */ |
157 | public function getAssessmentItemRefByIdentifier( |
158 | \tao_models_classes_service_StorageDirectory $privateCompilationDirectory, |
159 | $identifier |
160 | ) { |
161 | $compilationDataService = $this->getServiceLocator()->get(CompilationDataService::SERVICE_ID); |
162 | $filename = "adaptive-assessment-item-ref-${identifier}"; |
163 | |
164 | return $compilationDataService->readCompilationData( |
165 | $privateCompilationDirectory, |
166 | $filename, |
167 | $filename |
168 | ); |
169 | } |
170 | |
171 | /** |
172 | * Get AssessmentItemRef by Identifiers |
173 | * |
174 | * This method enables you to access to a collection of pre-compiled versions of stand alone AssessmentItemRef |
175 | * objects, that can be run with stand alone AssessmentItemSessions. |
176 | * |
177 | * @return array An array of AssessmentItemRef objects. |
178 | */ |
179 | public function getAssessmentItemRefByIdentifiers( |
180 | \tao_models_classes_service_StorageDirectory $privateCompilationDirectory, |
181 | array $identifiers |
182 | ) { |
183 | $assessmentItemRefs = []; |
184 | |
185 | foreach ($identifiers as $identifier) { |
186 | $assessmentItemRefs[] = $this->getAssessmentItemRefByIdentifier($privateCompilationDirectory, $identifier); |
187 | } |
188 | |
189 | return $assessmentItemRefs; |
190 | } |
191 | |
192 | /** |
193 | * Get AssessmentItemRefs corresponding to a given Adaptive Placeholder. |
194 | * |
195 | * This method will return an array of AssessmentItemRef objects corresponding to an Adaptive Placeholder. |
196 | * |
197 | * @return array |
198 | */ |
199 | public function getAssessmentItemRefsByPlaceholder( |
200 | \tao_models_classes_service_StorageDirectory $privateCompilationDirectory, |
201 | AssessmentItemRef $placeholder |
202 | ) { |
203 | $urlinfo = parse_url($placeholder->getHref()); |
204 | $adaptiveSectionId = ltrim($urlinfo['path'], '/'); |
205 | |
206 | $compilationDataService = $this->getServiceLocator()->get(CompilationDataService::SERVICE_ID); |
207 | $filename = "adaptive-assessment-section-${adaptiveSectionId}"; |
208 | |
209 | $component = $compilationDataService->readCompilationData( |
210 | $privateCompilationDirectory, |
211 | $filename, |
212 | $filename |
213 | ); |
214 | |
215 | return $component->getComponentsByClassName('assessmentItemRef')->getArrayCopy(); |
216 | } |
217 | |
218 | /** |
219 | * Get Information about a given Adaptive Section. |
220 | * |
221 | * This method returns Information about the "adaptivity" of a given QTI AssessmentSection. |
222 | * The method returns an associative array containing the following information: |
223 | * |
224 | * * 'qtiSectionIdentifier' => The original QTI Identifier of the section. |
225 | * * 'adaptiveSectionIdentifier' => The identifier of the adaptive section as known by the Adaptive Engine. |
226 | * * 'adaptiveEngineRef' => The URL to the Adaptive Engine End Point to be used for that Adaptive Section. |
227 | * |
228 | * In case of the Assessment Section is not adaptive, the method returns false. |
229 | * |
230 | * @param \qtism\data\AssessmentTest $test A given AssessmentTest object. |
231 | * @param \tao_models_classes_service_StorageDirectory $compilationDirectory The compilation directory where the |
232 | * test is compiled as a TAO Delivery. |
233 | * @param string $qtiAssessmentSectionIdentifier The QTI identifier of the AssessmentSection you would like to get |
234 | * "adaptivity" information. |
235 | * @return array|boolean Some "adaptivity" information or false in case of the given $qtiAssessmentSectionIdentifier |
236 | * does not correspond to an adaptive Assessment Section. |
237 | */ |
238 | public function getAdaptiveAssessmentSectionInfo( |
239 | AssessmentTest $test, |
240 | \tao_models_classes_service_StorageDirectory $compilationDirectory, |
241 | $basePath, |
242 | $qtiAssessmentSectionIdentifier |
243 | ) { |
244 | $info = CatUtils::getCatInfo($test); |
245 | $adaptiveInfo = [ |
246 | 'qtiSectionIdentifier' => $qtiAssessmentSectionIdentifier, |
247 | 'adaptiveSectionIdentifier' => false, |
248 | 'adaptiveEngineRef' => false |
249 | ]; |
250 | |
251 | if (isset($info[$qtiAssessmentSectionIdentifier])) { |
252 | if (isset($info[$qtiAssessmentSectionIdentifier]['adaptiveEngineRef'])) { |
253 | $adaptiveInfo['adaptiveEngineRef'] = $info[$qtiAssessmentSectionIdentifier]['adaptiveEngineRef']; |
254 | } |
255 | |
256 | if (isset($info[$qtiAssessmentSectionIdentifier]['adaptiveSettingsRef'])) { |
257 | $adaptiveInfo['adaptiveSectionIdentifier'] = trim( |
258 | $compilationDirectory->read( |
259 | "./${basePath}/" . $info[$qtiAssessmentSectionIdentifier]['adaptiveSettingsRef'] |
260 | ) |
261 | ); |
262 | } |
263 | } |
264 | |
265 | return (!isset($info[$qtiAssessmentSectionIdentifier]['adaptiveEngineRef']) |
266 | || !isset($info[$qtiAssessmentSectionIdentifier]['adaptiveSettingsRef'])) |
267 | ? false |
268 | : $adaptiveInfo; |
269 | } |
270 | |
271 | public function getAdaptiveSectionMap(\tao_models_classes_service_StorageDirectory $privateCompilationDirectory) |
272 | { |
273 | $dirId = $privateCompilationDirectory->getId(); |
274 | |
275 | if (!isset($this->sectionMapCache[$dirId])) { |
276 | $file = $privateCompilationDirectory->getFile( |
277 | \taoQtiTest_models_classes_QtiTestCompiler::ADAPTIVE_SECTION_MAP_FILENAME |
278 | ); |
279 | $sectionMap = $file->exists() ? json_decode($file->read(), true) : []; |
280 | $this->sectionMapCache[$dirId] = $sectionMap; |
281 | } |
282 | |
283 | return $this->sectionMapCache[$dirId]; |
284 | } |
285 | |
286 | /** |
287 | * Import XML data to QTI test RDF properties. |
288 | * |
289 | * This method will import the information found in the CAT specific information of adaptive sections |
290 | * of a QTI test into the ontology for a given $test. This method is designed to be called at QTI Test Import time. |
291 | * |
292 | * @param \core_kernel_classes_Resource $testResource |
293 | * @param \qtism\data\AssessmentTest $testDefinition |
294 | * @param string $localTestPath The path to the related QTI Test Definition file (XML) during import. |
295 | * @return bool |
296 | * @throws \common_Exception In case of error. |
297 | */ |
298 | public function importCatSectionIdsToRdfTest( |
299 | \core_kernel_classes_Resource $testResource, |
300 | AssessmentTest $testDefinition, |
301 | $localTestPath |
302 | ) { |
303 | $testUri = $testResource->getUri(); |
304 | $catProperties = []; |
305 | $assessmentSections = $testDefinition->getComponentsByClassName('assessmentSection', true); |
306 | $catInfo = CatUtils::getCatInfo($testDefinition); |
307 | $testBasePath = pathinfo($localTestPath, PATHINFO_DIRNAME); |
308 | |
309 | /** @var AssessmentSection $assessmentSection */ |
310 | foreach ($assessmentSections as $assessmentSection) { |
311 | $assessmentSectionIdentifier = $assessmentSection->getIdentifier(); |
312 | |
313 | if (isset($catInfo[$assessmentSectionIdentifier])) { |
314 | $settingsPath = "${testBasePath}/" . $catInfo[$assessmentSectionIdentifier]['adaptiveSettingsRef']; |
315 | $settingsContent = trim(file_get_contents($settingsPath)); |
316 | $catProperties[$assessmentSectionIdentifier] = $settingsContent; |
317 | |
318 | $this->createAdaptiveSection($assessmentSection, $catInfo, $testBasePath); |
319 | |
320 | $this->validateAdaptiveAssessmentSection( |
321 | $assessmentSection->getSectionParts(), |
322 | $catInfo[$assessmentSectionIdentifier]['adaptiveEngineRef'], |
323 | $settingsContent |
324 | ); |
325 | } |
326 | } |
327 | |
328 | if (empty($catProperties)) { |
329 | \common_Logger::t("No QTI CAT property value to store for test '${testUri}'."); |
330 | return true; |
331 | } |
332 | |
333 | if ( |
334 | $testResource->setPropertyValue( |
335 | $this->getProperty(self::CAT_ADAPTIVE_IDS_PROPERTY), |
336 | json_encode($catProperties) |
337 | ) |
338 | ) { |
339 | return true; |
340 | } else { |
341 | throw new \common_Exception("Unable to store CAT property value to test '${testUri}'."); |
342 | } |
343 | } |
344 | |
345 | |
346 | protected function createAdaptiveSection($assessmentSection, $catInfo, $testBasePath) |
347 | { |
348 | $assessmentSectionIdentifier = $assessmentSection->getIdentifier(); |
349 | $engine = $this->getEngine($catInfo[$assessmentSectionIdentifier]['adaptiveEngineRef']); |
350 | $settingsPath = "${testBasePath}/" . $catInfo[$assessmentSectionIdentifier]['adaptiveSettingsRef']; |
351 | |
352 | $usagedataContent = null; |
353 | if (isset($catInfo[$assessmentSectionIdentifier]['qtiUsagedataRef'])) { |
354 | $usagedataPath = "${testBasePath}/" . $catInfo[$assessmentSectionIdentifier]['qtiUsagedataRef']; |
355 | $usagedataContent = trim(file_get_contents($usagedataPath)); |
356 | } |
357 | |
358 | $metadataContent = null; |
359 | if (isset($catInfo[$assessmentSectionIdentifier]['qtiMetadataRef'])) { |
360 | $metadataPath = "${testBasePath}/" . $catInfo[$assessmentSectionIdentifier]['qtiMetadataRef']; |
361 | $metadataContent = trim(file_get_contents($metadataPath)); |
362 | } |
363 | |
364 | $settingsContent = trim(file_get_contents($settingsPath)); |
365 | $adaptSection = $engine->setupSection($settingsContent, $usagedataContent, $metadataContent); |
366 | } |
367 | |
368 | /** |
369 | * Validation for adaptive section |
370 | * @param SectionPartCollection $sectionsParts |
371 | * @param string $ref |
372 | * @param string $testAdminId |
373 | * @throws AdaptiveSectionInjectionException |
374 | */ |
375 | public function validateAdaptiveAssessmentSection(SectionPartCollection $sectionsParts, $ref, $testAdminId) |
376 | { |
377 | $engine = $this->getEngine($ref); |
378 | $adaptSection = $engine->setupSection($testAdminId); |
379 | //todo: remove this checking if tests/{getSectionId}/items will become a part of standard. |
380 | if (method_exists($adaptSection, 'getItemReferences')) { |
381 | $itemReferences = $adaptSection->getItemReferences(); |
382 | $dependencies = $sectionsParts->getKeys(); |
383 | |
384 | if ($catDiff = array_diff($dependencies, $itemReferences)) { |
385 | throw new AdaptiveSectionInjectionException( |
386 | 'Missed some CAT service items: ' . implode(', ', $catDiff), |
387 | $catDiff |
388 | ); |
389 | } |
390 | |
391 | if ($packageDiff = array_diff($dependencies, $itemReferences)) { |
392 | throw new AdaptiveSectionInjectionException( |
393 | 'Missed some package items: ' . implode(', ', $packageDiff), |
394 | $packageDiff |
395 | ); |
396 | } |
397 | } |
398 | } |
399 | |
400 | /** |
401 | * Is an AssessmentSection Adaptive? |
402 | * |
403 | * This method returns whether or not a given $section is adaptive. |
404 | * |
405 | * @param \qtism\data\AssessmentSection $section |
406 | * @return boolean |
407 | */ |
408 | public function isAssessmentSectionAdaptive(AssessmentSection $section) |
409 | { |
410 | $assessmentItemRefs = $section->getComponentsByClassName('assessmentItemRef'); |
411 | return count($assessmentItemRefs) === 1 && $this->isAdaptivePlaceholder($assessmentItemRefs[0]); |
412 | } |
413 | |
414 | /** |
415 | * Is an AssessmentItemRef an Adaptive Placeholder? |
416 | * |
417 | * This method returns whether or not a given $assessmentItemRef is a runtime adaptive placeholder. |
418 | * |
419 | * @param \qtism\data\AssessmentItemRef $assessmentItemRef |
420 | * @return boolean |
421 | */ |
422 | public function isAdaptivePlaceholder(AssessmentItemRef $assessmentItemRef) |
423 | { |
424 | return in_array( |
425 | \taoQtiTest_models_classes_QtiTestCompiler::ADAPTIVE_PLACEHOLDER_CATEGORY, |
426 | $assessmentItemRef->getCategories()->getArrayCopy() |
427 | ); |
428 | } |
429 | |
430 | /** |
431 | * @deprecated set on SelectNextAdaptiveItemEvent |
432 | */ |
433 | public function onQtiContinueInteraction($event) |
434 | { |
435 | if ($event instanceof QtiContinueInteractionEvent) { |
436 | $context = $event->getContext(); |
437 | $isAdaptive = $context->isAdaptive(); |
438 | $isCat = false; |
439 | |
440 | if ($isAdaptive) { |
441 | $isCat = true; |
442 | } |
443 | |
444 | $itemIdentifier = $event->getContext()->getCurrentAssessmentItemRef()->getIdentifier(); |
445 | $hrefParts = explode('|', $event->getRunnerService()->getItemHref($context, $itemIdentifier)); |
446 | $event->getRunnerService()->storeTraceVariable($context, $hrefParts[0], self::IS_CAT_ADAPTIVE, $isCat); |
447 | } |
448 | } |
449 | |
450 | /** |
451 | * Create the client and version, based on the entry $options. |
452 | * |
453 | * @param array $options |
454 | * @throws \common_exception_InconsistentData |
455 | */ |
456 | protected function getCatEngineClient(array $options = []) |
457 | { |
458 | if (!isset($options[self::OPTION_ENGINE_CLIENT])) { |
459 | throw new \InvalidArgumentException('No API client provided. Cannot connect to endpoint.'); |
460 | } |
461 | |
462 | $client = $options[self::OPTION_ENGINE_CLIENT]; |
463 | if (is_array($client)) { |
464 | $clientClass = isset($client['class']) ? $client['class'] : null; |
465 | $clientOptions = isset($client['options']) ? $client['options'] : []; |
466 | if (!is_a($clientClass, ClientInterface::class, true)) { |
467 | throw new \InvalidArgumentException('Client has to implement ClientInterface interface.'); |
468 | } |
469 | $client = new $clientClass($clientOptions); |
470 | } elseif (is_object($client)) { |
471 | if (!is_a($client, ClientInterface::class)) { |
472 | throw new \InvalidArgumentException('Client has to implement ClientInterface interface.'); |
473 | } |
474 | } else { |
475 | throw new \InvalidArgumentException('Client is misconfigured.'); |
476 | } |
477 | $this->propagate($client); |
478 | return $client; |
479 | } |
480 | |
481 | /** |
482 | * @param array $options |
483 | * |
484 | * @return string |
485 | */ |
486 | protected function getCatEngineVersion(array $options = []) |
487 | { |
488 | return isset($options[self::OPTION_ENGINE_VERSION]) ? $options[self::OPTION_ENGINE_VERSION] : ''; |
489 | } |
490 | |
491 | public function isAdaptive(AssessmentTestSession $testSession, AssessmentItemRef $currentAssessmentItemRef = null) |
492 | { |
493 | $currentAssessmentItemRef = (is_null($currentAssessmentItemRef)) |
494 | ? $testSession->getCurrentAssessmentItemRef() |
495 | : $currentAssessmentItemRef; |
496 | |
497 | if ($currentAssessmentItemRef) { |
498 | return $this->isAdaptivePlaceholder($currentAssessmentItemRef); |
499 | } else { |
500 | return false; |
501 | } |
502 | } |
503 | |
504 | /** |
505 | * If it is the initial call, reload cat section from $this->catSection cache |
506 | * |
507 | * @param AssessmentTestSession $testSession |
508 | * @param \tao_models_classes_service_StorageDirectory $compilationDirectory |
509 | * @param RouteItem|null $routeItem |
510 | * @return mixed |
511 | */ |
512 | public function getCatSection( |
513 | AssessmentTestSession $testSession, |
514 | \tao_models_classes_service_StorageDirectory $compilationDirectory, |
515 | RouteItem $routeItem = null |
516 | ) { |
517 | $routeItem = $routeItem ? $routeItem : $testSession->getRoute()->current(); |
518 | $sectionId = $routeItem->getAssessmentSection()->getIdentifier(); |
519 | |
520 | if (!isset($this->catSection[$sectionId]) || $this->isInitialCall === true) { |
521 | // No retrieval trial yet. |
522 | $adaptiveSectionMap = $this->getAdaptiveSectionMap($compilationDirectory); |
523 | |
524 | |
525 | if (isset($adaptiveSectionMap[$sectionId])) { |
526 | $this->catSection[$sectionId] = $this |
527 | ->getCatEngine($testSession, $compilationDirectory, $routeItem) |
528 | ->restoreSection($adaptiveSectionMap[$sectionId]['section']); |
529 | } else { |
530 | $this->catSection[$sectionId] = false; |
531 | } |
532 | } |
533 | |
534 | return $this->catSection[$sectionId]; |
535 | } |
536 | |
537 | public function getCatEngine( |
538 | AssessmentTestSession $testSession, |
539 | \tao_models_classes_service_StorageDirectory $compilationDirectory, |
540 | RouteItem $routeItem = null |
541 | ) { |
542 | $adaptiveSectionMap = $this->getAdaptiveSectionMap($compilationDirectory); |
543 | $routeItem = $routeItem ? $routeItem : $testSession->getRoute()->current(); |
544 | |
545 | $sectionId = $routeItem->getAssessmentSection()->getIdentifier(); |
546 | $catEngine = false; |
547 | |
548 | if (isset($adaptiveSectionMap[$sectionId])) { |
549 | $catEngine = $this->getEngine($adaptiveSectionMap[$sectionId]['endpoint']); |
550 | } |
551 | |
552 | return $catEngine; |
553 | } |
554 | |
555 | /** |
556 | * @param AssessmentTestSession $testSession |
557 | * @param \tao_models_classes_service_StorageDirectory $compilationDirectory |
558 | * @param RouteItem|null $routeItem |
559 | * @return array |
560 | */ |
561 | public function getPreviouslySeenCatItemIds( |
562 | AssessmentTestSession $testSession, |
563 | \tao_models_classes_service_StorageDirectory $compilationDirectory, |
564 | RouteItem $routeItem = null |
565 | ) { |
566 | $result = []; |
567 | |
568 | if ($catSection = $this->getCatSection($testSession, $compilationDirectory, $routeItem)) { |
569 | $items = $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->getCatValue( |
570 | $testSession->getSessionId(), |
571 | $catSection->getSectionId(), |
572 | 'cat-seen-item-ids' |
573 | ); |
574 | |
575 | $result = !$items ? [] : json_decode($items); |
576 | } |
577 | |
578 | return is_array($result) ? $result : []; |
579 | } |
580 | |
581 | /** |
582 | * @param AssessmentTestSession $testSession |
583 | * @param \tao_models_classes_service_StorageDirectory $compilationDirectory |
584 | * @param RouteItem|null $routeItem |
585 | * @return array |
586 | */ |
587 | public function getShadowTest( |
588 | AssessmentTestSession $testSession, |
589 | \tao_models_classes_service_StorageDirectory $compilationDirectory, |
590 | RouteItem $routeItem = null |
591 | ) { |
592 | $shadow = array_values( |
593 | array_unique( |
594 | array_merge( |
595 | $this->getPreviouslySeenCatItemIds($testSession, $compilationDirectory, $routeItem), |
596 | $this->getCatSession($testSession, $compilationDirectory, $routeItem)->getTestMap() |
597 | ) |
598 | ) |
599 | ); |
600 | |
601 | return $shadow; |
602 | } |
603 | |
604 | /** |
605 | * Get the current CAT Session Object. |
606 | * |
607 | * If it catSession from tao is not set, set the $this->isInitialCall to true |
608 | * |
609 | * @param AssessmentTestSession $testSession |
610 | * @param \tao_models_classes_service_StorageDirectory $compilationDirectory |
611 | * @param RouteItem|null $routeItem |
612 | * @return \oat\libCat\CatSession|false |
613 | */ |
614 | public function getCatSession( |
615 | AssessmentTestSession $testSession, |
616 | \tao_models_classes_service_StorageDirectory $compilationDirectory, |
617 | RouteItem $routeItem = null |
618 | ) { |
619 | if ($catSection = $this->getCatSection($testSession, $compilationDirectory, $routeItem)) { |
620 | $catSectionId = $catSection->getSectionId(); |
621 | |
622 | if (!isset($this->catSession[$catSectionId])) { |
623 | // No retrieval trial yet in the current execution context. |
624 | $this->catSession = false; |
625 | |
626 | $catSessionData = $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->getCatValue( |
627 | $testSession->getSessionId(), |
628 | $catSection->getSectionId(), |
629 | 'cat-session' |
630 | ); |
631 | |
632 | if ($catSessionData) { |
633 | // We already have something in persistence for the session, let's restore it. |
634 | $this->catSession[$catSectionId] = $catSection->restoreSession($catSessionData); |
635 | \common_Logger::d( |
636 | "CAT Session '" . $this->catSession[$catSectionId]->getTestTakerSessionId() |
637 | . "' for CAT Section '${catSectionId}' restored." |
638 | ); |
639 | } else { |
640 | // First time the session is required, let's initialize it. |
641 | $this->isInitialCall = true; |
642 | // Rebuild the catSection to be able to alter call options |
643 | $catSection = $this->getCatSection($testSession, $compilationDirectory, $routeItem); |
644 | $this->catSession[$catSectionId] = $catSection->initSession([], []); |
645 | $assessmentSection = $routeItem |
646 | ? $routeItem->getAssessmentSection() |
647 | : $testSession->getCurrentAssessmentSection(); |
648 | |
649 | $event = new InitializeAdaptiveSessionEvent( |
650 | $testSession, |
651 | $assessmentSection, |
652 | $this->catSession[$catSectionId] |
653 | ); |
654 | |
655 | $this->getServiceManager()->get(EventManager::SERVICE_ID)->trigger($event); |
656 | $this->persistCatSession( |
657 | $this->catSession[$catSectionId], |
658 | $testSession, |
659 | $compilationDirectory, |
660 | $routeItem |
661 | ); |
662 | \common_Logger::d( |
663 | "CAT Session '" . $this->catSession[$catSectionId]->getTestTakerSessionId() |
664 | . "' for CAT Section '${catSectionId}' initialized and persisted." |
665 | ); |
666 | } |
667 | } |
668 | |
669 | return $this->catSession[$catSectionId]; |
670 | } else { |
671 | return false; |
672 | } |
673 | } |
674 | |
675 | /** |
676 | * Persist the CAT Session Data. |
677 | * |
678 | * Persist the current CAT Session Data in storage. |
679 | * |
680 | * @param string $catSession JSON encoded CAT Session data. |
681 | * @param AssessmentTestSession $testSession |
682 | * @param \tao_models_classes_service_StorageDirectory $compilationDirectory |
683 | * @param RouteItem|null $routeItem |
684 | */ |
685 | public function persistCatSession( |
686 | $catSession, |
687 | AssessmentTestSession $testSession, |
688 | \tao_models_classes_service_StorageDirectory $compilationDirectory, |
689 | RouteItem $routeItem = null |
690 | ) { |
691 | if ($catSection = $this->getCatSection($testSession, $compilationDirectory, $routeItem)) { |
692 | $catSectionId = $catSection->getSectionId(); |
693 | $this->catSession[$catSectionId] = $catSession; |
694 | |
695 | $sessionId = $testSession->getSessionId(); |
696 | $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->setCatValue( |
697 | $sessionId, |
698 | $catSectionId, |
699 | 'cat-session', |
700 | json_encode($this->catSession[$catSectionId]) |
701 | ); |
702 | } |
703 | } |
704 | |
705 | public function getCurrentCatItemId( |
706 | AssessmentTestSession $testSession, |
707 | \tao_models_classes_service_StorageDirectory $compilationDirectory, |
708 | RouteItem $routeItem = null |
709 | ) { |
710 | $sessionId = $testSession->getSessionId(); |
711 | |
712 | $catItemId = $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->getCatValue( |
713 | $sessionId, |
714 | $this->getCatSection($testSession, $compilationDirectory, $routeItem)->getSectionId(), |
715 | 'current-cat-item-id' |
716 | ); |
717 | |
718 | return $catItemId; |
719 | } |
720 | |
721 | public function getCatAttempts( |
722 | AssessmentTestSession $testSession, |
723 | \tao_models_classes_service_StorageDirectory $compilationDirectory, |
724 | $identifier, |
725 | RouteItem $routeItem = null |
726 | ) { |
727 | $catAttempts = $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->getCatValue( |
728 | $testSession->getSessionId(), |
729 | $this->getCatSection($testSession, $compilationDirectory, $routeItem)->getSectionId(), |
730 | 'cat-attempts' |
731 | ); |
732 | |
733 | $catAttempts = ($catAttempts) ? $catAttempts : []; |
734 | |
735 | return (isset($catAttempts[$identifier])) ? $catAttempts[$identifier] : 0; |
736 | } |
737 | |
738 | /** |
739 | * Alter the timeout value for engine params |
740 | * |
741 | * Get the timeout value from options following if it is for initial or nextItem call |
742 | * If it's not specified in the config, do not alter the $options |
743 | * |
744 | * @param array $options |
745 | * @return array |
746 | */ |
747 | protected function alterTimeoutCallValue(array $options) |
748 | { |
749 | $timeoutValue = null; |
750 | if ($this->isInitialCall === true) { |
751 | if ($this->hasOption(self::OPTION_INITIAL_CALL_TIMEOUT)) { |
752 | $timeoutValue = $this->getOption(self::OPTION_INITIAL_CALL_TIMEOUT); |
753 | } |
754 | } else { |
755 | if ($this->hasOption(self::OPTION_NEXT_ITEM_CALL_TIMEOUT)) { |
756 | $timeoutValue = $this->getOption(self::OPTION_NEXT_ITEM_CALL_TIMEOUT); |
757 | } |
758 | } |
759 | |
760 | if (!is_null($timeoutValue)) { |
761 | $options[self::OPTION_ENGINE_CLIENT]['options']['http_client_options']['timeout'] = $timeoutValue; |
762 | } |
763 | |
764 | return $options; |
765 | } |
766 | } |