507 lines
19 KiB
PHP
507 lines
19 KiB
PHP
<?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);
|
|
}
|
|
}
|