tao-test/app/taoProctoring/model/execution/DeliveryExecutionList.php

440 lines
15 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) 2019 (original work) Open Assessment Technologies SA;
*/
namespace oat\taoProctoring\model\execution;
use common_Exception;
use common_exception_Error;
use common_exception_NotFound;
use common_ext_ExtensionException;
use common_ext_ExtensionsManager;
use oat\generis\model\OntologyAwareTrait;
use oat\oatbox\service\ConfigurableService;
use oat\oatbox\service\exception\InvalidServiceManagerException;
use oat\oatbox\user\User;
use oat\tao\model\service\ApplicationService;
use oat\taoDelivery\model\execution\DeliveryExecutionInterface;
use oat\taoProctoring\model\deliveryLog\DeliveryLog;
use oat\taoProctoring\model\deliveryLog\event\DeliveryLogEvent;
use oat\taoProctoring\model\monitorCache\DeliveryMonitoringService;
use oat\taoProctoring\model\TestSessionConnectivityStatusService;
use oat\taoQtiTest\models\QtiTestExtractionFailedException;
use oat\taoQtiTest\models\SessionStateService;
use tao_helpers_Uri;
/**
* Class DeliveryHelperService
* @author Bartlomiej Marszal
*/
class DeliveryExecutionList extends ConfigurableService
{
use OntologyAwareTrait;
/**
* Adjusts a list of delivery executions: add information, format the result
* @param DeliveryExecution[] $deliveryExecutions
* @return array
* @throws common_ext_ExtensionException
* @throws common_exception_Error
* @internal param array $options
*/
public function adjustDeliveryExecutions($deliveryExecutions)
{
$executions = [];
$userExtraFields = $this->getUserExtraFields();
/** @var array $cachedData */
foreach ($deliveryExecutions as $cachedData) {
$executions[] = $this->getSingleExecution($cachedData, $userExtraFields);
}
return $executions;
}
/**
* @param $cachedData
* @return array
* @throws common_exception_Error
* @throws common_ext_ExtensionException
*/
private function createTestTaker($cachedData)
{
$testTaker = [];
/* @var $user User */
$testTaker['id'] = $cachedData[DeliveryMonitoringService::TEST_TAKER];
$testTaker['test_taker_last_name'] = isset($cachedData[DeliveryMonitoringService::TEST_TAKER_LAST_NAME])
? $this->sanitizeUserInput($cachedData[DeliveryMonitoringService::TEST_TAKER_LAST_NAME])
: '';
$testTaker['test_taker_first_name'] = isset($cachedData[DeliveryMonitoringService::TEST_TAKER_FIRST_NAME])
? $this->sanitizeUserInput($cachedData[DeliveryMonitoringService::TEST_TAKER_FIRST_NAME])
: '';
return $testTaker;
}
/**
* @param $cachedData
* @param $extraFields
* @return array
* @throws common_Exception
* @throws common_exception_Error
* @throws common_ext_ExtensionException
* @throws QtiTestExtractionFailedException
*/
private function createExecution($cachedData, $extraFields): array
{
$online = $this->isOnline($cachedData);
$isTimerAdjustmentAllowed = $this->getDeliveryExecutionManagerService()->isTimerAdjustmentAllowed(
$cachedData[DeliveryMonitoringService::DELIVERY_EXECUTION_ID]
);
$executionState = $cachedData[DeliveryMonitoringService::STATUS];
$adjustedTime = $isTimerAdjustmentAllowed ? $this->getDeliveryExecutionManagerService()
->getAdjustedTime($cachedData[DeliveryMonitoringService::DELIVERY_EXECUTION_ID]) : 0;
$execution = array(
'id' => $cachedData[DeliveryMonitoringService::DELIVERY_EXECUTION_ID],
'delivery' => array(
'uri' => $cachedData[DeliveryMonitoringService::DELIVERY_ID],
'label' => $this->sanitizeUserInput($cachedData[DeliveryMonitoringService::DELIVERY_NAME]),
),
'start_time' => $cachedData[DeliveryMonitoringService::START_TIME],
'allowExtraTime' => isset($cachedData[DeliveryMonitoringService::ALLOW_EXTRA_TIME])
? (bool)$cachedData[DeliveryMonitoringService::ALLOW_EXTRA_TIME]
: null,
'allowTimerAdjustment' => $isTimerAdjustmentAllowed,
'timer' => [
'lastActivity' => $this->getLastActivity($cachedData, $online),
'countDown' => DeliveryExecution::STATE_ACTIVE === $executionState && $online,
'approximatedRemaining' => $this->getApproximatedRemainingTime($cachedData, $online),
'remaining_time' => $this->getRemainingTime($cachedData),
'extraTime' => (float) ($cachedData[DeliveryMonitoringService::EXTRA_TIME] ?? 0),
'extendedTime' => (isset($cachedData[DeliveryMonitoringService::EXTENDED_TIME]) && $cachedData[DeliveryMonitoringService::EXTENDED_TIME] > 1)
? (float)$cachedData[DeliveryMonitoringService::EXTENDED_TIME]
: '',
'consumedExtraTime' => (float) ($cachedData[DeliveryMonitoringService::CONSUMED_EXTRA_TIME] ?? 0),
'adjustedTime' => $adjustedTime
],
'testTaker' => $this->createTestTaker($cachedData),
'extraFields' => $extraFields,
'state' => $this->createState($cachedData),
);
if ($this->isOnline($cachedData)) {
$execution['online'] = $online;
}
if ($isTimerAdjustmentAllowed) {
$reason = $this->getLastProctorPauseReason($cachedData[DeliveryMonitoringService::DELIVERY_EXECUTION_ID]);
if ($reason) {
$execution['lastPauseReason'] = $reason;
}
$execution['timer']['timeAdjustmentLimits'] = [
'decrease' => $this->getDeliveryExecutionManagerService()->getTimerAdjustmentDecreaseLimit(
$cachedData[DeliveryMonitoringService::DELIVERY_EXECUTION_ID]
),
'increase' => $this->getDeliveryExecutionManagerService()->getTimerAdjustmentIncreaseLimit(
$cachedData[DeliveryMonitoringService::DELIVERY_EXECUTION_ID]
),
];
}
return $execution;
}
private function isPausedByProctor($lastPause): bool
{
$url = tao_helpers_Uri::getPath(
_url('pauseExecutions', 'Monitor', 'taoProctoring')
);
return isset(
$lastPause[0][DeliveryLog::DATA]['reason'],
$lastPause[0][DeliveryLog::DATA]['context']
)
&& mb_strpos($lastPause[0][DeliveryLog::DATA]['context'], $url) !== false;
}
private function getLastProctorPauseReason(string $deliveryExecutionId): ?array
{
$reason = null;
$lastPause = $this->getDeliveryLogService()->search([
DeliveryLog::DELIVERY_EXECUTION_ID => $deliveryExecutionId,
DeliveryLog::EVENT_ID => DeliveryLogEvent::EVENT_ID_TEST_PAUSE,
], [
'order' => 'created_at',
'dir' => 'desc',
'limit' => 1,
]);
if ($this->isPausedByProctor($lastPause)) {
$reason = $lastPause[0][DeliveryLog::DATA]['reason'];
}
return $reason;
}
/**
* @return DeliveryLog|object
*/
private function getDeliveryLogService(): DeliveryLog
{
return $this->getServiceLocator()->get(DeliveryLog::SERVICE_ID);
}
/**
* @param $cachedData
* @param $userExtraFields
* @return array
* @throws InvalidServiceManagerException
* @throws QtiTestExtractionFailedException
* @throws common_Exception
* @throws common_exception_Error
* @throws common_exception_NotFound
* @throws common_ext_ExtensionException
*/
private function getSingleExecution($cachedData, $userExtraFields)
{
$extraFields = [];
foreach ($userExtraFields as $field) {
$extraFields[$field['id']] = $this->getFieldId($cachedData, $field);
}
return $this->createExecution($cachedData, $extraFields);
}
/**
* @param $cachedData
* @param $field
* @return string
* @throws common_exception_Error
* @throws common_ext_ExtensionException
*/
private function getFieldId($cachedData, $field)
{
$value = isset($cachedData[$field['id']])
? $this->sanitizeUserInput($cachedData[$field['id']])
: '';
if (\common_Utils::isUri($value)) {
$value = $this->getResource($value)->getLabel();
}
return $value;
}
/**
* @param array $cachedData
* @param $online
* @return float
*/
private function getApproximatedRemainingTime(array $cachedData, $online)
{
$now = microtime(true);
$remaining = $this->getRemainingTime($cachedData);
$elapsedApprox = 0;
$executionState = $cachedData[DeliveryMonitoringService::STATUS];
if (
$executionState === DeliveryExecution::STATE_ACTIVE
&& isset($cachedData[DeliveryMonitoringService::LAST_TEST_TAKER_ACTIVITY])
) {
$lastActivity = (float)$cachedData[DeliveryMonitoringService::LAST_TEST_TAKER_ACTIVITY];
$elapsedApprox = $now - $lastActivity;
$duration = (float)($cachedData[DeliveryMonitoringService::ITEM_DURATION] ?? 0);
$duration -= (float)($cachedData[DeliveryMonitoringService::STORED_ITEM_DURATION] ?? 0);
$elapsedApprox += $duration;
}
if (is_bool($online) && $online === false) {
$elapsedApprox = 0;
}
return round((float)$remaining - $elapsedApprox);
}
/**
* @param array $cachedData
* @return int
*/
private function getRemainingTime(array $cachedData)
{
return (int) ($cachedData[DeliveryMonitoringService::REMAINING_TIME] ?? 0);
}
/**
* Get array of user specific extra fields to be displayed in the monitoring data table
*
* @return array
* @throws common_ext_ExtensionException
*/
private function getUserExtraFields()
{
$proctoringExtension = $this->getExtensionManagerService()->getExtensionById('taoProctoring');
$userExtraFields = $proctoringExtension->getConfig('monitoringUserExtraFields');
if (empty($userExtraFields) || !is_array($userExtraFields)) {
return [];
}
$userExtraFieldsSettings = $proctoringExtension->getConfig('monitoringUserExtraFieldsSettings');
$extraFields = [];
foreach ($userExtraFields as $name => $uri) {
$extraFields[] = $this->mergeExtraFieldsSettings($uri, $name, $userExtraFieldsSettings);
}
return $extraFields;
}
private function mergeExtraFieldsSettings($uri, $name, $userExtraFieldsSettings)
{
$property = $this->getProperty($uri);
$settings = array_key_exists($name, $userExtraFieldsSettings)
? $userExtraFieldsSettings[$name]
: [];
return array_merge([
'id' => $name,
'property' => $property,
'label' => __($property->getLabel()),
], $settings);
}
/**
* @param array $cachedData
* @return mixed|string
*/
private function getProgressString(array $cachedData)
{
$progressStr = $cachedData[DeliveryMonitoringService::CURRENT_ASSESSMENT_ITEM];
$progress = json_decode($progressStr, true);
if ($progress === null) {
return $progressStr;
}
if (in_array($cachedData[DeliveryMonitoringService::STATUS], [DeliveryExecutionInterface::STATE_TERMINATED, DeliveryExecutionInterface::STATE_FINISHED], true)) {
return $progress['title'];
}
$format = $this->getSessionStateService()->hasOption(SessionStateService::OPTION_STATE_FORMAT)
? $this->getSessionStateService()->getOption(SessionStateService::OPTION_STATE_FORMAT)
: __('%s - item %p/%c');
$map = array(
'%s' => $progress['title'] ?? '',
'%p' => $progress['itemPosition'] ?? '',
'%c' => $progress['itemCount'] ?? ''
);
return strtr($format, $map);
}
/**
* @param array $cachedData
* @return bool|null
*/
private function isOnline(array $cachedData)
{
if ($this->getTestSessionConnectivityStatusService()->hasOnlineMode()) {
return $this->getTestSessionConnectivityStatusService()->isOnline($cachedData[DeliveryMonitoringService::DELIVERY_EXECUTION_ID]);
}
return null;
}
/**
* @param array $cachedData
* @param bool|null $online
* @return float|null
*/
private function getLastActivity(array $cachedData, ?bool $online)
{
if ($online && isset($cachedData[DeliveryMonitoringService::LAST_TEST_TAKER_ACTIVITY])) {
return $cachedData[DeliveryMonitoringService::LAST_TEST_TAKER_ACTIVITY];
}
return null;
}
/**
* @param $input
* @return string
* @throws common_exception_Error
* @throws common_ext_ExtensionException
*/
private function sanitizeUserInput($input)
{
return htmlentities($input, ENT_COMPAT, $this->getApplicationService()->getDefaultEncoding());
}
/**
* @return ApplicationService
*/
private function getApplicationService()
{
return $this->getServiceLocator()->get(ApplicationService::SERVICE_ID);
}
/**
* @return TestSessionConnectivityStatusService
*/
private function getTestSessionConnectivityStatusService()
{
return $this->getServiceLocator()->get(TestSessionConnectivityStatusService::SERVICE_ID);
}
/**
* @return SessionStateService
*/
private function getSessionStateService()
{
return $this->getServiceLocator()->get(SessionStateService::SERVICE_ID);
}
/**
* @return common_ext_ExtensionsManager
*/
private function getExtensionManagerService()
{
return $this->getServiceLocator()->get(common_ext_ExtensionsManager::SERVICE_ID);
}
/**
* @return DeliveryExecutionManagerService
*/
private function getDeliveryExecutionManagerService()
{
return $this->getServiceLocator()->get(DeliveryExecutionManagerService::SERVICE_ID);
}
/**
* @param $cachedData
* @return array
*/
private function createState($cachedData): array
{
$progressStr = $this->getProgressString($cachedData);
$state = [
'status' => $cachedData[DeliveryMonitoringService::STATUS],
'progress' => __($progressStr)
];
return $state;
}
}