<?php /** * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; under version 2 * of the License (non-upgradable). * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * Copyright (c) 2017 (original work) Open Assessment Technologies SA; * */ declare(strict_types=1); namespace oat\taoProctoring\model\execution; use common_Exception; use common_exception_Error; use common_exception_MissingParameter; use common_exception_NotFound; use common_ext_ExtensionException; use common_session_Session; use Exception; use oat\oatbox\event\EventManager; use oat\oatbox\service\ConfigurableService; use oat\oatbox\service\exception\InvalidServiceManagerException; use oat\oatbox\session\SessionService; use oat\taoDelivery\model\execution\DeliveryExecution as BaseDeliveryExecution; use oat\taoDelivery\model\execution\DeliveryExecutionInterface; use oat\taoDelivery\model\execution\ServiceProxy; use oat\taoProctoring\model\event\DeliveryExecutionTimerAdjusted; use oat\taoProctoring\model\implementation\TestSessionService; use oat\taoProctoring\model\monitorCache\DeliveryMonitoringData; use oat\taoProctoring\model\monitorCache\DeliveryMonitoringService; use oat\taoQtiTest\models\QtiTestExtractionFailedException; use oat\taoQtiTest\models\runner\session\TestSession; use oat\taoQtiTest\models\runner\StorageManager; use oat\taoQtiTest\models\runner\time\QtiTimeConstraint; use oat\taoQtiTest\models\runner\time\QtiTimer; use oat\taoQtiTest\models\runner\time\QtiTimerFactory; use oat\taoQtiTest\models\runner\time\TimerAdjustmentService; use oat\taoTests\models\runner\time\TimePoint; use oat\taoQtiTest\models\runner\time\TimerAdjustmentServiceInterface; use oat\taoTests\models\runner\time\TimerStrategyInterface; use qtism\common\datatypes\QtiDuration; use qtism\data\AssessmentTest; use qtism\data\QtiIdentifiable; use qtism\runtime\tests\AssessmentTestSessionState; /** * Class DeliveryExecutionManagerService * @package oat\taoProctoring\model\execution */ class DeliveryExecutionManagerService extends ConfigurableService { public const SERVICE_ID = 'taoProctoring/DeliveryExecutionManagerService'; protected const NO_TIME_ADJUSTMENT_LIMIT = -1; private $deliveryExecutions = []; /** * @param $deliveryExecutionId * @return BaseDeliveryExecution */ public function getDeliveryExecutionById($deliveryExecutionId): BaseDeliveryExecution { if (!isset($this->deliveryExecutions[$deliveryExecutionId])) { $deliveryExecution = $this->getServiceProxy() ->getDeliveryExecution($deliveryExecutionId); $this->deliveryExecutions[$deliveryExecutionId] = $deliveryExecution; } return $this->deliveryExecutions[$deliveryExecutionId]; } /** * @return ServiceProxy|object */ private function getServiceProxy() { return $this->getServiceLocator()->get(ServiceProxy::SERVICE_ID); } /** * Gets the delivery time counter * * @param DeliveryExecutionInterface $deliveryExecution * @return QtiTimer * @throws InvalidServiceManagerException * @throws QtiTestExtractionFailedException * @throws common_Exception * @throws common_exception_Error * @throws common_exception_NotFound * @throws common_ext_ExtensionException */ public function getDeliveryTimer($deliveryExecution) { if (is_string($deliveryExecution)) { $deliveryExecution = $this->getDeliveryExecutionById($deliveryExecution); } $testSession = $this->getTestSessionService()->getTestSession($deliveryExecution, true); if ($testSession instanceof TestSession) { $timer = $testSession->getTimer(); } else { $qtiTimerFactory = $this->getServiceLocator()->get(QtiTimerFactory::SERVICE_ID); $timer = $qtiTimerFactory->getTimer($deliveryExecution->getIdentifier(), $deliveryExecution->getUserIdentifier()); } return $timer; } /** * @param TestSession $testSession * @param $part * @return int|null */ protected function getPartTimeLimits($testSession, $part) { $timeLimits = $part->getTimeLimits(); if ($timeLimits && ($maxTime = $timeLimits->getMaxTime()) !== null) { if ($testSession !== null && ($timer = $testSession->getTimer()) !== null) { $maxTime = $this->getTimerAdjustmentService()->getAdjustedMaxTime($part, $timer); } return $maxTime->getSeconds(true); } return null; } /** * Gets the actual time limits for a test session * @param TestSession $testSession * @return int|null */ public function getTimeLimits($testSession) { $seconds = null; if ($item = $testSession->getCurrentAssessmentItemRef()) { $seconds = $this->getPartTimeLimits($testSession, $item); } if (!$seconds && $section = $testSession->getCurrentAssessmentSection()) { $seconds = $this->getPartTimeLimits($testSession, $section); } if (!$seconds && $testPart = $testSession->getCurrentTestPart()) { $seconds = $this->getPartTimeLimits($testSession, $testPart); } if (!$seconds && $assessmentTest = $testSession->getAssessmentTest()) { $seconds = $this->getPartTimeLimits($testSession, $assessmentTest); } return $seconds; } /** * Sets the extra time to a list of delivery executions * @param $deliveryExecutions * @param int $extraTime * @return array * @throws common_exception_Error * @throws common_exception_MissingParameter * @throws common_exception_NotFound * @throws \oat\taoTests\models\runner\time\InvalidStorageException */ public function setExtraTime($deliveryExecutions, $extraTime = 0) { $deliveryMonitoringService = $this->getDeliveryMonitoringService(); $testSessionService = $this->getTestSessionService(); $result = ['processed' => [], 'unprocessed' => []]; /** @var DeliveryExecution $deliveryExecution */ foreach ($deliveryExecutions as $deliveryExecution) { if (is_string($deliveryExecution)) { $deliveryExecution = $this->getDeliveryExecutionById($deliveryExecution); } /** @var DeliveryMonitoringData $data */ $data = $deliveryMonitoringService->getData($deliveryExecution); $maxTime = 0; $timerTarget = TimePoint::TARGET_SERVER; // reopen the execution if already closed if ($deliveryExecution->getState()->getUri() == DeliveryExecution::STATE_FINISHED) { $deliveryExecution->setState(DeliveryExecution::STATE_ACTIVE); /* @var TestSession $testSession */ $testSession = $testSessionService->getTestSession($deliveryExecution); if ($testSession) { $timerTarget = $testSession->getTimerTarget(); $testSession->getRoute()->setPosition(0); $testSession->setState(AssessmentTestSessionState::INTERACTING); // The duration store contains durations (time spent) on test, testPart(s) and assessmentSection(s). $durationStore = $testSession->getDurationStore(); $offsetDuration = new QtiDuration("PT${extraTime}S"); $testDefinition = $testSession->getAssessmentTest(); $currentDuration = $durationStore[$testDefinition->getIdentifier()]; $offsetSeconds = $offsetDuration->getSeconds(true); $currentSeconds = $currentDuration->getSeconds(true); $newSeconds = $currentSeconds - $offsetSeconds; if ($newSeconds < 0) { $newSeconds = 0; } // Replace test duration with new duration. $durationStore[$testDefinition->getIdentifier()] = new QtiDuration("PT${newSeconds}S"); $testSessionService->persist($testSession); $maxTime = $this->getPartTimeLimits($testSession, $testDefinition); } } /** @var QtiTimer $timer */ $timer = $this->getDeliveryTimer($deliveryExecution); $timer ->setExtraTime($extraTime) ->save(); $data->update(DeliveryMonitoringService::EXTRA_TIME, $timer->getExtraTime()); $data->update(DeliveryMonitoringService::CONSUMED_EXTRA_TIME, $timer->getConsumedExtraTime(null, $maxTime, $timerTarget)); if ($deliveryMonitoringService->save($data)) { $result['processed'][$deliveryExecution->getIdentifier()] = true; } else { $result['unprocessed'][$deliveryExecution->getIdentifier()] = false; } } $this->getServiceLocator()->get(StorageManager::SERVICE_ID)->persist(); return $result; } /** * @param DeliveryExecutionInterface $deliveryExecution * @param $extendedTime * @throws common_exception_Error * @throws common_exception_MissingParameter * @throws common_exception_NotFound * @throws \oat\taoTests\models\runner\time\InvalidStorageException */ public function updateDeliveryExtendedTime(DeliveryExecutionInterface $deliveryExecution, $extendedTime) { $timer = $this->getDeliveryTimer($deliveryExecution); if ($timer->getExtendedTime()) { return; } $inputParameters = $this->getTestSessionService()->getRuntimeInputParameters($deliveryExecution); /** @var AssessmentTest $testDefinition */ $testDefinition = \taoQtiTest_helpers_Utils::getTestDefinition($inputParameters['QtiTestCompilation']); $components = $testDefinition->getComponentsByClassName(['testPart', 'assessmentSection', 'assessmentItemRef']); $components->attach($testDefinition); /** @var QtiIdentifiable $component */ foreach ($components as $component) { $timeLimits = $component->getTimeLimits(); if ($timeLimits && $timeLimits->hasMaxTime()) { $currentLimitSeconds = $timeLimits->getMaxTime()->getSeconds(true); $increaseSeconds = (int) $this->getServiceLocator() ->get(TimerStrategyInterface::SERVICE_ID) ->getExtraTime($currentLimitSeconds, $extendedTime); if ($increaseSeconds > 0) { $timer->getAdjustmentMap()->increase( $component->getIdentifier(), TimerAdjustmentServiceInterface::TYPE_EXTENDED_TIME, $increaseSeconds ); } } } $timer->setExtendedTime($extendedTime); $timer->save(); $this->getServiceLocator()->get(StorageManager::SERVICE_ID)->persist(); $deliveryMonitoringService = $this->getDeliveryMonitoringService(); $data = $deliveryMonitoringService->getData($deliveryExecution); $data->update(DeliveryMonitoringService::EXTENDED_TIME, $timer->getExtendedTime()); $deliveryMonitoringService->save($data); } /** * Registers timer adjustments to a list of delivery executions * @param array $deliveryExecutions * @param int $seconds * @param array $reason * @return array * @throws InvalidServiceManagerException * @throws QtiTestExtractionFailedException * @throws common_Exception * @throws common_exception_Error * @throws common_exception_NotFound * @throws common_ext_ExtensionException */ public function adjustTimers(array $deliveryExecutions, int $seconds, array $reason = []): array { $result = ['processed' => [], 'unprocessed' => []]; $timerAdjustmentService = $this->getTimerAdjustmentService(); $deliveryMonitoringService = $this->getDeliveryMonitoringService(); /** @var common_session_Session $session */ $session = $this->getServiceLocator()->get(SessionService::SERVICE_ID)->getCurrentSession(); $proctor = $session->getUser(); $eventManager = $this->getServiceLocator()->get(EventManager::SERVICE_ID); /** @var DeliveryExecution $deliveryExecution */ foreach ($deliveryExecutions as $deliveryExecution) { if (is_string($deliveryExecution)) { $deliveryExecution = $this->getDeliveryExecutionById($deliveryExecution); } $success = false; if ($this->isTimerAdjustmentAllowed($deliveryExecution)) { $success = $this->adjustDeliveryExecutionTimer($seconds, $deliveryExecution, $timerAdjustmentService); $data = $deliveryMonitoringService->getData($deliveryExecution); $data->updateData([DeliveryMonitoringService::REMAINING_TIME]); $deliveryMonitoringService->save($data); $eventManager->trigger(new DeliveryExecutionTimerAdjusted($deliveryExecution, $proctor, $seconds, $reason)); } if ($success) { $result['processed'][$deliveryExecution->getIdentifier()] = true; } else { $result['unprocessed'][$deliveryExecution->getIdentifier()] = false; } } return $result; } /** * @param $seconds * @param DeliveryExecutionInterface $deliveryExecution * @param TimerAdjustmentServiceInterface $timerAdjustmentService * @return bool * @throws InvalidServiceManagerException * @throws QtiTestExtractionFailedException * @throws common_Exception */ protected function adjustDeliveryExecutionTimer( $seconds, DeliveryExecutionInterface $deliveryExecution, TimerAdjustmentServiceInterface $timerAdjustmentService ): bool { $testSession = $this->getTestSessionService()->getTestSession($deliveryExecution); if ($seconds > 0) { $success = $timerAdjustmentService->increase( $testSession, $seconds, TimerAdjustmentServiceInterface::TYPE_TIME_ADJUSTMENT ); } else { $success = $timerAdjustmentService->decrease( $testSession, abs($seconds), TimerAdjustmentServiceInterface::TYPE_TIME_ADJUSTMENT ); } return $success; } /** * @param DeliveryExecutionInterface|string $deliveryExecution * @return bool */ public function isTimerAdjustmentAllowed($deliveryExecution): bool { if (is_string($deliveryExecution)) { $deliveryExecution = $this->getDeliveryExecutionById($deliveryExecution); } if ($deliveryExecution->getState()->getUri() !== DeliveryExecution::STATE_AWAITING) { return false; } $testSession = $this->getTestSessionService()->getTestSession($deliveryExecution); if (!$testSession instanceof TestSession) { return false; } $timeConstraint = $this->getTestSessionService()->getSmallestMaxTimeConstraint($testSession); if ($timeConstraint === null) { return false; } return true; } /** * @param string $deliveryExecutionId * @return int */ public function getTimerAdjustmentDecreaseLimit(string $deliveryExecutionId): int { $decreaseLimit = self::NO_TIME_ADJUSTMENT_LIMIT; try { $currentTimeConstraint = $this->getSmallestMaxTimeConstraint($deliveryExecutionId); if ($currentTimeConstraint) { $decreaseLimit = $currentTimeConstraint->getMaximumRemainingTime()->getSeconds(true); } } catch (Exception $e) { $this->logError("Cannot calculate minimum time adjustment limit."); } return $decreaseLimit; } /** * @param string $deliveryExecutionId * @return int */ public function getTimerAdjustmentIncreaseLimit(string $deliveryExecutionId): int { return self::NO_TIME_ADJUSTMENT_LIMIT; } /** * Returns timerAdjustment for the timer with smaller value for the current item/section/testPart/test chain * @param string $deliveryExecutionId * @return int * @throws QtiTestExtractionFailedException */ public function getAdjustedTime(string $deliveryExecutionId): int { $adjustedTime = 0; try { $currentTimeConstraint = $this->getSmallestMaxTimeConstraint($deliveryExecutionId); if ($currentTimeConstraint) { $adjustedTime = $this->getTimerAdjustmentService()->getAdjustmentByType( $currentTimeConstraint->getSource(), $currentTimeConstraint->getTimer(), TimerAdjustmentService::TYPE_TIME_ADJUSTMENT ); } } catch (Exception $e) { $this->logError("Cannot calculate adjusted time for provided execution ID: {$deliveryExecutionId}."); } return $adjustedTime; } /** * @return TestSessionService */ private function getTestSessionService() { return $this->getServiceLocator()->get(TestSessionService::SERVICE_ID); } /** * @return TimerAdjustmentServiceInterface */ private function getTimerAdjustmentService() { return $this->getServiceLocator()->get(TimerAdjustmentServiceInterface::SERVICE_ID); } /** * @return DeliveryMonitoringService */ private function getDeliveryMonitoringService() { return $this->getServiceLocator()->get(DeliveryMonitoringService::SERVICE_ID); } /** * @param string $deliveryExecutionId * @return QtiTimeConstraint|null * @throws InvalidServiceManagerException * @throws QtiTestExtractionFailedException * @throws common_Exception */ protected function getSmallestMaxTimeConstraint(string $deliveryExecutionId): ?QtiTimeConstraint { $deliveryExecution = $this->getDeliveryExecutionById($deliveryExecutionId); $testSession = $this->getTestSessionService()->getTestSession($deliveryExecution); if (!$testSession) { throw new common_Exception('Test Session not found'); } return $this->getTestSessionService()->getSmallestMaxTimeConstraint($testSession); } }