Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
4.49% |
12 / 267 |
|
16.28% |
7 / 43 |
CRAP | |
0.00% |
0 / 1 |
taoQtiTest_helpers_TestSession | |
4.49% |
12 / 267 |
|
16.28% |
7 / 43 |
6528.93 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
setResultServer | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getResultServer | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setTest | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setLock | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setReadOnly | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isReadOnly | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isLocked | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
getTest | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
submitItemResults | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
20 | |||
endTestSession | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
12 | |||
rewind | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
20 | |||
submitTestResults | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
getItemRefUri | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getTestDefinitionUri | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
buildLtiOutcomeProcessing | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
suspend | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
resume | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
beginTestSession | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
jumpTo | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
moveNext | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
moveBack | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
skip | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
moveNextTestPart | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
moveNextAssessmentSection | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
moveNextAssessmentItem | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
closeTestPart | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
30 | |||
closeAssessmentSection | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
30 | |||
closeAssessmentItem | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
checkTimeLimits | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getTimeoutCode | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
closeTimer | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
132 | |||
setState | |
37.50% |
3 / 8 |
|
0.00% |
0 / 1 |
7.91 | |||
triggerResultItemTransmissionEvent | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
triggerResultTestVariablesTransmissionEvent | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
isManualScored | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
30 | |||
triggerEventChange | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
triggerEventPaused | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
triggerEventResumed | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
triggerStateChanged | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getEventManager | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getServiceLocator | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSessionMemento | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | /** |
4 | * This program is free software; you can redistribute it and/or |
5 | * modify it under the terms of the GNU General Public License |
6 | * as published by the Free Software Foundation; under version 2 |
7 | * of the License (non-upgradable). |
8 | * |
9 | * This program is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | * GNU General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU General Public License |
15 | * along with this program; if not, write to the Free Software |
16 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
17 | * |
18 | * Copyright (c) 2013-2014 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT); |
19 | * |
20 | */ |
21 | |
22 | use oat\oatbox\event\EventManager; |
23 | use oat\oatbox\service\ServiceManager; |
24 | use oat\taoQtiTest\helpers\TestSessionMemento; |
25 | use oat\taoQtiTest\models\classes\event\ResultTestVariablesTransmissionEvent; |
26 | use oat\taoQtiTest\models\event\QtiTestChangeEvent; |
27 | use oat\taoQtiTest\models\event\QtiTestStateChangeEvent; |
28 | use oat\taoQtiTest\models\event\ResultItemVariablesTransmissionEvent; |
29 | use oat\taoResultServer\models\classes\ResultStorageWrapper; |
30 | use oat\taoTests\models\event\TestExecutionPausedEvent; |
31 | use oat\taoTests\models\event\TestExecutionResumedEvent; |
32 | use qtism\common\datatypes\QtiDuration; |
33 | use qtism\common\datatypes\QtiFloat; |
34 | use qtism\common\enums\BaseType; |
35 | use qtism\common\enums\Cardinality; |
36 | use qtism\data\AssessmentItemRef; |
37 | use qtism\data\AssessmentTest; |
38 | use qtism\data\expressions\ExpressionCollection; |
39 | use qtism\data\expressions\NumberCorrect; |
40 | use qtism\data\expressions\NumberPresented; |
41 | use qtism\data\expressions\operators\Divide; |
42 | use qtism\data\expressions\Variable; |
43 | use qtism\data\ExtendedAssessmentItemRef; |
44 | use qtism\data\processing\OutcomeProcessing; |
45 | use qtism\data\rules\OutcomeRuleCollection; |
46 | use qtism\data\rules\SetOutcomeValue; |
47 | use qtism\data\state\OutcomeDeclaration; |
48 | use qtism\runtime\common\OutcomeVariable; |
49 | use qtism\runtime\common\ProcessingException; |
50 | use qtism\runtime\processing\OutcomeProcessingEngine; |
51 | use qtism\runtime\tests\AbstractSessionManager; |
52 | use qtism\runtime\tests\AssessmentItemSession; |
53 | use qtism\runtime\tests\AssessmentItemSessionException; |
54 | use qtism\runtime\tests\AssessmentTestPlace; |
55 | use qtism\runtime\tests\AssessmentTestSession; |
56 | use qtism\runtime\tests\AssessmentTestSessionException; |
57 | use qtism\runtime\tests\AssessmentTestSessionState; |
58 | use qtism\runtime\tests\Route; |
59 | use Symfony\Component\Lock\LockInterface; |
60 | use Zend\ServiceManager\ServiceLocatorAwareInterface; |
61 | |
62 | /** |
63 | * A TAO Specific extension of QtiSm's AssessmentTestSession class. |
64 | * |
65 | * @author Jérôme Bogaerts <jerome@taotesting.com> |
66 | * |
67 | */ |
68 | class taoQtiTest_helpers_TestSession extends AssessmentTestSession |
69 | { |
70 | /** |
71 | * The ResultServer to be used to transmit Item and Test results. |
72 | * |
73 | * @var ResultStorageWrapper |
74 | */ |
75 | private $resultServer; |
76 | |
77 | /** |
78 | * The TAO Resource describing the test. |
79 | * |
80 | * @var core_kernel_classes_Resource |
81 | */ |
82 | private $test; |
83 | |
84 | /** |
85 | * @var int |
86 | */ |
87 | private $timeoutCode = null; |
88 | |
89 | /** |
90 | * Nr of times setState has been called |
91 | * |
92 | * @var integer |
93 | */ |
94 | private $setStateCount = 0; |
95 | |
96 | /** |
97 | * @var Lock|null |
98 | */ |
99 | private $lock = null; |
100 | |
101 | /** |
102 | * Mode specifies the type of access you require to the test session. |
103 | * In readonly mode exception will be thrown on attempt to persist it in the test session storage |
104 | * @var boolean |
105 | */ |
106 | private $readOnly = false; |
107 | |
108 | /** |
109 | * Create a new TAO QTI Test Session. |
110 | * |
111 | * @param AssessmentTest $assessmentTest The AssessmentTest object representing the QTI test definition. |
112 | * @param AbstractSessionManager $manager The manager to be used to create new AssessmentItemSession objects. |
113 | * @param Route $route The Route (sequence of items) to be taken by the candidate for this test session. |
114 | * @param ResultStorageWrapper $resultServer The Result Server where Item and Test Results must be sent to. |
115 | * @param core_kernel_classes_Resource $test The TAO Resource describing the test. |
116 | */ |
117 | public function __construct( |
118 | AssessmentTest $assessmentTest, |
119 | AbstractSessionManager $manager, |
120 | Route $route, |
121 | ResultStorageWrapper $resultServer, |
122 | core_kernel_classes_Resource $test |
123 | ) { |
124 | parent::__construct($assessmentTest, $manager, $route); |
125 | $this->setResultServer($resultServer); |
126 | $this->setTest($test); |
127 | } |
128 | |
129 | /** |
130 | * Set the ResultServer to be used to transmit Item and Test results. |
131 | * |
132 | * @param ResultStorageWrapper $resultServer |
133 | */ |
134 | protected function setResultServer(ResultStorageWrapper $resultServer) |
135 | { |
136 | $this->resultServer = $resultServer; |
137 | } |
138 | |
139 | /** |
140 | * Get the ResultServer in use to transmit Item and Test results. |
141 | * |
142 | * @return ResultStorageWrapper |
143 | */ |
144 | protected function getResultServer() |
145 | { |
146 | return $this->resultServer; |
147 | } |
148 | |
149 | /** |
150 | * Set the TAO Resource describing the test in database. |
151 | * |
152 | * @param core_kernel_classes_Resource $test A Resource from the database describing a TAO test. |
153 | */ |
154 | protected function setTest(core_kernel_classes_Resource $test) |
155 | { |
156 | $this->test = $test; |
157 | } |
158 | |
159 | /** |
160 | * @param LockInterface $lock |
161 | */ |
162 | public function setLock(LockInterface $lock) |
163 | { |
164 | $this->lock = $lock; |
165 | } |
166 | |
167 | /** |
168 | * This mode specifies the type of access you require to the test session. |
169 | * In readonly mode exception will be thrown on attempt to persist it in the test session storage |
170 | * @param bool $readOnly |
171 | */ |
172 | public function setReadOnly(bool $readOnly) |
173 | { |
174 | $this->readOnly = $readOnly; |
175 | } |
176 | |
177 | /** |
178 | * Get access mode |
179 | * @return bool |
180 | */ |
181 | public function isReadOnly() |
182 | { |
183 | return $this->readOnly; |
184 | } |
185 | |
186 | /** |
187 | * @return bool |
188 | */ |
189 | public function isLocked() |
190 | { |
191 | return ($this->lock instanceof LockInterface) && $this->lock->isAcquired(); |
192 | } |
193 | |
194 | /** |
195 | * Get the TAO Resource describing the test in database. |
196 | * |
197 | * @return core_kernel_classes_Resource A Resource from the database describing a TAO test. |
198 | */ |
199 | public function getTest() |
200 | { |
201 | return $this->test; |
202 | } |
203 | |
204 | /** |
205 | * @param AssessmentItemSession $itemSession |
206 | * @param int $occurence |
207 | * @throws AssessmentTestSessionException |
208 | * @throws taoQtiTest_helpers_TestSessionException |
209 | */ |
210 | protected function submitItemResults(AssessmentItemSession $itemSession, $occurence = 0) |
211 | { |
212 | parent::submitItemResults($itemSession, $occurence); |
213 | |
214 | $item = $itemSession->getAssessmentItem(); |
215 | |
216 | common_Logger::t("submitting results for item '" . $item->getIdentifier() . "." . $occurence . "'."); |
217 | |
218 | try { |
219 | $itemVariableSet = []; |
220 | |
221 | // Get the item session we just responsed and send to the |
222 | // result server. |
223 | $itemSession = $this->getItemSession($item, $occurence); |
224 | |
225 | foreach ($itemSession->getKeys() as $identifier) { |
226 | common_Logger::t("Examination of variable '${identifier}'"); |
227 | $itemVariableSet[] = $itemSession->getVariable($identifier); |
228 | } |
229 | |
230 | $this->triggerResultItemTransmissionEvent($itemVariableSet, $occurence, $item); |
231 | } catch (AssessmentTestSessionException $e) { |
232 | // Error whith parent::endAttempt(). |
233 | $msg = "An error occured while ending the attempt item '" . $item->getIdentifier() . "." . $occurence |
234 | . "'."; |
235 | throw new taoQtiTest_helpers_TestSessionException( |
236 | $msg, |
237 | taoQtiTest_helpers_TestSessionException::RESULT_SUBMISSION_ERROR, |
238 | $e |
239 | ); |
240 | } catch (taoQtiCommon_helpers_ResultTransmissionException $e) { |
241 | // Error with Result Server. |
242 | $msg = "An error occured while transmitting item results for item '" . $item->getIdentifier() . "." |
243 | . $occurence . "'."; |
244 | throw new taoQtiTest_helpers_TestSessionException( |
245 | $msg, |
246 | taoQtiTest_helpers_TestSessionException::RESULT_SUBMISSION_ERROR, |
247 | $e |
248 | ); |
249 | } |
250 | } |
251 | |
252 | /** |
253 | * QTISM endTestSession method overriding. |
254 | * |
255 | * It consists of including an additional processing when the test ends, |
256 | * in order to send the LtiOutcome |
257 | * |
258 | * @see http://www.imsglobal.org/lis/ Outcome Management Service |
259 | * @throws AssessmentTestSessionException |
260 | * @throws taoQtiTest_helpers_TestSessionException If the session is already ended or if an error occurs whil |
261 | * transmitting/processing the result. |
262 | */ |
263 | public function endTestSession() |
264 | { |
265 | $sessionMemento = $this->getSessionMemento(); |
266 | parent::endTestSession(); |
267 | |
268 | common_Logger::i('Ending test session: ' . $this->getSessionId()); |
269 | try { |
270 | // Compute the LtiOutcome variable for LTI support. |
271 | $this->setVariable( |
272 | new OutcomeVariable( |
273 | 'LtiOutcome', |
274 | Cardinality::SINGLE, |
275 | BaseType::FLOAT, |
276 | new QtiFloat(0.0) |
277 | ) |
278 | ); |
279 | $outcomeProcessingEngine = new OutcomeProcessingEngine($this->buildLtiOutcomeProcessing(), $this); |
280 | $outcomeProcessingEngine->process(); |
281 | |
282 | // if numberPresented returned 0, division by 0 -> null. |
283 | $testUri = $this->getTest()->getUri(); |
284 | $var = $this->getVariable('LtiOutcome'); |
285 | $varIdentifier = $var->getIdentifier(); |
286 | |
287 | common_Logger::t("Submitting test result '${varIdentifier}' related to test '${testUri}'."); |
288 | $this->triggerResultTestVariablesTransmissionEvent( |
289 | [$var], |
290 | $testUri |
291 | ); |
292 | } catch (ProcessingException $e) { |
293 | $msg = "An error occured while processing the 'LtiOutcome' outcome variable."; |
294 | throw new taoQtiTest_helpers_TestSessionException( |
295 | $msg, |
296 | taoQtiTest_helpers_TestSessionException::RESULT_SUBMISSION_ERROR, |
297 | $e |
298 | ); |
299 | } catch (taoQtiCommon_helpers_ResultTransmissionException $e) { |
300 | $msg = "An error occured during test-level outcome results transmission."; |
301 | throw new taoQtiTest_helpers_TestSessionException( |
302 | $msg, |
303 | taoQtiTest_helpers_TestSessionException::RESULT_SUBMISSION_ERROR, |
304 | $e |
305 | ); |
306 | } finally { |
307 | $this->unsetVariable('LtiOutcome'); |
308 | } |
309 | |
310 | $this->triggerEventChange($sessionMemento); |
311 | } |
312 | |
313 | /** |
314 | * Rewind the test to its first position |
315 | * @param boolean $allowTimeout Whether or not it is allowed to jump if the timeLimits in force of the jump target |
316 | * are not respected. |
317 | * @throws UnexpectedValueException |
318 | * @throws AssessmentTestSessionException If $position is out of the Route bounds or the jump is not allowed because |
319 | * of time constraints. |
320 | * @throws AssessmentItemSessionException |
321 | */ |
322 | public function rewind($allowTimeout = false) |
323 | { |
324 | $position = 0; |
325 | $this->suspendItemSession(); |
326 | $route = $this->getRoute(); |
327 | $oldPosition = $route->getPosition(); |
328 | |
329 | try { |
330 | $route->setPosition($position); |
331 | $this->selectEligibleItems(); |
332 | |
333 | // Check the time limits after the jump is trully performed. |
334 | if ($allowTimeout === false) { |
335 | $this->checkTimeLimits(false, true); |
336 | } |
337 | |
338 | // No exception thrown, interact! |
339 | $this->interactWithItemSession(); |
340 | } catch (AssessmentTestSessionException $e) { |
341 | // Rollback to previous position. |
342 | $route->setPosition($oldPosition); |
343 | throw $e; |
344 | } catch (OutOfBoundsException $e) { |
345 | $msg = "Position '${position}' is out of the Route bounds."; |
346 | throw new AssessmentTestSessionException($msg, AssessmentTestSessionException::FORBIDDEN_JUMP, $e); |
347 | } |
348 | } |
349 | |
350 | /** |
351 | * AssessmentTestSession implementations must override this method in order to submit test results |
352 | * from the current AssessmentTestSession to the appropriate data source. |
353 | * |
354 | * This method is triggered once at the end of the AssessmentTestSession. |
355 | * |
356 | * * @throws AssessmentTestSessionException With error code RESULT_SUBMISSION_ERROR if an error occurs while |
357 | * transmitting results. |
358 | */ |
359 | protected function submitTestResults() |
360 | { |
361 | $testUri = $this->getTest()->getUri(); |
362 | |
363 | common_Logger::t("Submitting test result related to test '" . $testUri . "'."); |
364 | $this->triggerResultTestVariablesTransmissionEvent( |
365 | $this->getAllVariables()->getArrayCopy(), |
366 | $testUri |
367 | ); |
368 | } |
369 | |
370 | /** |
371 | * Get the TAO URI of an item from an ExtendedAssessmentItemRef object. |
372 | * |
373 | * @param ExtendedAssessmentItemRef $itemRef |
374 | * @return string A URI. |
375 | */ |
376 | protected static function getItemRefUri(AssessmentItemRef $itemRef) |
377 | { |
378 | $parts = explode('|', $itemRef->getHref()); |
379 | return $parts[0]; |
380 | } |
381 | |
382 | /** |
383 | * Get the TAO Uri of the Test Definition from an ExtendedAssessmentItemRef object. |
384 | * |
385 | * @param ExtendedAssessmentItemRef $itemRef |
386 | * @return string A URI. |
387 | */ |
388 | protected static function getTestDefinitionUri(ExtendedAssessmentItemRef $itemRef) |
389 | { |
390 | $parts = explode('|', $itemRef->getHref()); |
391 | return $parts[2]; |
392 | } |
393 | |
394 | /** |
395 | * Build the OutcomeProcessing object representing the set of QTI instructions |
396 | * to be performed to compute the LtiOutcome variable value. |
397 | * |
398 | * @return OutcomeProcessing A QTI Data Model OutcomeProcessing object. |
399 | */ |
400 | protected function buildLtiOutcomeProcessing() |
401 | { |
402 | |
403 | // ltiOutcome is calculated based on the SCORE_RATIO_WEIGHTED outcome for weighted items or SCORE_RATIO for not |
404 | // rated items |
405 | $ratioVariable = $this->getVariable('SCORE_RATIO_WEIGHTED'); |
406 | if (is_null($ratioVariable)) { |
407 | $ratioVariable = $this->getVariable('SCORE_RATIO'); |
408 | } |
409 | |
410 | if (is_null($ratioVariable)) { |
411 | //if no SCORE_RATIO outcome has been found, we keep support legacy ltiOutcome calculation algorithm |
412 | //it is based on number of presented item for backwards compatibility |
413 | $numberCorrect = new NumberCorrect(); |
414 | $numberPresented = new NumberPresented(); |
415 | $divide = new Divide(new ExpressionCollection([$numberCorrect, $numberPresented])); |
416 | $outcomeRule = new SetOutcomeValue('LtiOutcome', $divide); |
417 | } else { |
418 | $outcomeRule = new SetOutcomeValue('LtiOutcome', new Variable($ratioVariable->getIdentifier())); |
419 | } |
420 | |
421 | return new OutcomeProcessing(new OutcomeRuleCollection([$outcomeRule])); |
422 | } |
423 | |
424 | /** |
425 | * Suspend the current test session if it is running. |
426 | */ |
427 | public function suspend() |
428 | { |
429 | $sessionMemento = $this->getSessionMemento(); |
430 | $running = $this->isRunning(); |
431 | parent::suspend(); |
432 | if ($running) { |
433 | $this->triggerEventChange($sessionMemento); |
434 | $this->triggerEventPaused(); |
435 | common_Logger::i("QTI Test with session ID '" . $this->getSessionId() . "' suspended."); |
436 | } |
437 | } |
438 | |
439 | /** |
440 | * Resume the current test session if it is suspended. |
441 | */ |
442 | public function resume() |
443 | { |
444 | $sessionMemento = $this->getSessionMemento(); |
445 | $suspended = $this->getState() === AssessmentTestSessionState::SUSPENDED; |
446 | parent::resume(); |
447 | if ($suspended) { |
448 | $this->triggerEventChange($sessionMemento); |
449 | $this->triggerEventResumed(); |
450 | common_Logger::i("QTI Test with session ID '" . $this->getSessionId() . "' resumed."); |
451 | } |
452 | } |
453 | |
454 | /** |
455 | * Begins the test session. Calling this method will make the state |
456 | * change into AssessmentTestSessionState::INTERACTING. |
457 | * |
458 | * @qtism-test-interaction |
459 | * @qtism-test-duration-update |
460 | */ |
461 | public function beginTestSession() |
462 | { |
463 | // fake increase of state count to ensure setState triggers event |
464 | $this->setStateCount++; |
465 | $sessionMemento = $this->getSessionMemento(); |
466 | parent::beginTestSession(); |
467 | $this->triggerStateChanged($sessionMemento); |
468 | $this->triggerEventChange($sessionMemento); |
469 | } |
470 | |
471 | /** |
472 | * Perform a 'jump' to a given position in the Route sequence. The current navigation |
473 | * mode must be LINEAR to be able to jump. |
474 | * |
475 | * @param integer $position The position in the route the jump has to be made. |
476 | * @param boolean $allowTimeout Whether or not it is allowed to jump if the timeLimits in force of the jump target |
477 | * are not respected. |
478 | * @throws AssessmentTestSessionException If $position is out of the Route bounds or the jump is not allowed because |
479 | * of time constraints. |
480 | * @qtism-test-interaction |
481 | * @qtism-test-duration-update |
482 | */ |
483 | public function jumpTo($position, $allowTimeout = false) |
484 | { |
485 | $sessionMemento = $this->getSessionMemento(); |
486 | parent::jumpTo($position, $allowTimeout); |
487 | $this->triggerEventChange($sessionMemento); |
488 | } |
489 | |
490 | /** |
491 | * Ask the test session to move to next RouteItem in the Route sequence. |
492 | * |
493 | * If $allowTimeout is set to true, the very next RouteItem in the Route sequence will bet set |
494 | * as the current RouteItem, whether or not it is timed out or not. |
495 | * |
496 | * On the other hand, if $allowTimeout is set to false, the next RouteItem in the Route sequence |
497 | * which is not timed out will be set as the current RouteItem. If there is no more following RouteItems |
498 | * that are not timed out in the Route sequence, the test session ends gracefully. |
499 | * |
500 | * @param boolean $allowTimeout If set to true, the next RouteItem in the Route sequence does not have to respect |
501 | * the timeLimits in force. Default value is false. |
502 | * @throws AssessmentTestSessionException If the test session is not running or an issue occurs during the |
503 | * transition (e.g. branching, preConditions, ...). |
504 | * @qtism-test-interaction |
505 | * @qtism-test-duration-update |
506 | */ |
507 | public function moveNext($allowTimeout = false) |
508 | { |
509 | $sessionMemento = $this->getSessionMemento(); |
510 | parent::moveNext($allowTimeout); |
511 | $this->triggerEventChange($sessionMemento); |
512 | } |
513 | |
514 | /** |
515 | * Ask the test session to move to the previous RouteItem in the Route sequence. |
516 | * |
517 | * If $allowTimeout is set to true, the previous RouteItem in the Route sequence will bet set |
518 | * as the current RouteItem, whether or not it is timed out. |
519 | * |
520 | * On the other hand, if $allowTimeout is set to false, the previous RouteItem in the Route sequence |
521 | * which is not timed out will be set as the current RouteItem. If there is no more previous RouteItems |
522 | * that are not timed out in the Route sequence, the current RouteItem remains the same and an |
523 | * AssessmentTestSessionException with the appropriate timing error code is thrown. |
524 | * |
525 | * @param boolean $allowTimeout If set to true, the next RouteItem in the sequence does not have to respect |
526 | * timeLimits in force. Default value is false. |
527 | * @throws AssessmentTestSessionException If the test session is not running or an issue occurs during the |
528 | * transition (e.g. branching, preConditions, ...) or |
529 | * if $allowTimeout = false and there absolutely no possibility to move |
530 | * backward (even the first RouteItem is timed out). |
531 | * @qtism-test-interaction |
532 | * @qtism-test-duration-update |
533 | */ |
534 | public function moveBack($allowTimeout = false) |
535 | { |
536 | $sessionMemento = $this->getSessionMemento(); |
537 | parent::moveBack($allowTimeout); |
538 | $this->triggerEventChange($sessionMemento); |
539 | } |
540 | |
541 | /** |
542 | * Skip the current item. |
543 | * |
544 | * @throws AssessmentTestSessionException If the test session is not running or it is the last route item of the |
545 | * testPart but the SIMULTANEOUS submission mode is in force and not all |
546 | * responses were provided. |
547 | * @qtism-test-interaction |
548 | * @qtism-test-duration-update |
549 | */ |
550 | public function skip() |
551 | { |
552 | $sessionMemento = $this->getSessionMemento(); |
553 | parent::skip(); |
554 | $this->triggerEventChange($sessionMemento); |
555 | } |
556 | |
557 | /** |
558 | * Set the position in the Route at the very next TestPart in the Route sequence or, if the current |
559 | * testPart is the last one of the test session, the test session ends gracefully. If the submission mode |
560 | * is simultaneous, the pending responses are processed. |
561 | * |
562 | * @throws AssessmentTestSessionException If the test is currently not running. |
563 | */ |
564 | public function moveNextTestPart() |
565 | { |
566 | $sessionMemento = $this->getSessionMemento(); |
567 | parent::moveNextTestPart(); |
568 | $this->triggerEventChange($sessionMemento); |
569 | } |
570 | |
571 | /** |
572 | * Set the position in the Route at the very next assessmentSection in the route sequence. |
573 | * |
574 | * * If there is no assessmentSection left in the flow, the test session ends gracefully. |
575 | * * If there are still pending responses, they are processed. |
576 | * |
577 | * @throws AssessmentTestSessionException If the test is not running. |
578 | */ |
579 | public function moveNextAssessmentSection() |
580 | { |
581 | $sessionMemento = $this->getSessionMemento(); |
582 | parent::moveNextAssessmentSection(); |
583 | $this->triggerEventChange($sessionMemento); |
584 | } |
585 | |
586 | /** |
587 | * Set the position in the Route at the very next assessmentItem in the route sequence. |
588 | * |
589 | * * If there is no item left in the flow, the test session ends gracefully. |
590 | * * If there are still pending responses, they are processed. |
591 | * |
592 | * @throws AssessmentTestSessionException If the test is not running. |
593 | */ |
594 | public function moveNextAssessmentItem() |
595 | { |
596 | $sessionMemento = $this->getSessionMemento(); |
597 | parent::moveNextAssessmentItem(); |
598 | $this->triggerEventChange($sessionMemento); |
599 | } |
600 | |
601 | /** |
602 | * Set the position in the Route at the very next TestPart in the Route sequence or, if the current |
603 | * testPart is the last one of the test session, the test session ends gracefully. If the submission mode |
604 | * is simultaneous, the pending responses are processed. |
605 | * |
606 | * @throws AssessmentTestSessionException If the test is currently not running. |
607 | */ |
608 | public function closeTestPart() |
609 | { |
610 | $sessionMemento = $this->getSessionMemento(); |
611 | if ($this->isRunning() === false) { |
612 | $msg = "Cannot move to the next testPart while the state of the test session is INITIAL or CLOSED."; |
613 | throw new AssessmentTestSessionException($msg, AssessmentTestSessionException::STATE_VIOLATION); |
614 | } |
615 | |
616 | $route = $this->getRoute(); |
617 | $from = $route->current(); |
618 | |
619 | while ($route->valid() === true && $route->current()->getTestPart() === $from->getTestPart()) { |
620 | $itemSession = $this->getCurrentAssessmentItemSession(); |
621 | $itemSession->endItemSession(); |
622 | $this->nextRouteItem(); |
623 | } |
624 | |
625 | if ($this->isRunning() === true) { |
626 | $this->interactWithItemSession(); |
627 | } |
628 | |
629 | $this->triggerEventChange($sessionMemento); |
630 | } |
631 | |
632 | /** |
633 | * Set the position in the Route at the very next assessmentSection in the route sequence. |
634 | * |
635 | * * If there is no assessmentSection left in the flow, the test session ends gracefully. |
636 | * * If there are still pending responses, they are processed. |
637 | * |
638 | * @throws AssessmentTestSessionException If the test is not running. |
639 | */ |
640 | public function closeAssessmentSection() |
641 | { |
642 | $sessionMemento = $this->getSessionMemento(); |
643 | if ($this->isRunning() === false) { |
644 | $msg = "Cannot move to the next assessmentSection while the state of the test session is INITIAL or " |
645 | . "CLOSED."; |
646 | throw new AssessmentTestSessionException($msg, AssessmentTestSessionException::STATE_VIOLATION); |
647 | } |
648 | |
649 | $route = $this->getRoute(); |
650 | $from = $route->current(); |
651 | |
652 | while ( |
653 | $route->valid() === true |
654 | && $route->current()->getAssessmentSection() === $from->getAssessmentSection() |
655 | ) { |
656 | $itemSession = $this->getCurrentAssessmentItemSession(); |
657 | $itemSession->endItemSession(); |
658 | $this->nextRouteItem(); |
659 | } |
660 | |
661 | if ($this->isRunning() === true) { |
662 | $this->interactWithItemSession(); |
663 | } |
664 | |
665 | $this->triggerEventChange($sessionMemento); |
666 | } |
667 | |
668 | /** |
669 | * Set the position in the Route at the very next assessmentItem in the route sequence. |
670 | * |
671 | * * If there is no item left in the flow, the test session ends gracefully. |
672 | * * If there are still pending responses, they are processed. |
673 | * |
674 | * @throws AssessmentTestSessionException If the test is not running. |
675 | */ |
676 | public function closeAssessmentItem() |
677 | { |
678 | $sessionMemento = $this->getSessionMemento(); |
679 | if ($this->isRunning() === false) { |
680 | $msg = "Cannot move to the next testPart while the state of the test session is INITIAL or CLOSED."; |
681 | throw new AssessmentTestSessionException($msg, AssessmentTestSessionException::STATE_VIOLATION); |
682 | } |
683 | |
684 | $itemSession = $this->getCurrentAssessmentItemSession(); |
685 | $itemSession->endItemSession(); |
686 | $this->nextRouteItem(); |
687 | |
688 | if ($this->isRunning() === true) { |
689 | $this->interactWithItemSession(); |
690 | } |
691 | |
692 | $this->triggerEventChange($sessionMemento); |
693 | } |
694 | |
695 | /** |
696 | * @param bool $includeMinTime |
697 | * @param bool $includeAssessmentItem |
698 | * @param bool $acceptableLatency |
699 | * @throws AssessmentTestSessionException |
700 | */ |
701 | public function checkTimeLimits($includeMinTime = false, $includeAssessmentItem = false, $acceptableLatency = true) |
702 | { |
703 | try { |
704 | parent::checkTimeLimits($includeMinTime, $includeAssessmentItem, $acceptableLatency); |
705 | } catch (AssessmentTestSessionException $e) { |
706 | $this->timeoutCode = $e->getCode(); |
707 | throw $e; |
708 | } |
709 | } |
710 | |
711 | /** |
712 | * @return null|int |
713 | */ |
714 | public function getTimeoutCode() |
715 | { |
716 | return $this->timeoutCode; |
717 | } |
718 | |
719 | /** |
720 | * Closes a timer |
721 | * @param string $identifier |
722 | * @param string [$type] |
723 | */ |
724 | public function closeTimer($identifier, $type = null) |
725 | { |
726 | switch ($type) { |
727 | case 'assessmentTest': |
728 | $places = AssessmentTestPlace::ASSESSMENT_TEST; |
729 | break; |
730 | |
731 | case 'testPart': |
732 | $places = AssessmentTestPlace::TEST_PART; |
733 | break; |
734 | |
735 | case 'assessmentSection': |
736 | $places = AssessmentTestPlace::ASSESSMENT_SECTION; |
737 | break; |
738 | |
739 | case 'assessmentItemRef': |
740 | $places = AssessmentTestPlace::ASSESSMENT_ITEM; |
741 | break; |
742 | |
743 | default: |
744 | $places = AssessmentTestPlace::ASSESSMENT_TEST |
745 | | AssessmentTestPlace::TEST_PART |
746 | | AssessmentTestPlace::ASSESSMENT_SECTION |
747 | | AssessmentTestPlace::ASSESSMENT_ITEM; |
748 | } |
749 | |
750 | $constraints = $this->getTimeConstraints($places); |
751 | foreach ($constraints as $constraint) { |
752 | $source = $constraint->getSource(); |
753 | $placeId = $source->getIdentifier(); |
754 | if ($placeId === $identifier) { |
755 | if (($maxTime = $constraint->getAdjustedMaxTime()) !== null) { |
756 | $constraintDuration = $constraint->getDuration(); |
757 | if ($constraintDuration instanceof QtiDuration) { |
758 | $constraintDuration->sub($constraintDuration); |
759 | $constraintDuration->add($maxTime); |
760 | if ($constraint->getApplyExtraTime()) { |
761 | $extraTime = $constraint->getTimer()->getExtraTime(); |
762 | $constraintDuration->add(new QtiDuration('PT' . $extraTime . 'S')); |
763 | } |
764 | } |
765 | } |
766 | } |
767 | } |
768 | } |
769 | |
770 | /** |
771 | * Override setState to trigger events on state change |
772 | * Only trigger on thir call or higher: |
773 | * |
774 | * Call Nr 1: called in constructor |
775 | * Call Nr 2: called in initialiser |
776 | * Call Nr 3+: real state change |
777 | * |
778 | * Except during creation of session in beginTestSession |
779 | * triggerStateChanged is triggered manually |
780 | * |
781 | * @inheritdoc |
782 | * @param int $state |
783 | */ |
784 | public function setState($state) |
785 | { |
786 | $this->setStateCount++; |
787 | if ($this->setStateCount <= 2) { |
788 | return parent::setState($state); |
789 | } else { |
790 | $previousState = $this->getState(); |
791 | $sessionMemento = $this->getSessionMemento(); |
792 | parent::setState($state); |
793 | if ($previousState !== null && $previousState !== $state) { |
794 | $this->triggerStateChanged($sessionMemento); |
795 | } |
796 | } |
797 | } |
798 | |
799 | private function triggerResultItemTransmissionEvent( |
800 | array $variables, |
801 | $occurrence, |
802 | ExtendedAssessmentItemRef $item |
803 | ): void { |
804 | $transmissionId = sprintf('%s.%s.%s', $this->getSessionId(), $item, $occurrence); |
805 | $this->getEventManager()->trigger(new ResultItemVariablesTransmissionEvent( |
806 | $this->getSessionId(), |
807 | $variables, |
808 | $transmissionId, |
809 | self::getItemRefUri($item), |
810 | self::getTestDefinitionUri($item) |
811 | )); |
812 | } |
813 | |
814 | private function triggerResultTestVariablesTransmissionEvent( |
815 | array $variables, |
816 | string $testUri = '' |
817 | ): void { |
818 | $this->getEventManager()->trigger(new ResultTestVariablesTransmissionEvent( |
819 | $this->getSessionId(), |
820 | $variables, |
821 | $this->getSessionId(), |
822 | $testUri |
823 | )); |
824 | } |
825 | |
826 | public function isManualScored(): bool |
827 | { |
828 | /** @var AssessmentItemRef $itemRef */ |
829 | foreach ($this->getRoute()->getAssessmentItemRefs() as $itemRef) { |
830 | foreach ($itemRef->getComponents() as $component) { |
831 | if ( |
832 | $component instanceof OutcomeDeclaration |
833 | && $component->isExternallyScored() |
834 | ) { |
835 | return true; |
836 | } |
837 | } |
838 | } |
839 | |
840 | return false; |
841 | } |
842 | |
843 | /** |
844 | * @param TestSessionMemento $sessionMemento |
845 | */ |
846 | protected function triggerEventChange(TestSessionMemento $sessionMemento) |
847 | { |
848 | $event = new QtiTestChangeEvent($this, $sessionMemento); |
849 | if ($event instanceof ServiceLocatorAwareInterface) { |
850 | $event->setServiceLocator($this->getServiceLocator()); |
851 | } |
852 | $this->getEventManager()->trigger($event); |
853 | } |
854 | |
855 | protected function triggerEventPaused() |
856 | { |
857 | $event = new TestExecutionPausedEvent( |
858 | $this->getSessionId() |
859 | ); |
860 | $this->getEventManager()->trigger($event); |
861 | } |
862 | |
863 | protected function triggerEventResumed() |
864 | { |
865 | $event = new TestExecutionResumedEvent( |
866 | $this->getSessionId() |
867 | ); |
868 | $this->getEventManager()->trigger($event); |
869 | } |
870 | |
871 | /** |
872 | * @param TestSessionMemento $sessionMemento |
873 | */ |
874 | protected function triggerStateChanged(TestSessionMemento $sessionMemento) |
875 | { |
876 | $event = new QtiTestStateChangeEvent($this, $sessionMemento); |
877 | if ($event instanceof ServiceLocatorAwareInterface) { |
878 | $event->setServiceLocator($this->getServiceLocator()); |
879 | } |
880 | $this->getEventManager()->trigger($event); |
881 | } |
882 | |
883 | /** |
884 | * @return EventManager |
885 | */ |
886 | protected function getEventManager() |
887 | { |
888 | return $this->getServiceLocator()->get(EventManager::SERVICE_ID); |
889 | } |
890 | |
891 | protected function getServiceLocator() |
892 | { |
893 | return ServiceManager::getServiceManager(); |
894 | } |
895 | |
896 | /** |
897 | * @return TestSessionMemento |
898 | */ |
899 | protected function getSessionMemento() |
900 | { |
901 | return new TestSessionMemento($this); |
902 | } |
903 | } |