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 | } |