Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
40.00% covered (danger)
40.00%
70 / 175
52.94% covered (warning)
52.94%
9 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
DeliveryExecutionManagerService
40.00% covered (danger)
40.00%
70 / 175
52.94% covered (warning)
52.94%
9 / 17
733.38
0.00% covered (danger)
0.00%
0 / 1
 getDeliveryExecutionById
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getServiceProxy
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDeliveryTimer
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 getPartTimeLimits
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
30
 getTimeLimits
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
72
 setExtraTime
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
56
 updateDeliveryExtendedTime
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
42
 adjustTimers
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
5
 adjustDeliveryExecutionTimer
61.54% covered (warning)
61.54%
8 / 13
0.00% covered (danger)
0.00%
0 / 1
2.23
 isTimerAdjustmentAllowed
72.73% covered (warning)
72.73%
8 / 11
0.00% covered (danger)
0.00%
0 / 1
5.51
 getTimerAdjustmentDecreaseLimit
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getTimerAdjustmentIncreaseLimit
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAdjustedTime
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 getTestSessionService
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTimerAdjustmentService
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDeliveryMonitoringService
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSmallestMaxTimeConstraint
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; under version 2
7 * of the License (non-upgradable).
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 *
18 * Copyright (c) 2017 (original work) Open Assessment Technologies SA;
19 *
20 */
21
22declare(strict_types=1);
23
24namespace oat\taoProctoring\model\execution;
25
26use common_Exception;
27use common_exception_Error;
28use common_exception_MissingParameter;
29use common_exception_NotFound;
30use common_ext_ExtensionException;
31use common_session_Session;
32use Exception;
33use oat\oatbox\event\EventManager;
34use oat\oatbox\service\ConfigurableService;
35use oat\oatbox\service\exception\InvalidServiceManagerException;
36use oat\oatbox\session\SessionService;
37use oat\taoDelivery\model\execution\DeliveryExecution as BaseDeliveryExecution;
38use oat\taoDelivery\model\execution\DeliveryExecutionInterface;
39use oat\taoDelivery\model\execution\ServiceProxy;
40use oat\taoProctoring\model\event\DeliveryExecutionTimerAdjusted;
41use oat\taoProctoring\model\implementation\TestSessionService;
42use oat\taoProctoring\model\monitorCache\DeliveryMonitoringData;
43use oat\taoProctoring\model\monitorCache\DeliveryMonitoringService;
44use oat\taoQtiTest\models\QtiTestExtractionFailedException;
45use oat\taoQtiTest\models\runner\session\TestSession;
46use oat\taoQtiTest\models\runner\StorageManager;
47use oat\taoQtiTest\models\runner\time\QtiTimeConstraint;
48use oat\taoQtiTest\models\runner\time\QtiTimer;
49use oat\taoQtiTest\models\runner\time\QtiTimerFactory;
50use oat\taoQtiTest\models\runner\time\TimerAdjustmentService;
51use oat\taoTests\models\runner\time\TimePoint;
52use oat\taoQtiTest\models\runner\time\TimerAdjustmentServiceInterface;
53use oat\taoTests\models\runner\time\TimerStrategyInterface;
54use qtism\common\datatypes\QtiDuration;
55use qtism\data\AssessmentTest;
56use qtism\data\QtiIdentifiable;
57use qtism\runtime\tests\AssessmentTestSessionState;
58
59/**
60 * Class DeliveryExecutionManagerService
61 * @package oat\taoProctoring\model\execution
62 */
63class DeliveryExecutionManagerService extends ConfigurableService
64{
65    public const SERVICE_ID = 'taoProctoring/DeliveryExecutionManagerService';
66
67    protected const NO_TIME_ADJUSTMENT_LIMIT = -1;
68
69    private $deliveryExecutions = [];
70
71    /**
72     * @param $deliveryExecutionId
73     * @return BaseDeliveryExecution
74     */
75    public function getDeliveryExecutionById($deliveryExecutionId): BaseDeliveryExecution
76    {
77        if (!isset($this->deliveryExecutions[$deliveryExecutionId])) {
78            $deliveryExecution = $this->getServiceProxy()
79                ->getDeliveryExecution($deliveryExecutionId);
80            $this->deliveryExecutions[$deliveryExecutionId] = $deliveryExecution;
81        }
82
83        return $this->deliveryExecutions[$deliveryExecutionId];
84    }
85
86    /**
87     * @return ServiceProxy|object
88     */
89    private function getServiceProxy()
90    {
91        return $this->getServiceLocator()->get(ServiceProxy::SERVICE_ID);
92    }
93
94    /**
95     * Gets the delivery time counter
96     *
97     * @param DeliveryExecutionInterface $deliveryExecution
98     * @return QtiTimer
99     * @throws InvalidServiceManagerException
100     * @throws QtiTestExtractionFailedException
101     * @throws common_Exception
102     * @throws common_exception_Error
103     * @throws common_exception_NotFound
104     * @throws common_ext_ExtensionException
105     */
106    public function getDeliveryTimer($deliveryExecution)
107    {
108        if (is_string($deliveryExecution)) {
109            $deliveryExecution = $this->getDeliveryExecutionById($deliveryExecution);
110        }
111
112        $testSession = $this->getTestSessionService()->getTestSession($deliveryExecution, true);
113        if ($testSession instanceof TestSession) {
114            $timer = $testSession->getTimer();
115        } else {
116            $qtiTimerFactory = $this->getServiceLocator()->get(QtiTimerFactory::SERVICE_ID);
117            $timer = $qtiTimerFactory->getTimer(
118                $deliveryExecution->getIdentifier(),
119                $deliveryExecution->getUserIdentifier()
120            );
121        }
122
123        return $timer;
124    }
125
126    /**
127     * @param TestSession $testSession
128     * @param $part
129     * @return int|null
130     */
131    protected function getPartTimeLimits($testSession, $part)
132    {
133        $timeLimits = $part->getTimeLimits();
134        if ($timeLimits && ($maxTime = $timeLimits->getMaxTime()) !== null) {
135            if ($testSession !== null && ($timer = $testSession->getTimer()) !== null) {
136                $maxTime = $this->getTimerAdjustmentService()->getAdjustedMaxTime($part, $timer);
137            }
138            return $maxTime->getSeconds(true);
139        }
140        return null;
141    }
142
143    /**
144     * Gets the actual time limits for a test session
145     * @param TestSession $testSession
146     * @return int|null
147     */
148    public function getTimeLimits($testSession)
149    {
150        $seconds = null;
151
152        if ($item = $testSession->getCurrentAssessmentItemRef()) {
153            $seconds = $this->getPartTimeLimits($testSession, $item);
154        }
155
156        if (!$seconds && $section = $testSession->getCurrentAssessmentSection()) {
157            $seconds = $this->getPartTimeLimits($testSession, $section);
158        }
159
160        if (!$seconds && $testPart = $testSession->getCurrentTestPart()) {
161            $seconds = $this->getPartTimeLimits($testSession, $testPart);
162        }
163
164        if (!$seconds && $assessmentTest = $testSession->getAssessmentTest()) {
165            $seconds = $this->getPartTimeLimits($testSession, $assessmentTest);
166        }
167
168        return $seconds;
169    }
170
171    /**
172     * Sets the extra time to a list of delivery executions
173     * @param $deliveryExecutions
174     * @param int $extraTime
175     * @return array
176     * @throws common_exception_Error
177     * @throws common_exception_MissingParameter
178     * @throws common_exception_NotFound
179     * @throws \oat\taoTests\models\runner\time\InvalidStorageException
180     */
181    public function setExtraTime($deliveryExecutions, $extraTime = 0)
182    {
183        $deliveryMonitoringService = $this->getDeliveryMonitoringService();
184
185        $testSessionService = $this->getTestSessionService();
186
187        $result = ['processed' => [], 'unprocessed' => []];
188
189        /** @var DeliveryExecution $deliveryExecution */
190        foreach ($deliveryExecutions as $deliveryExecution) {
191            if (is_string($deliveryExecution)) {
192                $deliveryExecution = $this->getDeliveryExecutionById($deliveryExecution);
193            }
194
195            /** @var DeliveryMonitoringData $data */
196            $data = $deliveryMonitoringService->getData($deliveryExecution);
197            $maxTime = 0;
198            $timerTarget = TimePoint::TARGET_SERVER;
199
200            // reopen the execution if already closed
201            if ($deliveryExecution->getState()->getUri() == DeliveryExecution::STATE_FINISHED) {
202                $deliveryExecution->setState(DeliveryExecution::STATE_ACTIVE);
203
204                /* @var TestSession $testSession */
205                $testSession = $testSessionService->getTestSession($deliveryExecution);
206
207                if ($testSession) {
208                    $timerTarget = $testSession->getTimerTarget();
209                    $testSession->getRoute()->setPosition(0);
210                    $testSession->setState(AssessmentTestSessionState::INTERACTING);
211
212                    // The duration store contains durations (time spent) on test, testPart(s) and assessmentSection(s).
213                    $durationStore = $testSession->getDurationStore();
214
215                    $offsetDuration = new QtiDuration("PT${extraTime}S");
216                    $testDefinition = $testSession->getAssessmentTest();
217                    $currentDuration = $durationStore[$testDefinition->getIdentifier()];
218
219                    $offsetSeconds = $offsetDuration->getSeconds(true);
220                    $currentSeconds = $currentDuration->getSeconds(true);
221                    $newSeconds = $currentSeconds - $offsetSeconds;
222
223                    if ($newSeconds < 0) {
224                        $newSeconds = 0;
225                    }
226
227                    // Replace test duration with new duration.
228                    $durationStore[$testDefinition->getIdentifier()] = new QtiDuration("PT${newSeconds}S");
229
230                    $testSessionService->persist($testSession);
231                    $maxTime = $this->getPartTimeLimits($testSession, $testDefinition);
232                }
233            }
234
235            /** @var QtiTimer $timer */
236            $timer = $this->getDeliveryTimer($deliveryExecution);
237            $timer
238                ->setExtraTime($extraTime)
239                ->save();
240
241            $data->update(DeliveryMonitoringService::EXTRA_TIME, $timer->getExtraTime());
242            $data->update(
243                DeliveryMonitoringService::CONSUMED_EXTRA_TIME,
244                $timer->getConsumedExtraTime(null, $maxTime, $timerTarget)
245            );
246            if ($deliveryMonitoringService->save($data)) {
247                $result['processed'][$deliveryExecution->getIdentifier()] = true;
248            } else {
249                $result['unprocessed'][$deliveryExecution->getIdentifier()] = false;
250            }
251        }
252
253        $this->getServiceLocator()->get(StorageManager::SERVICE_ID)->persist();
254
255        return $result;
256    }
257
258    /**
259     * @param DeliveryExecutionInterface $deliveryExecution
260     * @param $extendedTime
261     * @throws common_exception_Error
262     * @throws common_exception_MissingParameter
263     * @throws common_exception_NotFound
264     * @throws \oat\taoTests\models\runner\time\InvalidStorageException
265     */
266    public function updateDeliveryExtendedTime(DeliveryExecutionInterface $deliveryExecution, $extendedTime)
267    {
268        $timer = $this->getDeliveryTimer($deliveryExecution);
269        if ($timer->getExtendedTime()) {
270            return;
271        }
272
273        $inputParameters = $this->getTestSessionService()->getRuntimeInputParameters($deliveryExecution);
274        /** @var AssessmentTest $testDefinition */
275        $testDefinition = \taoQtiTest_helpers_Utils::getTestDefinition($inputParameters['QtiTestCompilation']);
276        $components = $testDefinition->getComponentsByClassName(['testPart', 'assessmentSection', 'assessmentItemRef']);
277        $components->attach($testDefinition);
278
279        /** @var QtiIdentifiable $component */
280        foreach ($components as $component) {
281            $timeLimits = $component->getTimeLimits();
282            if ($timeLimits && $timeLimits->hasMaxTime()) {
283                $currentLimitSeconds = $timeLimits->getMaxTime()->getSeconds(true);
284                $increaseSeconds = (int) $this->getServiceLocator()
285                    ->get(TimerStrategyInterface::SERVICE_ID)
286                    ->getExtraTime($currentLimitSeconds, $extendedTime);
287                if ($increaseSeconds > 0) {
288                    $timer->getAdjustmentMap()->increase(
289                        $component->getIdentifier(),
290                        TimerAdjustmentServiceInterface::TYPE_EXTENDED_TIME,
291                        $increaseSeconds
292                    );
293                }
294            }
295        }
296        $timer->setExtendedTime($extendedTime);
297        $timer->save();
298        $this->getServiceLocator()->get(StorageManager::SERVICE_ID)->persist();
299
300        $deliveryMonitoringService = $this->getDeliveryMonitoringService();
301        $data = $deliveryMonitoringService->getData($deliveryExecution);
302        $data->update(DeliveryMonitoringService::EXTENDED_TIME, $timer->getExtendedTime());
303        $deliveryMonitoringService->save($data);
304    }
305
306    /**
307     * Registers timer adjustments to a list of delivery executions
308     * @param array $deliveryExecutions
309     * @param int $seconds
310     * @param array $reason
311     * @return array
312     * @throws InvalidServiceManagerException
313     * @throws QtiTestExtractionFailedException
314     * @throws common_Exception
315     * @throws common_exception_Error
316     * @throws common_exception_NotFound
317     * @throws common_ext_ExtensionException
318     */
319    public function adjustTimers(array $deliveryExecutions, int $seconds, array $reason = []): array
320    {
321        $result = ['processed' => [], 'unprocessed' => []];
322
323        $timerAdjustmentService = $this->getTimerAdjustmentService();
324        $deliveryMonitoringService = $this->getDeliveryMonitoringService();
325
326        /** @var common_session_Session $session */
327        $session = $this->getServiceLocator()->get(SessionService::SERVICE_ID)->getCurrentSession();
328        $proctor = $session->getUser();
329
330        $eventManager = $this->getServiceLocator()->get(EventManager::SERVICE_ID);
331
332        /** @var DeliveryExecution $deliveryExecution */
333        foreach ($deliveryExecutions as $deliveryExecution) {
334            if (is_string($deliveryExecution)) {
335                $deliveryExecution = $this->getDeliveryExecutionById($deliveryExecution);
336            }
337
338            $success = false;
339            if ($this->isTimerAdjustmentAllowed($deliveryExecution)) {
340                $success = $this->adjustDeliveryExecutionTimer($seconds, $deliveryExecution, $timerAdjustmentService);
341
342                $data = $deliveryMonitoringService->getData($deliveryExecution);
343                $data->updateData([DeliveryMonitoringService::REMAINING_TIME]);
344
345                $deliveryMonitoringService->save($data);
346
347                $eventManager->trigger(
348                    new DeliveryExecutionTimerAdjusted($deliveryExecution, $proctor, $seconds, $reason)
349                );
350            }
351
352            if ($success) {
353                $result['processed'][$deliveryExecution->getIdentifier()] = true;
354            } else {
355                $result['unprocessed'][$deliveryExecution->getIdentifier()] = false;
356            }
357        }
358
359        return $result;
360    }
361
362    /**
363     * @param $seconds
364     * @param DeliveryExecutionInterface $deliveryExecution
365     * @param TimerAdjustmentServiceInterface $timerAdjustmentService
366     * @return bool
367     * @throws InvalidServiceManagerException
368     * @throws QtiTestExtractionFailedException
369     * @throws common_Exception
370     */
371    protected function adjustDeliveryExecutionTimer(
372        $seconds,
373        DeliveryExecutionInterface $deliveryExecution,
374        TimerAdjustmentServiceInterface $timerAdjustmentService
375    ): bool {
376        $testSession = $this->getTestSessionService()->getTestSession($deliveryExecution);
377        if ($seconds > 0) {
378            $success = $timerAdjustmentService->increase(
379                $testSession,
380                $seconds,
381                TimerAdjustmentServiceInterface::TYPE_TIME_ADJUSTMENT
382            );
383        } else {
384            $success = $timerAdjustmentService->decrease(
385                $testSession,
386                abs($seconds),
387                TimerAdjustmentServiceInterface::TYPE_TIME_ADJUSTMENT
388            );
389        }
390        return $success;
391    }
392
393    /**
394     * @param DeliveryExecutionInterface|string $deliveryExecution
395     * @return bool
396     */
397    public function isTimerAdjustmentAllowed($deliveryExecution): bool
398    {
399        if (is_string($deliveryExecution)) {
400            $deliveryExecution = $this->getDeliveryExecutionById($deliveryExecution);
401        }
402
403        if ($deliveryExecution->getState()->getUri() !== DeliveryExecution::STATE_AWAITING) {
404            return false;
405        }
406
407        $testSession = $this->getTestSessionService()->getTestSession($deliveryExecution);
408        if (!$testSession instanceof TestSession) {
409            return false;
410        }
411
412        $timeConstraint = $this->getTestSessionService()->getSmallestMaxTimeConstraint($testSession);
413        if ($timeConstraint === null) {
414            return false;
415        }
416
417        return true;
418    }
419
420    /**
421     * @param string $deliveryExecutionId
422     * @return int
423     */
424    public function getTimerAdjustmentDecreaseLimit(string $deliveryExecutionId): int
425    {
426        $decreaseLimit = self::NO_TIME_ADJUSTMENT_LIMIT;
427        try {
428            $currentTimeConstraint = $this->getSmallestMaxTimeConstraint($deliveryExecutionId);
429            if ($currentTimeConstraint) {
430                $decreaseLimit = $currentTimeConstraint->getMaximumRemainingTime()->getSeconds(true);
431            }
432        } catch (Exception $e) {
433            $this->logError("Cannot calculate minimum time adjustment limit.");
434        }
435
436        return $decreaseLimit;
437    }
438
439    /**
440     * @param string $deliveryExecutionId
441     * @return int
442     */
443    public function getTimerAdjustmentIncreaseLimit(string $deliveryExecutionId): int
444    {
445        return self::NO_TIME_ADJUSTMENT_LIMIT;
446    }
447
448    /**
449     * Returns timerAdjustment for the timer with smaller value for the current item/section/testPart/test chain
450     * @param string $deliveryExecutionId
451     * @return int
452     * @throws QtiTestExtractionFailedException
453     */
454    public function getAdjustedTime(string $deliveryExecutionId): int
455    {
456        $adjustedTime = 0;
457        try {
458            $currentTimeConstraint = $this->getSmallestMaxTimeConstraint($deliveryExecutionId);
459            if ($currentTimeConstraint) {
460                $adjustedTime = $this->getTimerAdjustmentService()->getAdjustmentByType(
461                    $currentTimeConstraint->getSource(),
462                    $currentTimeConstraint->getTimer(),
463                    TimerAdjustmentService::TYPE_TIME_ADJUSTMENT
464                );
465            }
466        } catch (Exception $e) {
467            $this->logError("Cannot calculate adjusted time for provided execution ID: {$deliveryExecutionId}.");
468        }
469
470        return $adjustedTime;
471    }
472
473    /**
474     * @return TestSessionService
475     */
476    private function getTestSessionService()
477    {
478        return $this->getServiceLocator()->get(TestSessionService::SERVICE_ID);
479    }
480
481    /**
482     * @return TimerAdjustmentServiceInterface
483     */
484    private function getTimerAdjustmentService()
485    {
486        return $this->getServiceLocator()->get(TimerAdjustmentServiceInterface::SERVICE_ID);
487    }
488
489    /**
490     * @return DeliveryMonitoringService
491     */
492    private function getDeliveryMonitoringService()
493    {
494        return $this->getServiceLocator()->get(DeliveryMonitoringService::SERVICE_ID);
495    }
496
497    /**
498     * @param string $deliveryExecutionId
499     * @return QtiTimeConstraint|null
500     * @throws InvalidServiceManagerException
501     * @throws QtiTestExtractionFailedException
502     * @throws common_Exception
503     */
504    protected function getSmallestMaxTimeConstraint(string $deliveryExecutionId): ?QtiTimeConstraint
505    {
506        $deliveryExecution = $this->getDeliveryExecutionById($deliveryExecutionId);
507        $testSession = $this->getTestSessionService()->getTestSession($deliveryExecution);
508
509        if (!$testSession) {
510            throw new common_Exception('Test Session not found');
511        }
512
513        return $this->getTestSessionService()->getSmallestMaxTimeConstraint($testSession);
514    }
515}