Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 647
0.00% covered (danger)
0.00%
0 / 37
CRAP
0.00% covered (danger)
0.00%
0 / 1
taoQtiTest_helpers_TestRunnerUtils
0.00% covered (danger)
0.00%
0 / 647
0.00% covered (danger)
0.00%
0 / 37
19740
0.00% covered (danger)
0.00%
0 / 1
 getServiceManager
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExtendedStateService
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildItemServiceCall
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
2
 buildServiceCallId
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 setInitialOutcomes
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 preserveOutcomes
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 isTimeout
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getCurrentItemUri
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 buildActionCallUrl
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 buildServiceApi
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 noHttpClientCache
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 beginCandidateInteraction
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 doesAllowSkipping
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 doesValidateResponses
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 doesAllowComment
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 buildTimeConstraints
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 buildPossibleJumps
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 buildAssessmentTestContext
0.00% covered (danger)
0.00%
0 / 177
0.00% covered (danger)
0.00%
0 / 1
132
 getItemRef
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
110
 setItemFlag
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getItemFlag
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getItemUsage
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 isItemInformational
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 isItemCompleted
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
56
 getItemInfo
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 getJumpsMap
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 getNavigatorMap
0.00% covered (danger)
0.00%
0 / 126
0.00% covered (danger)
0.00%
0 / 1
462
 countItems
0.00% covered (danger)
0.00%
0 / 68
0.00% covered (danger)
0.00%
0 / 1
156
 getTestMap
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 testCompletion
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 considerProgress
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
42
 doesAllowExit
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 doesAllowLogout
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getCategories
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getAllCategories
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 isQtiValueNull
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
30
 getDurationWithMicroseconds
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
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) 2014 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
19*
20*/
21
22use oat\taoQtiTest\models\runner\RunnerService;
23use oat\taoQtiTest\models\runner\time\TimerLabelFormatterService;
24use qtism\common\datatypes\QtiDuration;
25use qtism\data\NavigationMode;
26use qtism\data\SubmissionMode;
27use qtism\runtime\common\Container;
28use qtism\runtime\tests\AssessmentTestSession;
29use qtism\runtime\tests\AssessmentTestSessionException;
30use qtism\runtime\tests\AssessmentItemSession;
31use qtism\runtime\tests\AssessmentItemSessionState;
32use qtism\runtime\tests\AssessmentTestSessionState;
33use qtism\runtime\tests\Jump;
34use qtism\runtime\tests\RouteItem;
35use oat\taoQtiTest\models\ExtendedStateService;
36use oat\taoQtiTest\models\QtiTestCompilerIndex;
37use oat\taoQtiTest\models\runner\rubric\QtiRunnerRubric;
38use qtism\common\datatypes\QtiString;
39use oat\oatbox\service\ServiceManager;
40use oat\taoQtiTest\models\runner\RunnerServiceContext;
41
42/**
43* Utility methods for the QtiTest Test Runner.
44*
45* @author Jérôme Bogaerts <jerome@taotesting.com>
46*
47*/
48class taoQtiTest_helpers_TestRunnerUtils
49{
50    /**
51     * Temporary helper until proper ServiceManager integration
52     * @return ServiceManager
53     */
54    protected static function getServiceManager()
55    {
56        return ServiceManager::getServiceManager();
57    }
58
59    /**
60     * Temporary helper until proper ServiceManager integration
61     * @return ExtendedStateService
62     */
63    public static function getExtendedStateService()
64    {
65        return self::getServiceManager()->get(ExtendedStateService::SERVICE_ID);
66    }
67
68    /**
69     * Get the ServiceCall object representing how to call the current Assessment Item to be
70     * presented to a candidate in a given Assessment Test $session.
71     *
72     * @param AssessmentTestSession $session An AssessmentTestSession Object.
73     * @param string $testDefinition URI The URI of the knowledge base resource representing the folder where the QTI
74     *                               Test Definition is stored.
75     * @param string $testCompilation URI The URI of the knowledge base resource representing the folder where the QTI
76     *                                Test Compilation is stored.
77     * @return tao_models_classes_service_ServiceCall A ServiceCall object.
78     */
79    public static function buildItemServiceCall(AssessmentTestSession $session, $testDefinitionUri, $testCompilationUri)
80    {
81
82        $href = $session->getCurrentAssessmentItemRef()->getHref();
83
84        // retrive itemUri & itemPath.
85        $parts = explode('|', $href);
86
87        $definition =  new core_kernel_classes_Resource(
88            RunnerService::INSTANCE_TEST_ITEM_RUNNER_SERVICE
89        );
90        $serviceCall = new tao_models_classes_service_ServiceCall($definition);
91
92        $uriResource = new core_kernel_classes_Resource(
93            taoItems_models_classes_ItemsService::INSTANCE_FORMAL_PARAM_ITEM_URI
94        );
95        $uriParam = new tao_models_classes_service_ConstantParameter($uriResource, $parts[0]);
96        $serviceCall->addInParameter($uriParam);
97
98        $pathResource = new core_kernel_classes_Resource(
99            taoItems_models_classes_ItemsService::INSTANCE_FORMAL_PARAM_ITEM_PATH
100        );
101        $pathParam = new tao_models_classes_service_ConstantParameter($pathResource, $parts[1]);
102        $serviceCall->addInParameter($pathParam);
103
104        $dataPathResource = new core_kernel_classes_Resource(
105            taoItems_models_classes_ItemsService::INSTANCE_FORMAL_PARAM_ITEM_DATA_PATH
106        );
107        $dataPathParam = new tao_models_classes_service_ConstantParameter($dataPathResource, $parts[2]);
108        $serviceCall->addInParameter($dataPathParam);
109
110        $parentServiceCallIdResource = new core_kernel_classes_Resource(
111            RunnerService::INSTANCE_FORMAL_PARAM_TEST_ITEM_RUNNER_PARENT_CALL_ID
112        );
113        $parentServiceCallIdParam = new tao_models_classes_service_ConstantParameter(
114            $parentServiceCallIdResource,
115            $session->getSessionId()
116        );
117        $serviceCall->addInParameter($parentServiceCallIdParam);
118
119        $testDefinitionResource = new core_kernel_classes_Resource(
120            taoQtiTest_models_classes_QtiTestService::INSTANCE_FORMAL_PARAM_TEST_DEFINITION
121        );
122        $testDefinitionParam = new tao_models_classes_service_ConstantParameter(
123            $testDefinitionResource,
124            $testDefinitionUri
125        );
126        $serviceCall->addInParameter($testDefinitionParam);
127
128        $testCompilationResource = new core_kernel_classes_Resource(
129            taoQtiTest_models_classes_QtiTestService::INSTANCE_FORMAL_PARAM_TEST_COMPILATION
130        );
131        $testCompilationParam = new tao_models_classes_service_ConstantParameter(
132            $testCompilationResource,
133            $testCompilationUri
134        );
135        $serviceCall->addInParameter($testCompilationParam);
136
137        return $serviceCall;
138    }
139
140    /**
141     * Build the Service Call ID of the current Assessment Item to be presented to a candidate
142     * in a given Assessment Test $session.
143     *
144     * @return string A service call id composed of the session identifier,  the identifier of the item and its
145     *                occurence number in the route.
146     */
147    public static function buildServiceCallId(AssessmentTestSession $session)
148    {
149
150        $sessionId = $session->getSessionId();
151        $itemId = $session->getCurrentAssessmentItemRef()->getIdentifier();
152        $occurence = $session->getCurrentAssessmentItemRefOccurence();
153        return "${sessionId}.${itemId}.${occurence}";
154    }
155
156    /**
157     * Set the initial outcomes defined in the rdf outcome map configuration file
158     *
159     * @param AssessmentTestSession $session
160     * @param \oat\oatbox\user\User $testTaker
161     * @throws common_exception_Error
162     * @throws common_ext_ExtensionException
163     */
164    public static function setInitialOutcomes(AssessmentTestSession $session, \oat\oatbox\user\User $testTaker)
165    {
166        $rdfOutcomeMap = \common_ext_ExtensionsManager::singleton()
167            ->getExtensionById('taoQtiTest')
168            ->getConfig('rdfOutcomeMap');
169        if (is_array($rdfOutcomeMap)) {
170            foreach ($rdfOutcomeMap as $outcomeId => $rdfPropUri) {
171                //set outcome value
172                $values = $testTaker->getPropertyValues($rdfPropUri);
173                $outcome = $session->getVariable($outcomeId);
174                if (!is_null($outcome) && count($values)) {
175                    $outcome->setValue(new QtiString((string)$values[0]));
176                }
177            }
178        }
179    }
180
181    /**
182     * Preserve the outcomes variables set in the "rdfOutcomeMap" config
183     * This is required to prevent those special outcomes from being reset before every outcome processing
184     *
185     * @param AssessmentTestSession $session
186     * @throws common_ext_ExtensionException
187     */
188    public static function preserveOutcomes(AssessmentTestSession $session)
189    {
190        //preserve the special outcomes defined in the rdfOutcomeMap config
191        $rdfOutcomeMap = \common_ext_ExtensionsManager::singleton()
192            ->getExtensionById('taoQtiTest')
193            ->getConfig('rdfOutcomeMap');
194        if (is_array($rdfOutcomeMap) === true) {
195            $session->setPreservedOutcomeVariables(array_keys($rdfOutcomeMap));
196        }
197    }
198
199    /**
200     * Whether or not the current Assessment Item to be presented to the candidate is timed-out. By timed-out
201     * we mean:
202     *
203     * * current Assessment Test level time limits are not respected OR,
204     * * current Test Part level time limits are not respected OR,
205     * * current Assessment Section level time limits are not respected OR,
206     * * current Assessment Item level time limits are not respected.
207     *
208     * @param AssessmentTestSession $session The AssessmentTestSession object you want to know it is timed-out.
209     * @return boolean
210     */
211    public static function isTimeout(AssessmentTestSession $session)
212    {
213
214        try {
215            $session->checkTimeLimits(false, true, false);
216        } catch (AssessmentTestSessionException $e) {
217            return true;
218        }
219
220        return false;
221    }
222
223    /**
224     * Get the URI referencing the current Assessment Item (in the knowledge base)
225     * to be presented to the candidate.
226     *
227     * @param AssessmentTestSession $session An AssessmentTestSession object.
228     * @return string A URI.
229     */
230    public static function getCurrentItemUri(AssessmentTestSession $session)
231    {
232        $href = $session->getCurrentAssessmentItemRef()->getHref();
233        $parts = explode('|', $href);
234
235        return $parts[0];
236    }
237
238    /**
239     * Build the URL to be called to perform a given action on the Test Runner controller.
240     *
241     * @param AssessmentTestSession $session An AssessmentTestSession object.
242     * @param string $action The action name e.g. 'moveForward', 'moveBackward', 'skip', ...
243     * @param string $qtiTestDefinitionUri The URI of a reference to an Assessment Test definition in the knowledge
244     *                                     base.
245     * @param string $qtiTestCompilationUri The Uri of a reference to an Assessment Test compilation in the knowledge
246     *                                      base.
247     * @param string $standalone
248     * @return string A URL to be called to perform an action.
249     */
250    public static function buildActionCallUrl(
251        AssessmentTestSession $session,
252        $action,
253        $qtiTestDefinitionUri,
254        $qtiTestCompilationUri,
255        $standalone
256    ) {
257        return _url($action, 'TestRunner', null, [
258            'QtiTestDefinition' => $qtiTestDefinitionUri,
259            'QtiTestCompilation' => $qtiTestCompilationUri,
260            'standalone' => $standalone,
261            'serviceCallId' => $session->getSessionId(),
262        ]);
263    }
264
265    public static function buildServiceApi(
266        AssessmentTestSession $session,
267        $qtiTestDefinitionUri,
268        $qtiTestCompilationUri
269    ) {
270        $serviceCall = self::buildItemServiceCall($session, $qtiTestDefinitionUri, $qtiTestCompilationUri);
271        $itemServiceCallId = self::buildServiceCallId($session);
272        return tao_helpers_ServiceJavascripts::getServiceApi($serviceCall, $itemServiceCallId);
273    }
274
275    /**
276     * Tell the client to not cache the current request. Supports HTTP 1.0 to 1.1.
277     */
278    public static function noHttpClientCache()
279    {
280        // From stackOverflow:
281        // http://stackoverflow.com/questions/49547/making-sure-a-web-page-is-not-cached-across-all-browsers
282        // license is Creative Commons Attribution Share Alike (author Edward Wilde)
283        header('Cache-Control: no-cache, no-store, must-revalidate'); // HTTP 1.1.
284        header('Pragma: no-cache'); // HTTP 1.0.
285        header('Expires: 0'); // Proxies.
286    }
287
288    /**
289     * Make the candidate interact with the current Assessment Item to be presented. A new attempt
290     * will begin automatically if the candidate still has available attempts. Otherwise,
291     * nothing happends.
292     *
293     * @param AssessmentTestSession $session The AssessmentTestSession you want to make the candidate interact with.
294     */
295    public static function beginCandidateInteraction(AssessmentTestSession $session)
296    {
297        $itemSession = $session->getCurrentAssessmentItemSession();
298        $itemSessionState = $itemSession->getState();
299
300        $initial = $itemSessionState === AssessmentItemSessionState::INITIAL;
301        $suspended = $itemSessionState === AssessmentItemSessionState::SUSPENDED;
302        $remainingAttempts = $itemSession->getRemainingAttempts();
303        $attemptable = $remainingAttempts === -1 || $remainingAttempts > 0;
304
305        if ($initial === true || ($suspended === true && $attemptable === true)) {
306            // Begin the very first attempt.
307            $session->beginAttempt();
308        }
309        // Otherwise, the item is not attemptable bt the candidate.
310    }
311
312    /**
313     * Whether or not the candidate taking the given $session is allowed
314     * to skip the presented Assessment Item.
315     *
316     * @param AssessmentTestSession $session A given AssessmentTestSession object.
317     * @return boolean
318     */
319    public static function doesAllowSkipping(AssessmentTestSession $session)
320    {
321        $doesAllowSkipping = true;
322        $submissionMode = $session->getCurrentSubmissionMode();
323
324        $routeItem = $session->getRoute()->current();
325        $routeControl = $routeItem->getItemSessionControl();
326
327        if (empty($routeControl) === false) {
328            $doesAllowSkipping = $routeControl->getItemSessionControl()->doesAllowSkipping();
329        }
330
331        return $doesAllowSkipping && $submissionMode === SubmissionMode::INDIVIDUAL;
332    }
333
334    /**
335     * Whether or not the candidate's response is validated
336     *
337     * @param AssessmentTestSession $session A given AssessmentTestSession object.
338     * @return boolean
339     */
340    public static function doesValidateResponses(AssessmentTestSession $session)
341    {
342        $doesValidateResponses = true;
343        $submissionMode = $session->getCurrentSubmissionMode();
344
345        $routeItem = $session->getRoute()->current();
346        $routeControl = $routeItem->getItemSessionControl();
347
348        if (empty($routeControl) === false) {
349            $doesValidateResponses = $routeControl->getItemSessionControl()->mustValidateResponses();
350        }
351
352        return $doesValidateResponses && $submissionMode === SubmissionMode::INDIVIDUAL;
353    }
354
355    /**
356     * Whether or not the candidate taking the given $session is allowed to make
357     * a comment on the presented Assessment Item.
358     *
359     * @param AssessmentTestSession $session A given AssessmentTestSession object.
360     * @return boolean
361     */
362    public static function doesAllowComment(AssessmentTestSession $session)
363    {
364        $doesAllowComment = false;
365
366        $routeItem = $session->getRoute()->current();
367        $routeControl = $routeItem->getItemSessionControl();
368
369        if (empty($routeControl) === false) {
370            $doesAllowComment = $routeControl->getItemSessionControl()->doesAllowComment();
371        }
372
373        return $doesAllowComment;
374    }
375
376    /**
377     * Build an array where each cell represent a time constraint (a.k.a. time limits)
378     * in force. Each cell is actually an array with two keys:
379     *
380     * * 'source': The identifier of the QTI component emitting the constraint
381     *   (e.g. AssessmentTest, TestPart, AssessmentSection, AssessmentItemRef).
382     * * 'seconds': The number of remaining seconds until it times out.
383     *
384     * @param AssessmentTestSession $session An AssessmentTestSession object.
385     * @return array
386     */
387    public static function buildTimeConstraints(AssessmentTestSession $session)
388    {
389        $constraints = [];
390        /** @var TimerLabelFormatterService $timerLabelFormatter */
391        $timerLabelFormatter = static::getServiceManager()->get(TimerLabelFormatterService::SERVICE_ID);
392
393        foreach ($session->getTimeConstraints() as $tc) {
394            // Only consider time constraints in force.
395            if ($tc->getMaximumRemainingTime() !== false) {
396                $label = method_exists($tc->getSource(), 'getTitle')
397                    ? $tc->getSource()->getTitle()
398                    : $tc->getSource()->getIdentifier();
399                $constraints[] = [
400                    'label' => $timerLabelFormatter->format($label),
401                    'source' => $tc->getSource()->getIdentifier(),
402                    'seconds' => self::getDurationWithMicroseconds($tc->getMaximumRemainingTime()),
403                    'allowLateSubmission' => $tc->allowLateSubmission(),
404                    'qtiClassName' => $tc->getSource()->getQtiClassName()
405                ];
406            }
407        }
408
409        return $constraints;
410    }
411
412    /**
413     * Build an array where each cell represent a possible Assessment Item a candidate
414     * can jump on during a given $session. Each cell is an array with two keys:
415     *
416     * * 'identifier': The identifier of the Assessment Item the candidate is allowed to jump on.
417     * * 'position': The position in the route of the Assessment Item.
418     *
419     * @param AssessmentTestSession $session A given AssessmentTestSession object.
420     * @return array
421     */
422    public static function buildPossibleJumps(AssessmentTestSession $session)
423    {
424        $jumps = [];
425
426        foreach ($session->getPossibleJumps() as $jumpObject) {
427            $jump = [];
428            $jump['identifier'] = $jumpObject->getTarget()->getAssessmentItemRef()->getIdentifier();
429            $jump['position'] = $jumpObject->getPosition();
430
431            $jumps[] = $jump;
432        }
433
434        return $jumps;
435    }
436
437    /**
438     * Build the context of the given candidate test $session as an associative array. This array
439     * is especially usefull to transmit the test context to a view as JSON data.
440     *
441     * The returned array contains the following keys:
442     *
443     * * state: The state of test session.
444     * * navigationMode: The current navigation mode.
445     * * submissionMode: The current submission mode.
446     * * remainingAttempts: The number of remaining attempts for the current item.
447     * * isAdaptive: Whether or not the current item is adaptive.
448     * * itemIdentifier: The identifier of the current item.
449     * * itemSessionState: The state of the current assessment item session.
450     * * timeConstraints: The time constraints in force.
451     * * testTitle: The title of the test.
452     * * testPartId: The identifier of the current test part.
453     * * sectionTitle: The title of the current section.
454     * * numberItems: The total number of items eligible to the candidate.
455     * * numberCompleted: The total number items considered to be completed by the candidate.
456     * * moveForwardUrl: The URL to be dereferenced to perform a moveNext on the session.
457     * * moveBackwardUrl: The URL to be dereferenced to perform a moveBack on the session.
458     * * skipUrl: The URL to be dereferenced to perform a skip on the session.
459     * * commentUrl: The URL to be dereferenced to leave a comment about the current item.
460     * * timeoutUrl: The URL to be dereferenced when the time constraints in force reach their maximum.
461     * * canMoveBackward: Whether or not the candidate is allowed/able to move backward.
462     * * jumps: The possible jumpers the candidate is allowed to undertake among eligible items.
463     * * itemServiceApiCall: The JavaScript code to be executed to instantiate the current item.
464     * * rubrics: The XHTML compiled content of the rubric blocks to be displayed for the current item if any.
465     * * allowComment: Whether or not the candidate is allowed to leave a comment about the current item.
466     * * allowSkipping: Whether or not the candidate is allowed to skip the current item.
467     * * considerProgress: Whether or not the test driver view must consider to give a test progress feedback.
468     *
469     * @param AssessmentTestSession $session A given AssessmentTestSession object.
470     * @param array $testMeta An associative array containing meta-data about the test definition taken by the
471     *                        candidate.
472     * @param QtiTestCompilerIndex $itemIndex
473     * @param string $qtiTestDefinitionUri The URI of a reference to an Assessment Test definition in the knowledge
474     *                                     base.
475     * @param string $qtiTestCompilationUri The Uri of a reference to an Assessment Test compilation in the knowledge
476     *                                      base.
477     * @param string $standalone
478     * @param array $compilationDirs An array containing respectively the private and public compilation directories.
479     * @return array The context of the candidate session.
480     */
481    public static function buildAssessmentTestContext(
482        AssessmentTestSession $session,
483        array $testMeta,
484        $itemIndex,
485        $qtiTestDefinitionUri,
486        $qtiTestCompilationUri,
487        $standalone,
488        $compilationDirs
489    ) {
490        $context = [];
491
492        // The state of the test session.
493        $context['state'] = $session->getState();
494
495        // Default values for the test session context.
496        $context['navigationMode'] = null;
497        $context['submissionMode'] = null;
498        $context['remainingAttempts'] = 0;
499        $context['isAdaptive'] = false;
500
501        $hasBeenPaused = false;
502        if (common_ext_ExtensionsManager::singleton()->isEnabled('taoProctoring')) {
503            $hasBeenPaused = \oat\taoProctoring\helpers\DeliveryHelper::getHasBeenPaused($session->getSessionId());
504        }
505        $context['hasBeenPaused'] = $hasBeenPaused;
506
507
508        if ($session->getState() === AssessmentTestSessionState::INTERACTING) {
509            $config = common_ext_ExtensionsManager::singleton()
510                ->getExtensionById('taoQtiTest')
511                ->getConfig('testRunner');
512
513            // The navigation mode.
514            $context['navigationMode'] = $session->getCurrentNavigationMode();
515
516            // The submission mode.
517            $context['submissionMode'] = $session->getCurrentSubmissionMode();
518
519            // The number of remaining attempts for the current item.
520            $context['remainingAttempts'] = $session->getCurrentRemainingAttempts();
521
522            // Whether or not the current step is time out.
523            $context['isTimeout'] = self::isTimeout($session);
524
525            // The identifier of the current item.
526            $context['itemIdentifier'] = $session->getCurrentAssessmentItemRef()->getIdentifier();
527
528            // The state of the current AssessmentTestSession.
529            $context['itemSessionState'] = $session->getCurrentAssessmentItemSession()->getState();
530
531            // Whether the current item is adaptive.
532            $context['isAdaptive'] = $session->isCurrentAssessmentItemAdaptive();
533
534            // Whether the current item is the very last one of the test.
535            $context['isLast'] = $session->getRoute()->isLast();
536
537            // The current position in the route.
538            $context['itemPosition'] = $session->getRoute()->getPosition();
539
540            // Time constraints.
541            $context['timeConstraints'] = self::buildTimeConstraints($session);
542
543            // Test title.
544            $context['testTitle'] = $session->getAssessmentTest()->getTitle();
545
546            // Test Part title.
547            $context['testPartId'] = $session->getCurrentTestPart()->getIdentifier();
548
549            // Current Section title.
550            $context['sectionTitle'] = $session->getCurrentAssessmentSection()->getTitle();
551
552            // Number of items composing the test session.
553            $context['numberItems'] = $session->getRouteCount(AssessmentTestSession::ROUTECOUNT_FLOW);
554
555            // Number of items completed during the test session.
556            $context['numberCompleted'] = self::testCompletion($session);
557
558            // Number of items presented during the test session.
559            $context['numberPresented'] = $session->numberPresented();
560
561            // Whether or not the progress of the test can be inferred.
562            $context['considerProgress'] = self::considerProgress($session, $testMeta, $config);
563
564            // Whether or not the deepest current section is visible.
565            $context['isDeepestSectionVisible'] = $session->getCurrentAssessmentSection()->isVisible();
566
567            // The URLs to be called to move forward/backward in the Assessment Test Session or skip or comment.
568            $context['moveForwardUrl'] = self::buildActionCallUrl(
569                $session,
570                'moveForward',
571                $qtiTestDefinitionUri,
572                $qtiTestCompilationUri,
573                $standalone
574            );
575            $context['moveBackwardUrl'] = self::buildActionCallUrl(
576                $session,
577                'moveBackward',
578                $qtiTestDefinitionUri,
579                $qtiTestCompilationUri,
580                $standalone
581            );
582            $context['nextSectionUrl'] = self::buildActionCallUrl(
583                $session,
584                'nextSection',
585                $qtiTestDefinitionUri,
586                $qtiTestCompilationUri,
587                $standalone
588            );
589            $context['skipUrl'] = self::buildActionCallUrl(
590                $session,
591                'skip',
592                $qtiTestDefinitionUri,
593                $qtiTestCompilationUri,
594                $standalone
595            );
596            $context['commentUrl'] = self::buildActionCallUrl(
597                $session,
598                'comment',
599                $qtiTestDefinitionUri,
600                $qtiTestCompilationUri,
601                $standalone
602            );
603            $context['timeoutUrl'] = self::buildActionCallUrl(
604                $session,
605                'timeout',
606                $qtiTestDefinitionUri,
607                $qtiTestCompilationUri,
608                $standalone
609            );
610            $context['endTestSessionUrl'] = self::buildActionCallUrl(
611                $session,
612                'endTestSession',
613                $qtiTestDefinitionUri,
614                $qtiTestCompilationUri,
615                $standalone
616            );
617            $context['keepItemTimedUrl'] = self::buildActionCallUrl(
618                $session,
619                'keepItemTimed',
620                $qtiTestDefinitionUri,
621                $qtiTestCompilationUri,
622                $standalone
623            );
624            // If the candidate is allowed to move backward e.g. first item of the test.
625            $context['canMoveBackward'] = $session->canMoveBackward();
626
627            // The places in the test session where the candidate is allowed to jump to.
628            $context['jumps'] = self::buildPossibleJumps($session);
629
630            // The test review screen setup
631            if (!empty($config['test-taker-review']) && $context['considerProgress']) {
632                // The navigation map in order to build the test navigator
633                $navigator = self::getNavigatorMap($session, $itemIndex);
634                if ($navigator !== NavigationMode::LINEAR) {
635                    $context['navigatorMap'] = $navigator['map'];
636                    $context['itemFlagged'] = self::getItemFlag($session, $context['itemPosition']);
637                } else {
638                    $navigator = self::countItems($session);
639                }
640
641                // Extract the progression stats
642                $context['numberFlagged'] = $navigator['numberItemsFlagged'];
643                $context['numberItemsPart'] = $navigator['numberItemsPart'];
644                $context['numberItemsSection'] = $navigator['numberItemsSection'];
645                $context['numberCompletedPart'] = $navigator['numberCompletedPart'];
646                $context['numberCompletedSection'] = $navigator['numberCompletedSection'];
647                $context['numberPresentedPart'] = $navigator['numberPresentedPart'];
648                $context['numberPresentedSection'] = $navigator['numberPresentedSection'];
649                $context['numberFlaggedPart'] = $navigator['numberFlaggedPart'];
650                $context['numberFlaggedSection'] = $navigator['numberFlaggedSection'];
651                $context['itemPositionPart'] = $navigator['itemPositionPart'];
652                $context['itemPositionSection'] = $navigator['itemPositionSection'];
653
654                // The URLs to be called to move to a particular item in the Assessment Test Session or mark item for
655                // later review.
656                $context['jumpUrl'] = self::buildActionCallUrl(
657                    $session,
658                    'jumpTo',
659                    $qtiTestDefinitionUri,
660                    $qtiTestCompilationUri,
661                    $standalone
662                );
663                $context['markForReviewUrl'] = self::buildActionCallUrl(
664                    $session,
665                    'markForReview',
666                    $qtiTestDefinitionUri,
667                    $qtiTestCompilationUri,
668                    $standalone
669                );
670            } else {
671                // Setup data for progress bar when displaying position and timed section exit control
672                $numberItems = self::countItems($session);
673                $context['numberCompletedPart'] = $numberItems['numberCompletedPart'];
674                $context['numberCompletedSection'] = $numberItems['numberCompletedSection'];
675                $context['numberItemsSection'] = $numberItems['numberItemsSection'];
676                $context['numberItemsPart'] = $numberItems['numberItemsPart'];
677                $context['itemPositionPart'] = $numberItems['itemPositionPart'];
678                $context['itemPositionSection'] = $numberItems['itemPositionSection'];
679            }
680
681            // The code to be executed to build the ServiceApi object to be injected in the QTI Item frame.
682            $context['itemServiceApiCall'] = self::buildServiceApi(
683                $session,
684                $qtiTestDefinitionUri,
685                $qtiTestCompilationUri
686            );
687
688            // Rubric Blocks.
689            /** @var QtiRunnerRubric $rubricBlockHelper */
690            $rubricBlockHelper = self::getServiceManager()->get(QtiRunnerRubric::SERVICE_ID);
691            $context['rubrics'] = $rubricBlockHelper->getRubricBlock(
692                $session->getRoute()->current(),
693                $session,
694                $compilationDirs
695            );
696
697            // Comment allowed? Skipping allowed? Logout or Exit allowed ?
698            $context['allowComment'] = self::doesAllowComment($session);
699            $context['allowSkipping'] = self::doesAllowSkipping($session);
700            $context['exitButton'] = self::doesAllowExit($session);
701            $context['logoutButton'] = self::doesAllowLogout($session);
702            $context['categories'] = self::getCategories($session);
703
704            // loads the specific config into the context object
705            $configMap = [
706                // name in config                   => name in context object
707                'timerWarning'                      => 'timerWarning',
708                'timerWarningForScreenreader'       => 'timerWarningForScreenreader',
709                'progress-indicator'                => 'progressIndicator',
710                'progress-indicator-scope'          => 'progressIndicatorScope',
711                'test-taker-review'                 => 'reviewScreen',
712                'test-taker-review-region'          => 'reviewRegion',
713                'test-taker-review-scope'           => 'reviewScope',
714                'test-taker-review-prevents-unseen' => 'reviewPreventsUnseen',
715                'test-taker-review-can-collapse'    => 'reviewCanCollapse',
716                'next-section'                      => 'nextSection',
717                'keep-timer-up-to-timeout'          => 'keepTimerUpToTimeout',
718            ];
719            foreach ($configMap as $configKey => $contextKey) {
720                if (isset($config[$configKey])) {
721                    $context[$contextKey] = $config[$configKey];
722                }
723            }
724
725            // optionally extend the context
726            if (isset($config['extraContextBuilder']) && class_exists($config['extraContextBuilder'])) {
727                $builder = new $config['extraContextBuilder']();
728                if ($builder instanceof \oat\taoQtiTest\models\TestContextBuilder) {
729                    $builder->extendAssessmentTestContext(
730                        $context,
731                        $session,
732                        $testMeta,
733                        $qtiTestDefinitionUri,
734                        $qtiTestCompilationUri,
735                        $standalone,
736                        $compilationDirs
737                    );
738                } else {
739                    common_Logger::d(
740                        'Try to use an extra context builder class that is not an instance of '
741                        . '\\oat\\taoQtiTest\\models\\TestContextBuilder!'
742                    );
743                }
744            }
745        }
746
747        return $context;
748    }
749
750    /**
751     * Gets the item reference for a particular item in the test
752     *
753     * @param AssessmentTestSession $session
754     * @param string|Jump|RouteItem $itemPosition
755     * @return null|string
756     */
757    public static function getItemRef(
758        AssessmentTestSession $session,
759        $itemPosition,
760        RunnerServiceContext $context = null
761    ) {
762        $sessionId = $session->getSessionId();
763
764        $itemRef = null;
765        $routeItem = null;
766
767        if ($itemPosition && is_object($itemPosition)) {
768            if ($itemPosition instanceof RouteItem) {
769                $routeItem = $itemPosition;
770            } elseif ($itemPosition instanceof Jump) {
771                $routeItem = $itemPosition->getTarget();
772            }
773        } elseif ($context) {
774            $itemId = '';
775            $itemPosition = $context->getItemPositionInRoute($itemPosition, $itemId);
776
777            if ($itemId !== '') {
778                $itemRef = $itemId;
779            } else {
780                $routeItem = $session->getRoute()->getRouteItemAt($itemPosition);
781            }
782        } else {
783            $jumps = $session->getPossibleJumps();
784            foreach ($jumps as $jump) {
785                if ($itemPosition == $jump->getPosition()) {
786                    $routeItem = $jump->getTarget();
787                    break;
788                }
789            }
790        }
791
792        if ($routeItem) {
793            $itemRef = (string)$routeItem->getAssessmentItemRef();
794        }
795
796        return $itemRef;
797    }
798
799    /**
800     * Sets an item to be reviewed
801     * @param AssessmentTestSession $session
802     * @param string|Jump|RouteItem $itemPosition
803     * @param bool $flag
804     * @return bool
805     * @throws common_exception_Error
806     */
807    public static function setItemFlag(
808        AssessmentTestSession $session,
809        $itemPosition,
810        $flag,
811        RunnerServiceContext $context = null
812    ) {
813
814        $itemRef = self::getItemRef($session, $itemPosition, $context);
815        $result = self::getExtendedStateService()->setItemFlag($session->getSessionId(), $itemRef, $flag);
816
817        return $result;
818    }
819
820    /**
821     * Gets the marked for review state of an item
822     * @param AssessmentTestSession $session
823     * @param string|Jump|RouteItem $itemPosition
824     * @return bool
825     * @throws common_exception_Error
826     */
827    public static function getItemFlag(
828        AssessmentTestSession $session,
829        $itemPosition,
830        RunnerServiceContext $context = null
831    ) {
832        $result = false;
833
834        $itemRef = self::getItemRef($session, $itemPosition, $context);
835        if ($itemRef) {
836            $result = self::getExtendedStateService()->getItemFlag($session->getSessionId(), $itemRef);
837        }
838
839        return $result;
840    }
841
842    /**
843     * Gets the usage of an item
844     * @param RouteItem $routeItem
845     * @return string Return the usage, can be: default, informational, seeding
846     */
847    public static function getItemUsage(RouteItem $routeItem)
848    {
849        $itemRef = $routeItem->getAssessmentItemRef();
850        $categories = $itemRef->getCategories()->getArrayCopy();
851        $prefixCategory = 'x-tao-itemusage-';
852        $prefixCategoryLen = strlen($prefixCategory);
853        foreach ($categories as $category) {
854            if (!strncmp($category, $prefixCategory, $prefixCategoryLen)) {
855                // extract the option name from the category, transform to camelCase if needed
856                return lcfirst(
857                    str_replace(
858                        ' ',
859                        '',
860                        ucwords(strtr(substr($category, $prefixCategoryLen), ['-' => ' ', '_' => ' ']))
861                    )
862                );
863            }
864        }
865
866        return 'default';
867    }
868
869    /**
870     * Checks if an item is informational
871     * @param RouteItem $routeItem
872     * @param AssessmentItemSession $itemSession
873     * @return bool
874     */
875    public static function isItemInformational(RouteItem $routeItem, AssessmentItemSession $itemSession)
876    {
877        return !count($itemSession->getAssessmentItem()->getResponseDeclarations())
878            || 'informational' == self::getItemUsage($routeItem);
879    }
880
881    /**
882     * Checks if an item has been completed
883     * @param RouteItem $routeItem
884     * @param AssessmentItemSession $itemSession
885     * @param bool $partially (optional) Whether or not consider partially responded sessions as responded.
886     * @return bool
887     */
888    public static function isItemCompleted(RouteItem $routeItem, AssessmentItemSession $itemSession, $partially = true)
889    {
890        $completed = false;
891        if ($routeItem->getTestPart()->getNavigationMode() === NavigationMode::LINEAR) {
892            // In linear mode, we consider the item completed if it was presented.
893            if ($itemSession->isPresented() === true) {
894                $completed = true;
895            }
896        } else {
897            // In nonlinear mode we consider:
898            // - an adaptive item completed if it's completion status is 'completed'.
899            // - a non-adaptive item to be completed if it is responded.
900            $isAdaptive = $itemSession->getAssessmentItem()->isAdaptive();
901
902            if (
903                $isAdaptive === true
904                && $itemSession['completionStatus']->getValue() === AssessmentItemSession::COMPLETION_STATUS_COMPLETED
905            ) {
906                $completed = true;
907            } elseif ($isAdaptive === false && $itemSession->isResponded($partially) === true) {
908                $completed = true;
909            }
910        }
911
912        return $completed;
913    }
914
915    /**
916     * Gets infos about a particular item
917     * @param AssessmentTestSession $session
918     * @param Jump $jump
919     * @return array
920     */
921    private static function getItemInfo(AssessmentTestSession $session, Jump $jump)
922    {
923        $itemSession = $jump->getItemSession();
924        $routeItem = $jump->getTarget();
925        return [
926            'remainingAttempts' => $itemSession->getRemainingAttempts(),
927            'answered' => self::isItemCompleted($routeItem, $itemSession),
928            'viewed' => $itemSession->isPresented(),
929            'flagged' => self::getItemFlag($session, $jump),
930            'position' => $jump->getPosition()
931        ];
932    }
933
934    /**
935     * Builds a map of available jumps and count the flagged items
936     * @param AssessmentTestSession $session
937     * @param array $jumps
938     * @return array
939     */
940    private static function getJumpsMap(AssessmentTestSession $session, $jumps)
941    {
942        $jumpsMap = [];
943        $numberItemsFlagged = 0;
944        foreach ($jumps as $jump) {
945            $routeItem = $jump->getTarget();
946            $partId = $routeItem->getTestPart()->getIdentifier();
947            $sections = $routeItem->getAssessmentSections();
948            $sections->rewind();
949            $sectionId = key(current($sections));
950            $itemId = $routeItem->getAssessmentItemRef()->getIdentifier();
951
952            $jumpsMap[$partId][$sectionId][$itemId] = self::getItemInfo($session, $jump);
953            if ($jumpsMap[$partId][$sectionId][$itemId]['flagged']) {
954                $numberItemsFlagged++;
955            }
956        }
957
958        return [
959            'flagged' => $numberItemsFlagged,
960            'map' => $jumpsMap,
961        ];
962    }
963
964    /**
965     * Gets the section map for navigation between test parts, sections and items.
966     *
967     * @param AssessmentTestSession $session
968     * @param QtiTestCompilerIndex $itemIndex
969     * @return array A navigator map (parts, sections, items so on)
970     */
971    private static function getNavigatorMap(AssessmentTestSession $session, $itemIndex)
972    {
973
974        // get jumps
975        $jumps = $session->getPossibleJumps();
976
977        // no jumps, notify linear-mode
978        if (!$jumps->count()) {
979            return NavigationMode::LINEAR;
980        }
981
982        $jumpsMapInfo = self::getJumpsMap($session, $jumps);
983        $jumpsMap = $jumpsMapInfo['map'];
984        $numberItemsFlagged = $jumpsMapInfo['flagged'];
985
986
987        // the active test-part identifier
988        $activePart = $session->getCurrentTestPart()->getIdentifier();
989
990        // the active section identifier
991        $activeSection = $session->getCurrentAssessmentSection()->getIdentifier();
992
993        $route = $session->getRoute();
994
995        $activeItem = $session->getCurrentAssessmentItemRef()->getIdentifier();
996        if (isset($jumpsMap[$activePart][$activeSection][$activeItem])) {
997            $jumpsMap[$activePart][$activeSection][$activeItem]['active'] = true;
998        }
999
1000        // current position
1001        $oldPosition = $route->getPosition();
1002
1003        $route->setPosition($oldPosition);
1004
1005        // get config for the sequence number option
1006        $config = common_ext_ExtensionsManager::singleton()
1007            ->getExtensionById('taoQtiTest')
1008            ->getConfig('testRunner');
1009        $forceTitles = !empty($config['test-taker-review-force-title']);
1010        $uniqueTitle = isset($config['test-taker-review-item-title']) ? $config['test-taker-review-item-title'] : '%d';
1011        $useTitle = !empty($config['test-taker-review-use-title']);
1012        $language = \common_session_SessionManager::getSession()->getInterfaceLanguage();
1013
1014        $returnValue = [];
1015        $testParts   = [];
1016        $testPartIdx = 0;
1017        $numberItemsPart = 0;
1018        $numberItemsSection = 0;
1019        $numberCompletedPart = 0;
1020        $numberCompletedSection = 0;
1021        $numberPresentedPart = 0;
1022        $numberPresentedSection = 0;
1023        $numberFlaggedPart = 0;
1024        $numberFlaggedSection = 0;
1025        $itemPositionPart = 0;
1026        $itemPositionSection = 0;
1027        $itemPosition = $session->getRoute()->getPosition();
1028
1029        foreach ($jumps as $jump) {
1030            $testPart = $jump->getTarget()->getTestPart();
1031            $id = $testPart->getIdentifier();
1032
1033            if (isset($testParts[$id])) {
1034                continue;
1035            }
1036
1037            $sections = [];
1038
1039            if ($testPart->getNavigationMode() == NavigationMode::NONLINEAR) {
1040                $firstPositionPart = PHP_INT_MAX;
1041                foreach ($testPart->getAssessmentSections() as $sectionId => $section) {
1042                    $completed = 0;
1043                    $presented = 0;
1044                    $flagged = 0;
1045                    $items = [];
1046                    $firstPositionSection = PHP_INT_MAX;
1047                    $positionInSection = 0;
1048
1049                    foreach ($section->getSectionParts() as $itemId => $item) {
1050                        if (isset($jumpsMap[$id][$sectionId][$itemId])) {
1051                            $jumpInfo = $jumpsMap[$id][$sectionId][$itemId];
1052                            $itemUri = strstr($item->getHref(), '|', true);
1053                            $resItem  =  new \core_kernel_classes_Resource($itemUri);
1054                            if ($jumpInfo['answered']) {
1055                                ++$completed;
1056                            }
1057                            if ($jumpInfo['viewed']) {
1058                                ++$presented;
1059                            }
1060                            if ($jumpInfo['flagged']) {
1061                                ++$flagged;
1062                            }
1063                            if ($forceTitles) {
1064                                $label = sprintf($uniqueTitle, ++$positionInSection);
1065                            } else {
1066                                if ($useTitle) {
1067                                    $label = $itemIndex->getItemValue($itemUri, $language, 'title');
1068                                } else {
1069                                    $label = '';
1070                                }
1071
1072                                if (!$label) {
1073                                    $label = $itemIndex->getItemValue($itemUri, $language, 'label');
1074                                }
1075
1076                                if (!$label) {
1077                                    $label = $resItem->getLabel();
1078                                }
1079                            }
1080                            $items[]  = array_merge(
1081                                [
1082                                    'id' => $itemId,
1083                                    'label' => $label,
1084                                ],
1085                                $jumpInfo
1086                            );
1087
1088                            $firstPositionPart = min($firstPositionPart, $jumpInfo['position']);
1089                            $firstPositionSection = min($firstPositionSection, $jumpInfo['position']);
1090                        }
1091                    }
1092
1093                    $sectionData = [
1094                        'id'       => $sectionId,
1095                        'active'   => $sectionId === $activeSection,
1096                        'label'    => $section->getTitle(),
1097                        'answered' => $completed,
1098                        'items'    => $items
1099                    ];
1100                    $sections[] = $sectionData;
1101
1102                    if ($sectionData['active']) {
1103                        $numberItemsSection = count($items);
1104                        $itemPositionSection = $itemPosition - $firstPositionSection;
1105                        $numberCompletedSection = $completed;
1106                        $numberPresentedSection = $presented;
1107                        $numberFlaggedSection = $flagged;
1108                    }
1109                    if ($id === $activePart) {
1110                        $numberItemsPart += count($items);
1111                        $numberCompletedPart += $completed;
1112                        $numberPresentedPart += $presented;
1113                        $numberFlaggedPart += $flagged;
1114                    }
1115                }
1116
1117                if ($id === $activePart) {
1118                    $itemPositionPart = $itemPosition - $firstPositionPart;
1119                }
1120            }
1121
1122            $data = [
1123                'id'       => $id,
1124                'sections' => $sections,
1125                'active'   => $id === $activePart,
1126                'label'    => __('Part %d', ++$testPartIdx),
1127            ];
1128            if (empty($sections)) {
1129                $item = current(current($jumpsMap[$id]));
1130                $data['position'] = $item['position'];
1131                $data['itemId'] = key(current($jumpsMap[$id]));
1132            }
1133            $returnValue[] = $data;
1134            $testParts[$id] = false;
1135        }
1136
1137        return [
1138            'map' => $returnValue,
1139            'numberItemsFlagged' => $numberItemsFlagged,
1140            'numberItemsPart' => $numberItemsPart,
1141            'numberItemsSection' => $numberItemsSection,
1142            'numberCompletedPart' => $numberCompletedPart,
1143            'numberCompletedSection' => $numberCompletedSection,
1144            'numberPresentedPart' => $numberPresentedPart,
1145            'numberPresentedSection' => $numberPresentedSection,
1146            'numberFlaggedPart' => $numberFlaggedPart,
1147            'numberFlaggedSection' => $numberFlaggedSection,
1148            'itemPositionPart' => $itemPositionPart,
1149            'itemPositionSection' => $itemPositionSection,
1150        ];
1151    }
1152
1153    /**
1154     * Gets the number of items within the current section and the current part.
1155     *
1156     * @param AssessmentTestSession $session
1157     * @return array The list of counters (numberItemsSection and numberItemsPart)
1158     */
1159    private static function countItems(AssessmentTestSession $session)
1160    {
1161        // get jumps
1162        $jumps = self::getTestMap($session);
1163
1164        // the active test-part identifier
1165        $activePart = $session->getCurrentTestPart()->getIdentifier();
1166
1167        // the active section identifier
1168        $activeSection = $session->getCurrentAssessmentSection()->getIdentifier();
1169
1170        $jumpsMapInfo = self::getJumpsMap($session, $jumps);
1171        $jumpsMap = $jumpsMapInfo['map'];
1172        $numberItemsFlagged = $jumpsMapInfo['flagged'];
1173
1174        $testParts = [];
1175        $numberItemsPart = 0;
1176        $numberItemsSection = 0;
1177        $numberCompletedPart = 0;
1178        $numberCompletedSection = 0;
1179        $numberPresentedPart = 0;
1180        $numberPresentedSection = 0;
1181        $numberFlaggedPart = 0;
1182        $numberFlaggedSection = 0;
1183        $itemPositionPart = 0;
1184        $itemPositionSection = 0;
1185        $itemPosition = $session->getRoute()->getPosition();
1186        foreach ($jumps as $jump) {
1187            $testPart = $jump->getTarget()->getTestPart();
1188            $id = $testPart->getIdentifier();
1189
1190            if (isset($testParts[$id])) {
1191                continue;
1192            }
1193            $testParts[$id] = true;
1194
1195            $firstPositionPart = PHP_INT_MAX;
1196            foreach ($testPart->getAssessmentSections() as $sectionId => $section) {
1197                $completed = 0;
1198                $presented = 0;
1199                $flagged = 0;
1200                $numberItems = count($section->getSectionParts());
1201                $firstPositionSection = PHP_INT_MAX;
1202                foreach ($section->getSectionParts() as $itemId => $item) {
1203                    if (isset($jumpsMap[$id][$sectionId][$itemId])) {
1204                        $jumpInfo = $jumpsMap[$id][$sectionId][$itemId];
1205
1206                        if ($jumpInfo['answered']) {
1207                            ++$completed;
1208                        }
1209                        if ($jumpInfo['viewed']) {
1210                            ++$presented;
1211                        }
1212                        if ($jumpInfo['flagged']) {
1213                            ++$flagged;
1214                        }
1215
1216                        $firstPositionPart = min($firstPositionPart, $jumpInfo['position']);
1217                        $firstPositionSection = min($firstPositionSection, $jumpInfo['position']);
1218                    }
1219                }
1220
1221                if ($sectionId === $activeSection) {
1222                    $numberItemsSection = $numberItems;
1223                    $itemPositionSection = $itemPosition - $firstPositionSection;
1224                    $numberCompletedSection = $completed;
1225                    $numberPresentedSection = $presented;
1226                    $numberFlaggedSection = $flagged;
1227                }
1228                if ($id === $activePart) {
1229                    $numberItemsPart += $numberItems;
1230                    $numberCompletedPart += $completed;
1231                    $numberPresentedPart += $presented;
1232                    $numberFlaggedPart += $flagged;
1233                }
1234            }
1235            if ($id === $activePart) {
1236                $itemPositionPart = $itemPosition - $firstPositionPart;
1237            }
1238        }
1239
1240        return [
1241            'numberItemsFlagged' => $numberItemsFlagged,
1242            'numberItemsPart' => $numberItemsPart,
1243            'numberItemsSection' => $numberItemsSection,
1244            'numberCompletedPart' => $numberCompletedPart,
1245            'numberCompletedSection' => $numberCompletedSection,
1246            'numberPresentedPart' => $numberPresentedPart,
1247            'numberPresentedSection' => $numberPresentedSection,
1248            'numberFlaggedPart' => $numberFlaggedPart,
1249            'numberFlaggedSection' => $numberFlaggedSection,
1250            'itemPositionPart' => $itemPositionPart,
1251            'itemPositionSection' => $itemPositionSection,
1252        ];
1253    }
1254
1255    /**
1256     * Gets the map of the reachable items.
1257     * @param AssessmentTestSession $session
1258     * @return array The map of the test
1259     */
1260    public static function getTestMap($session)
1261    {
1262        $map = [];
1263
1264        if ($session->isRunning() !== false) {
1265            $route = $session->getRoute();
1266            $routeItems = $route->getAllRouteItems();
1267            $offset = $route->getRouteItemPosition($routeItems[0]);
1268            foreach ($routeItems as $routeItem) {
1269                $itemRef = $routeItem->getAssessmentItemRef();
1270                $occurrence = $routeItem->getOccurence();
1271
1272                // get the session related to this route item.
1273                $store = $session->getAssessmentItemSessionStore();
1274                $itemSession = $store->getAssessmentItemSession($itemRef, $occurrence);
1275                $map[] = new Jump($offset, $routeItem, $itemSession);
1276                $offset++;
1277            }
1278        }
1279
1280        return $map;
1281    }
1282
1283    /**
1284     * Compute the the number of completed items during a given
1285     * candidate test $session.
1286     *
1287     * @param AssessmentTestSession $session
1288     * @return integer
1289     */
1290    public static function testCompletion(AssessmentTestSession $session)
1291    {
1292        $completed = $session->numberCompleted();
1293
1294        if ($session->getCurrentNavigationMode() === NavigationMode::LINEAR && $completed > 0) {
1295            $completed--;
1296        }
1297
1298        return $completed;
1299    }
1300
1301    /**
1302     * Checks if the current test allows the progress bar to be displayed
1303     * @param AssessmentTestSession $session
1304     * @param array $testMeta
1305     * @param array $config
1306     * @return bool
1307     */
1308    public static function considerProgress(AssessmentTestSession $session, array $testMeta, array $config = [])
1309    {
1310        $considerProgress = true;
1311
1312        if (!empty($config['progress-indicator-forced'])) {
1313            // Caution: this piece of code can introduce a heavy load on very large tests
1314            // The local optimisation made here concerns:
1315            // - only check the part branchRules if the progress indicator must be forced for all tests
1316            // - branchRules check is ignored when the navigation mode is non linear.
1317            //
1318            // TODO: Perform this check at compilation time and store a map of parts options.
1319            //       This can be also done for navigation map (see getNavigatorMap and getJumpsMap)
1320
1321            $testPart = $session->getCurrentTestPart();
1322            if ($testPart->getNavigationMode() !== NavigationMode::NONLINEAR) {
1323                $branchings = $testPart->getComponentsByClassName('branchRule');
1324
1325                if (count($branchings) > 0) {
1326                    $considerProgress = false;
1327                }
1328            }
1329        } else {
1330            if ($testMeta['preConditions'] === true) {
1331                $considerProgress = false;
1332            } elseif ($testMeta['branchRules'] === true) {
1333                $considerProgress = false;
1334            }
1335        }
1336
1337        return $considerProgress;
1338    }
1339
1340    /**
1341     * Checks if the current session can be exited. If a context is pass we use it over the session
1342     *
1343     * @param AssessmentTestSession $session
1344     * @param RunnerServiceContext $context
1345     * @return bool
1346     */
1347    public static function doesAllowExit(AssessmentTestSession $session, RunnerServiceContext $context = null)
1348    {
1349        $config = common_ext_ExtensionsManager::singleton()
1350            ->getExtensionById('taoQtiTest')
1351            ->getConfig('testRunner');
1352        $exitButton = (isset($config['exitButton']) && $config['exitButton']);
1353        $categories = self::getCategories($session, $context);
1354        return ($exitButton && in_array('x-tao-option-exit', $categories));
1355    }
1356
1357    /**
1358     * Checks if the test taker can logout
1359     *
1360     * @param AssessmentTestSession $session
1361     * @return type
1362     */
1363    public static function doesAllowLogout(AssessmentTestSession $session)
1364    {
1365        $config = common_ext_ExtensionsManager::singleton()
1366            ->getExtensionById('taoQtiTest')
1367            ->getConfig('testRunner');
1368        return !(isset($config['exitButton']) && $config['exitButton']);
1369    }
1370
1371    /**
1372     * Get the array of available categories for the current itemRef
1373     * If we have a non null context we use it over the session
1374     *
1375     * @param \qtism\runtime\tests\AssessmentTestSession $session
1376     * @param RunnerServiceContext $context
1377     * @return array
1378     */
1379    public static function getCategories(AssessmentTestSession $session, RunnerServiceContext $context = null)
1380    {
1381        if (!is_null($context)) {
1382            return $context->getCurrentAssessmentItemRef()->getCategories()->getArrayCopy();
1383        }
1384        return $session->getCurrentAssessmentItemRef()->getCategories()->getArrayCopy();
1385    }
1386
1387
1388    /**
1389     * Get the array of available categories for the test
1390     *
1391     * @param \qtism\runtime\tests\AssessmentTestSession $session
1392     * @return array
1393     */
1394    public static function getAllCategories(AssessmentTestSession $session)
1395    {
1396        $prevCategories = null;
1397        $assessmentItemRefs = $session->getAssessmentTest()->getComponentsByClassName('assessmentItemRef');
1398
1399        /** @var \qtism\data\AssessmentItemRef $assessmentItemRef */
1400        foreach ($assessmentItemRefs as $assessmentItemRef) {
1401            $categories = $assessmentItemRef->getCategories();
1402            if (!is_null($prevCategories)) {
1403                $prevCategories->merge($categories);
1404            } else {
1405                $prevCategories = $categories;
1406            }
1407        }
1408
1409        return (!is_null($prevCategories)) ? array_unique($prevCategories->getArrayCopy()) : [];
1410    }
1411
1412    /**
1413     * Whether or not $value is considered as a null QTI value.
1414     *
1415     * @param $value
1416     * @return boolean
1417     */
1418    public static function isQtiValueNull($value)
1419    {
1420        return is_null($value) === true
1421            || ($value instanceof QtiString && $value->getValue() === '')
1422            || ($value instanceof Container && count($value) === 0);
1423    }
1424
1425    /**
1426     * Gets the amount of seconds with the microseconds as fractional part from a Duration instance.
1427     * @param QtiDuration $duration
1428     * @return float|null
1429     */
1430    public static function getDurationWithMicroseconds($duration)
1431    {
1432        if ($duration) {
1433            if (method_exists($duration, 'getMicroseconds')) {
1434                return $duration->getMicroseconds(true) / 1e6;
1435            }
1436            return $duration->getSeconds(true);
1437        }
1438        return null;
1439    }
1440}