tao-test/app/taoQtiTest/models/classes/runner/synchronisation/synchronisationService/ResponseGenerator.php

170 lines
6.0 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) 2020 (original work) Open Assessment Technologies SA;
*
* @author Oleksandr Zagovorychev <zagovorichev@gmail.com>
*/
declare(strict_types=1);
namespace oat\taoQtiTest\models\runner\synchronisation\synchronisationService;
use common_Exception;
use common_exception_InconsistentData;
use common_Logger;
use oat\oatbox\service\ConfigurableService;
use oat\taoQtiTest\models\runner\QtiRunnerServiceContext;
use oat\taoQtiTest\models\runner\synchronisation\TestRunnerAction;
use ResolverException;
class ResponseGenerator extends ConfigurableService
{
/**
* Typical amount of time added on TimePoints to avoid timestamp collisions.
* This value will be used to adjust intervals between moves in the synced time line.
*/
private const TIMEPOINT_INTERVAL = .001;
/**
* @param array $data
* @param array $availableActions
* @return array
*/
public function prepareActions(array $data, array $availableActions): array
{
/** @var TestRunnerActionResolver $resolver */
$resolver = $this->getServiceLocator()->get(TestRunnerActionResolver::class);
$actions = [];
foreach ($data as $entry) {
try {
$actions[] = $resolver->resolve($entry, $availableActions);
} catch (common_exception_InconsistentData | ResolverException $e) {
$responseAction = $entry;
$responseAction['error'] = $e->getMessage();
$responseAction['success'] = false;
$actions[] = $this->transform($responseAction);
}
}
// ensure the actions are in chronological order
usort($actions, static function ($a, $b) {
$timeA = $a instanceof TestRunnerAction ? $a->getTimestamp() : 0;
$timeB = $b instanceof TestRunnerAction ? $b->getTimestamp() : 0;
return $timeA - $timeB;
});
return $actions;
}
private function transformField(string $fromFieldName, string $toFieldName, array $data): array
{
if (array_key_exists($fromFieldName, $data)) {
$data[$toFieldName] = $data[$fromFieldName];
unset($data[$fromFieldName]);
}
return $data;
}
private function transform($data)
{
if (is_array($data)) {
$data = $this->transformField('parameters', 'requestParameters', $data);
$data = $this->transformField('action', 'name', $data);
}
return $data;
}
/**
* @param array $actions
* @return float
*/
private function computeDuration(array $actions): float
{
$duration = 0;
foreach ($actions as $action) {
if ($action instanceof TestRunnerAction && $action->hasRequestParameter('itemDuration')) {
$duration += $action->getRequestParameter('itemDuration') + self::TIMEPOINT_INTERVAL;
}
}
return $duration;
}
/**
* determine the start timestamp of the actions:
* - check if the total duration of actions to sync is comprised within
* the elapsed time since the last TimePoint.
* - otherwise compute the start timestamp from now minus the duration
* (caution! this could introduce inconsistency in the TimeLine as the ranges could be interlaced)
*
* @param array $actions
* @param QtiRunnerServiceContext $serviceContext
* @param float $timeNow
* @return float
*/
public function getLastActionTimestamp(array $actions, QtiRunnerServiceContext $serviceContext, float $timeNow): float
{
$lastRegisteredTimestamp = (float) $serviceContext->getTestSession()->getTimer()->getLastRegisteredTimestamp();
$actionsDuration = $this->computeDuration($actions);
$elapsed = $timeNow - $lastRegisteredTimestamp;
if ($actionsDuration > $elapsed) {
common_Logger::t('Ignoring the last timestamp to take into account the actual duration to sync. Could introduce TimeLine inconsistency!');
$lastRegisteredTimestamp = $timeNow - $actionsDuration;
}
return $lastRegisteredTimestamp;
}
/**
* @param TestRunnerAction $action
* @param float $now
* @param float $last
* @param QtiRunnerServiceContext $serviceContext
* @return array
*/
public function getActionResponse(
TestRunnerAction $action,
float $now,
float &$last,
QtiRunnerServiceContext $serviceContext
): array
{
try {
$serviceContext->setSyncingMode($action->getRequestParameter('offline'));
if ($action->hasRequestParameter('itemDuration') && $serviceContext->isSyncingMode()) {
$last += $action->getRequestParameter('itemDuration') + self::TIMEPOINT_INTERVAL;
$action->setTime($last);
} else {
$action->setTime($now);
}
$action->setServiceContext($serviceContext);
$actionResponse = $action->process();
} catch (common_Exception $e) {
$actionResponse = ['error' => $e->getMessage()];
$actionResponse['success'] = false;
}
$actionResponse['name'] = $action->getName();
$actionResponse['timestamp'] = $action->getTimeStamp();
$actionResponse['requestParameters'] = $action->getRequestParameters();
return $actionResponse;
}
}