2136 lines
78 KiB
PHP
2136 lines
78 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) 2016-2017 (original work) Open Assessment Technologies SA ;
|
|
*/
|
|
|
|
/**
|
|
* @author Jean-Sébastien Conan <jean-sebastien.conan@vesperiagroup.com>
|
|
*/
|
|
|
|
namespace oat\taoQtiTest\models\runner;
|
|
|
|
use common_exception_InvalidArgumentType as InvalidArgumentTypeException;
|
|
use common_persistence_AdvKeyValuePersistence;
|
|
use common_persistence_KeyValuePersistence;
|
|
use League\Flysystem\FileNotFoundException;
|
|
use oat\libCat\result\ItemResult;
|
|
use oat\libCat\result\ResultVariable;
|
|
use oat\oatbox\event\EventManager;
|
|
use oat\oatbox\service\ConfigurableService;
|
|
use oat\tao\model\theme\ThemeService;
|
|
use oat\taoDelivery\model\execution\Delete\DeliveryExecutionDeleteRequest;
|
|
use oat\taoDelivery\model\execution\DeliveryExecution;
|
|
use oat\taoDelivery\model\execution\DeliveryServerService;
|
|
use oat\taoDelivery\model\execution\ServiceProxy;
|
|
use oat\taoDelivery\model\RuntimeService;
|
|
use oat\taoItems\model\render\ItemAssetsReplacement;
|
|
use oat\taoQtiItem\model\portableElement\exception\PortableElementNotFoundException;
|
|
use oat\taoQtiItem\model\portableElement\exception\PortableModelMissing;
|
|
use oat\taoQtiItem\model\portableElement\PortableElementService;
|
|
use oat\taoQtiItem\model\QtiJsonItemCompiler;
|
|
use oat\taoQtiTest\models\cat\CatService;
|
|
use oat\taoQtiTest\models\cat\GetDeliveryExecutionsItems;
|
|
use oat\taoQtiTest\models\event\AfterAssessmentTestSessionClosedEvent;
|
|
use oat\taoQtiTest\models\event\QtiContinueInteractionEvent;
|
|
use oat\taoQtiTest\models\event\TestExitEvent;
|
|
use oat\taoQtiTest\models\event\TestInitEvent;
|
|
use oat\taoQtiTest\models\event\TestTimeoutEvent;
|
|
use oat\taoQtiTest\models\ExtendedStateService;
|
|
use oat\taoQtiTest\models\files\QtiFlysystemFileManager;
|
|
use oat\taoQtiTest\models\runner\config\QtiRunnerConfig;
|
|
use oat\taoQtiTest\models\runner\config\RunnerConfig;
|
|
use oat\taoQtiTest\models\runner\map\QtiRunnerMap;
|
|
use oat\taoQtiTest\models\runner\navigation\QtiRunnerNavigation;
|
|
use oat\taoQtiTest\models\runner\rubric\QtiRunnerRubric;
|
|
use oat\taoQtiTest\models\runner\session\TestSession;
|
|
use oat\taoQtiTest\models\runner\toolsStates\ToolsStateStorage;
|
|
use oat\taoQtiTest\models\TestSessionService;
|
|
use qtism\common\datatypes\QtiString as QtismString;
|
|
use qtism\common\enums\BaseType;
|
|
use qtism\common\enums\Cardinality;
|
|
use qtism\data\AssessmentItemRef;
|
|
use qtism\data\NavigationMode;
|
|
use qtism\data\SubmissionMode;
|
|
use qtism\runtime\common\ResponseVariable;
|
|
use qtism\runtime\common\State;
|
|
use qtism\runtime\common\Utils;
|
|
use qtism\runtime\tests\AssessmentItemSession;
|
|
use qtism\runtime\tests\AssessmentItemSessionState;
|
|
use qtism\runtime\tests\AssessmentTestSession;
|
|
use qtism\runtime\tests\AssessmentTestSessionException;
|
|
use qtism\runtime\tests\AssessmentTestSessionState;
|
|
use qtism\runtime\tests\RouteItem;
|
|
use qtism\runtime\tests\SessionManager;
|
|
use tao_models_classes_service_StateStorage;
|
|
use taoQtiTest_helpers_TestRunnerUtils as TestRunnerUtils;
|
|
|
|
/**
|
|
* Class QtiRunnerService
|
|
*
|
|
* QTI implementation service for the test runner
|
|
*
|
|
* @package oat\taoQtiTest\models
|
|
*/
|
|
class QtiRunnerService extends ConfigurableService implements RunnerService
|
|
{
|
|
public const SERVICE_ID = 'taoQtiTest/QtiRunnerService';
|
|
|
|
/**
|
|
* @deprecated use SERVICE_ID
|
|
*/
|
|
public const CONFIG_ID = 'taoQtiTest/QtiRunnerService';
|
|
|
|
public const TOOL_ITEM_THEME_SWITCHER = 'itemThemeSwitcher';
|
|
public const TOOL_ITEM_THEME_SWITCHER_KEY = 'taoQtiTest/runner/plugins/tools/itemThemeSwitcher/itemThemeSwitcher';
|
|
|
|
private const TIMEOUT_EXCEPTION_CODES = [
|
|
AssessmentTestSessionException::ASSESSMENT_TEST_DURATION_OVERFLOW,
|
|
AssessmentTestSessionException::TEST_PART_DURATION_OVERFLOW,
|
|
AssessmentTestSessionException::ASSESSMENT_SECTION_DURATION_OVERFLOW,
|
|
AssessmentTestSessionException::ASSESSMENT_ITEM_DURATION_OVERFLOW,
|
|
];
|
|
|
|
/**
|
|
* The test runner config
|
|
* @var RunnerConfig
|
|
*/
|
|
protected $testConfig;
|
|
|
|
/**
|
|
* Use to store retrieved item data, inside the same request
|
|
* @var array
|
|
*/
|
|
private $dataCache = [];
|
|
|
|
/**
|
|
* Get the data folder from a given item definition
|
|
* @param string $itemRef - formatted as itemURI|publicFolderURI|privateFolderURI
|
|
* @return array the path
|
|
* @throws \common_Exception
|
|
*/
|
|
private function loadItemData($itemRef, $path)
|
|
{
|
|
$cacheKey = $itemRef . $path;
|
|
if (! empty($cacheKey) && isset($this->dataCache[$itemRef . $path])) {
|
|
return $this->dataCache[$itemRef . $path];
|
|
}
|
|
|
|
$directoryIds = explode('|', $itemRef);
|
|
if (count($directoryIds) < 3) {
|
|
if (is_scalar($itemRef)) {
|
|
$itemRefInfo = gettype($itemRef) . ': ' . strval($itemRef);
|
|
} elseif (is_object($itemRef)) {
|
|
$itemRefInfo = gettype($itemRef) . ': ' . get_class($itemRef);
|
|
} else {
|
|
$itemRefInfo = gettype($itemRef);
|
|
}
|
|
|
|
throw new \common_exception_InconsistentData("The itemRef (value = '${itemRefInfo}') is not formatted correctly.");
|
|
}
|
|
|
|
$itemUri = $directoryIds[0];
|
|
$userDataLang = \common_session_SessionManager::getSession()->getDataLanguage();
|
|
$directory = \tao_models_classes_service_FileStorage::singleton()->getDirectoryById($directoryIds[2]);
|
|
|
|
if ($directory->has($userDataLang)) {
|
|
$lang = $userDataLang;
|
|
} elseif ($directory->has(DEFAULT_LANG)) {
|
|
\common_Logger::d(
|
|
$userDataLang . ' is not part of compilation directory for item : ' . $itemUri . ' use ' . DEFAULT_LANG
|
|
);
|
|
$lang = DEFAULT_LANG;
|
|
} else {
|
|
throw new \common_Exception(
|
|
'item : ' . $itemUri . 'is neither compiled in ' . $userDataLang . ' nor in ' . DEFAULT_LANG
|
|
);
|
|
}
|
|
try {
|
|
$content = $directory->read($lang . DIRECTORY_SEPARATOR . $path);
|
|
/** @var ItemAssetsReplacement $assetService */
|
|
$assetService = $this->getServiceManager()->get(ItemAssetsReplacement::SERVICE_ID);
|
|
$jsonContent = json_decode($content, true);
|
|
$jsonAssets = [];
|
|
if (isset($jsonContent['assets'])) {
|
|
foreach ($jsonContent['assets'] as $type => $assets) {
|
|
foreach ($assets as $key => $asset) {
|
|
$jsonAssets[$type][$key] = $assetService->postProcessAssets($asset);
|
|
}
|
|
}
|
|
$jsonContent["assets"] = $jsonAssets;
|
|
}
|
|
|
|
$this->dataCache[$cacheKey] = $jsonContent;
|
|
return $this->dataCache[$cacheKey];
|
|
} catch (FileNotFoundException $e) {
|
|
throw new \tao_models_classes_FileNotFoundException(
|
|
$path . ' for item reference ' . $itemRef
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the test session for a particular delivery execution
|
|
*
|
|
* This method is called before each action (moveNext, moveBack, pause, ...) call.
|
|
*
|
|
* @param string $testDefinitionUri The URI of the test
|
|
* @param string $testCompilationUri The URI of the compiled delivery
|
|
* @param string $testExecutionUri The URI of the delivery execution
|
|
* @param string $userUri User identifier. If null current user will be used
|
|
* @return QtiRunnerServiceContext
|
|
* @throws \common_Exception
|
|
*/
|
|
public function getServiceContext($testDefinitionUri, $testCompilationUri, $testExecutionUri, $userUri = null)
|
|
{
|
|
// create a service context based on the provided URI
|
|
// initialize the test session and related objects
|
|
$serviceContext = new QtiRunnerServiceContext($testDefinitionUri, $testCompilationUri, $testExecutionUri);
|
|
$this->propagate($serviceContext);
|
|
$serviceContext->setTestConfig($this->getTestConfig());
|
|
$serviceContext->setUserUri($userUri);
|
|
|
|
$sessionService = $this->getServiceManager()->get(TestSessionService::SERVICE_ID);
|
|
$sessionService->registerTestSession(
|
|
$serviceContext->getTestSession(),
|
|
$serviceContext->getStorage(),
|
|
$serviceContext->getCompilationDirectory()
|
|
);
|
|
|
|
return $serviceContext;
|
|
}
|
|
|
|
/**
|
|
* Checks the created context, then initializes it.
|
|
* @param RunnerServiceContext $context
|
|
* @return RunnerServiceContext
|
|
* @throws \common_Exception
|
|
*/
|
|
public function initServiceContext(RunnerServiceContext $context)
|
|
{
|
|
// will throw exception if the test session is not valid
|
|
$this->check($context);
|
|
|
|
// starts the context
|
|
$context->init();
|
|
|
|
return $context;
|
|
}
|
|
|
|
/**
|
|
* Persists the AssessmentTestSession into binary data.
|
|
* @param QtiRunnerServiceContext $context
|
|
*/
|
|
public function persist(QtiRunnerServiceContext $context)
|
|
{
|
|
$testSession = $context->getTestSession();
|
|
$sessionId = $testSession->getSessionId();
|
|
|
|
\common_Logger::d("Persisting QTI Assessment Test Session '${sessionId}'...");
|
|
$context->getStorage()->persist($testSession);
|
|
if ($this->isTerminated($context)) {
|
|
/** @var StorageManager $storageManager */
|
|
$storageManager = $this->getServiceManager()->get(StorageManager::SERVICE_ID);
|
|
$storageManager->persist();
|
|
|
|
$userId = \common_session_SessionManager::getSession()->getUser()->getIdentifier();
|
|
$eventManager = $this->getServiceManager()->get(EventManager::SERVICE_ID);
|
|
$eventManager->trigger(new AfterAssessmentTestSessionClosedEvent($testSession, $userId));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initializes the delivery execution session
|
|
*
|
|
* This method is called whenever a candidate enters the test. This includes
|
|
*
|
|
* * Newly launched/instantiated test session.
|
|
* * The candidate refreshes the client (F5).
|
|
* * Resumed test sessions.
|
|
*
|
|
* @param RunnerServiceContext $context
|
|
* @return boolean
|
|
* @throws \common_Exception
|
|
*/
|
|
public function init(RunnerServiceContext $context)
|
|
{
|
|
if (!$context instanceof QtiRunnerServiceContext) {
|
|
throw new InvalidArgumentTypeException(
|
|
'QtiRunnerService',
|
|
'init',
|
|
0,
|
|
QtiRunnerServiceContext::class,
|
|
$context
|
|
);
|
|
}
|
|
|
|
/* @var TestSession $session */
|
|
$session = $context->getTestSession();
|
|
|
|
// code borrowed from the previous implementation, but the reset timers option has been discarded
|
|
if ($session->getState() === AssessmentTestSessionState::INITIAL) {
|
|
// The test has just been instantiated.
|
|
$session->beginTestSession();
|
|
$event = new TestInitEvent($session);
|
|
$this->getServiceManager()->get(EventManager::SERVICE_ID)->trigger($event);
|
|
\common_Logger::i(sprintf('Assessment Test Session begun. Session id: %s', $session->getSessionId()));
|
|
|
|
if ($context->isAdaptive()) {
|
|
\common_Logger::t("Very first item is adaptive.");
|
|
$nextCatItemId = $context->selectAdaptiveNextItem();
|
|
$context->persistCurrentCatItemId($nextCatItemId);
|
|
$context->persistSeenCatItemIds($nextCatItemId);
|
|
}
|
|
} elseif ($session->getState() === AssessmentTestSessionState::SUSPENDED) {
|
|
$session->resume();
|
|
}
|
|
|
|
$session->initItemTimer();
|
|
if ($session->isTimeout() === false) {
|
|
TestRunnerUtils::beginCandidateInteraction($session);
|
|
}
|
|
|
|
$this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->clearEvents($session->getSessionId());
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Gets the test runner config
|
|
* @return RunnerConfig
|
|
* @throws \common_ext_ExtensionException
|
|
*/
|
|
public function getTestConfig()
|
|
{
|
|
if (is_null($this->testConfig)) {
|
|
$this->testConfig = $this->getServiceManager()->get(QtiRunnerConfig::SERVICE_ID);
|
|
}
|
|
return $this->testConfig;
|
|
}
|
|
|
|
/**
|
|
* Gets the test definition data
|
|
*
|
|
* @deprecated the testData is not necessary anymore
|
|
* if the config is given directly to the test runner configuration
|
|
*
|
|
* @param RunnerServiceContext $context
|
|
* @return array
|
|
* @throws \common_Exception
|
|
*/
|
|
public function getTestData(RunnerServiceContext $context)
|
|
{
|
|
if (!$context instanceof QtiRunnerServiceContext) {
|
|
throw new InvalidArgumentTypeException(
|
|
'QtiRunnerService',
|
|
'getTestData',
|
|
0,
|
|
QtiRunnerServiceContext::class,
|
|
$context
|
|
);
|
|
}
|
|
|
|
$testDefinition = $context->getTestDefinition();
|
|
|
|
$response['title'] = $testDefinition->getTitle();
|
|
$response['identifier'] = $testDefinition->getIdentifier();
|
|
$response['className'] = $testDefinition->getQtiClassName();
|
|
$response['toolName'] = $testDefinition->getToolName();
|
|
$response['exclusivelyLinear'] = $testDefinition->isExclusivelyLinear();
|
|
$response['hasTimeLimits'] = $testDefinition->hasTimeLimits();
|
|
|
|
//states that can be found in the context
|
|
$response['states'] = [
|
|
'initial' => AssessmentTestSessionState::INITIAL,
|
|
'interacting' => AssessmentTestSessionState::INTERACTING,
|
|
'modalFeedback' => AssessmentTestSessionState::MODAL_FEEDBACK,
|
|
'suspended' => AssessmentTestSessionState::SUSPENDED,
|
|
'closed' => AssessmentTestSessionState::CLOSED
|
|
];
|
|
|
|
$response['itemStates'] = [
|
|
'initial' => AssessmentItemSessionState::INITIAL,
|
|
'interacting' => AssessmentItemSessionState::INTERACTING,
|
|
'modalFeedback' => AssessmentItemSessionState::MODAL_FEEDBACK,
|
|
'suspended' => AssessmentItemSessionState::SUSPENDED,
|
|
'closed' => AssessmentItemSessionState::CLOSED,
|
|
'solution' => AssessmentItemSessionState::SOLUTION,
|
|
'review' => AssessmentItemSessionState::REVIEW,
|
|
'notSelected' => AssessmentItemSessionState::NOT_SELECTED
|
|
];
|
|
|
|
$timeLimits = $testDefinition->getTimeLimits();
|
|
if ($timeLimits) {
|
|
if ($timeLimits->hasMinTime()) {
|
|
$response['timeLimits']['minTime'] = [
|
|
'duration' => TestRunnerUtils::getDurationWithMicroseconds($timeLimits->getMinTime()),
|
|
'iso' => $timeLimits->getMinTime()->__toString(),
|
|
];
|
|
}
|
|
|
|
if ($timeLimits->hasMaxTime()) {
|
|
$response['timeLimits']['maxTime'] = [
|
|
'duration' => TestRunnerUtils::getDurationWithMicroseconds($timeLimits->getMaxTime()),
|
|
'iso' => $timeLimits->getMaxTime()->__toString(),
|
|
];
|
|
}
|
|
}
|
|
|
|
$response['config'] = $this->getTestConfig()->getConfig();
|
|
|
|
if ($this->isThemeSwitcherEnabled()) {
|
|
$themeSwitcherPlugin = [
|
|
self::TOOL_ITEM_THEME_SWITCHER => [
|
|
"activeNamespace" => $this->getCurrentThemeId(),
|
|
],
|
|
];
|
|
|
|
$response["config"]["plugins"] = array_merge($response["config"]["plugins"], $themeSwitcherPlugin);
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
|
|
/**
|
|
* Gets the test context object
|
|
* @param RunnerServiceContext $context
|
|
* @return array
|
|
* @throws \common_Exception
|
|
*/
|
|
public function getTestContext(RunnerServiceContext $context)
|
|
{
|
|
if (!$context instanceof QtiRunnerServiceContext) {
|
|
throw new InvalidArgumentTypeException(
|
|
'QtiRunnerService',
|
|
'getTestContext',
|
|
0,
|
|
QtiRunnerServiceContext::class,
|
|
$context
|
|
);
|
|
}
|
|
|
|
/* @var TestSession $session */
|
|
$session = $context->getTestSession();
|
|
|
|
// The state of the test session.
|
|
$response['state'] = $session->getState();
|
|
|
|
// Default values for the test session context.
|
|
$response['navigationMode'] = null;
|
|
$response['submissionMode'] = null;
|
|
$response['remainingAttempts'] = 0;
|
|
$response['isAdaptive'] = false;
|
|
|
|
// Context of interacting test
|
|
if ($session->getState() === AssessmentTestSessionState::INTERACTING) {
|
|
$config = $this->getTestConfig();
|
|
$route = $session->getRoute();
|
|
$currentItem = $route->current();
|
|
$itemSession = $session->getCurrentAssessmentItemSession();
|
|
$itemRef = $context->getCurrentAssessmentItemRef();
|
|
|
|
$reviewConfig = $config->getConfigValue('review');
|
|
$displaySubsectionTitle = isset($reviewConfig['displaySubsectionTitle']) ? (bool) $reviewConfig['displaySubsectionTitle'] : true;
|
|
|
|
if ($displaySubsectionTitle) {
|
|
$currentSection = $session->getCurrentAssessmentSection();
|
|
} else {
|
|
$sections = $currentItem->getAssessmentSections()->getArrayCopy();
|
|
$currentSection = $sections[0];
|
|
}
|
|
|
|
$testOptions = $config->getTestOptions($context);
|
|
|
|
// The navigation mode.
|
|
$response['navigationMode'] = $session->getCurrentNavigationMode();
|
|
$response['isLinear'] = $response['navigationMode'] == NavigationMode::LINEAR;
|
|
|
|
// The submission mode.
|
|
$response['submissionMode'] = $session->getCurrentSubmissionMode();
|
|
|
|
// The number of remaining attempts for the current item.
|
|
$response['remainingAttempts'] = $session->getCurrentRemainingAttempts();
|
|
|
|
// Whether or not the current step is timed out.
|
|
$response['isTimeout'] = $session->isTimeout();
|
|
|
|
// The identifier of the current item.
|
|
$response['itemIdentifier'] = $itemRef->getIdentifier();
|
|
|
|
// The number of current attempt (1 for the first time ...)
|
|
$response['attempt'] = ($context->isAdaptive()) ? $context->getCatAttempts($response['itemIdentifier']) + 1 : $itemSession['numAttempts']->getValue();
|
|
|
|
// The state of the current AssessmentTestSession.
|
|
$response['itemSessionState'] = $itemSession->getState();
|
|
|
|
// Whether the current item is adaptive.
|
|
$response['isAdaptive'] = $session->isCurrentAssessmentItemAdaptive();
|
|
|
|
// Whether the current section is adaptive.
|
|
$response['isCatAdaptive'] = $context->isAdaptive();
|
|
|
|
// Whether the test map must be updated.
|
|
// TODO: detect if the map need to be updated and set the flag
|
|
$response['needMapUpdate'] = false;
|
|
|
|
// Whether the current item is the very last one of the test.
|
|
$response['isLast'] = (!$context->isAdaptive()) ? $route->isLast() : false;
|
|
|
|
// The current position in the route.
|
|
$response['itemPosition'] = $context->getCurrentPosition();
|
|
|
|
// The current item flagged state
|
|
$response['itemFlagged'] = TestRunnerUtils::getItemFlag($session, $response['itemPosition'], $context);
|
|
|
|
// The current item answered state
|
|
$response['itemAnswered'] = $this->isItemCompleted($context, $currentItem, $itemSession);
|
|
|
|
// Time constraints.
|
|
$response['timeConstraints'] = $this->buildTimeConstraints($context);
|
|
|
|
// Test Part title.
|
|
$response['testPartId'] = $session->getCurrentTestPart()->getIdentifier();
|
|
|
|
// Current Section title.
|
|
$response['sectionId'] = $currentSection->getIdentifier();
|
|
$response['sectionTitle'] = $currentSection->getTitle();
|
|
|
|
// Number of items composing the test session.
|
|
$response['numberItems'] = $route->count();
|
|
|
|
// Number of items completed during the test session.
|
|
$response['numberCompleted'] = TestRunnerUtils::testCompletion($session);
|
|
|
|
// Number of items presented during the test session.
|
|
$response['numberPresented'] = $session->numberPresented();
|
|
|
|
// Whether or not the progress of the test can be inferred.
|
|
$response['considerProgress'] = TestRunnerUtils::considerProgress($session, $context->getTestMeta(), $config->getConfig());
|
|
|
|
// Whether or not the deepest current section is visible.
|
|
$response['isDeepestSectionVisible'] = $currentSection->isVisible();
|
|
|
|
// If the candidate is allowed to move backward e.g. first item of the test.
|
|
$response['canMoveBackward'] = $context->canMoveBackward();
|
|
|
|
//Number of rubric blocks
|
|
$response['numberRubrics'] = count($currentItem->getRubricBlockRefs());
|
|
|
|
//add rubic blocks
|
|
if ($response['numberRubrics'] > 0) {
|
|
$response['rubrics'] = $this->getRubrics($context, $session->getCurrentAssessmentItemRef());
|
|
}
|
|
|
|
//prevent the user from submitting empty (i.e. default or null) responses, feature availability
|
|
$response['enableAllowSkipping'] = $config->getConfigValue('enableAllowSkipping');
|
|
|
|
//contextual value
|
|
$response['allowSkipping'] = $testOptions['allowSkipping'];
|
|
|
|
//prevent the user from submitting an invalid response
|
|
$response['enableValidateResponses'] = $config->getConfigValue('enableValidateResponses');
|
|
|
|
//contextual value
|
|
$response['validateResponses'] = $testOptions['validateResponses'];
|
|
|
|
//does the item has modal feedbacks ?
|
|
$response['hasFeedbacks'] = $this->hasFeedbacks($context, $itemRef->getHref());
|
|
|
|
// append dynamic options
|
|
$response['options'] = $testOptions;
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
|
|
/**
|
|
* Gets the map of the test items
|
|
* @param RunnerServiceContext $context
|
|
* @param bool $partial the full testMap or only the current section
|
|
* @return array
|
|
* @throws \common_Exception
|
|
*/
|
|
public function getTestMap(RunnerServiceContext $context, $partial = false)
|
|
{
|
|
if (!$context instanceof QtiRunnerServiceContext) {
|
|
throw new InvalidArgumentTypeException(
|
|
'QtiRunnerService',
|
|
'getTestMap',
|
|
0,
|
|
QtiRunnerServiceContext::class,
|
|
$context
|
|
);
|
|
}
|
|
|
|
$mapService = $this->getServiceLocator()->get(QtiRunnerMap::SERVICE_ID);
|
|
|
|
if ($partial) {
|
|
return $mapService->getScopedMap($context, $this->getTestConfig());
|
|
}
|
|
|
|
return $mapService->getMap($context, $this->getTestConfig());
|
|
}
|
|
|
|
/**
|
|
* Gets the rubrics related to the current session state.
|
|
* @param RunnerServiceContext $context
|
|
* @param AssessmentItemRef $itemRef (optional) otherwise use the current
|
|
* @return mixed
|
|
* @throws \common_Exception
|
|
*/
|
|
public function getRubrics(RunnerServiceContext $context, AssessmentItemRef $itemRef = null)
|
|
{
|
|
if (!$context instanceof QtiRunnerServiceContext) {
|
|
throw new InvalidArgumentTypeException(
|
|
'QtiRunnerService',
|
|
'getRubrics',
|
|
0,
|
|
QtiRunnerServiceContext::class,
|
|
$context
|
|
);
|
|
}
|
|
|
|
$rubricHelper = $this->getServiceLocator()->get(QtiRunnerRubric::SERVICE_ID);
|
|
return $rubricHelper->getRubrics($context, $itemRef);
|
|
}
|
|
|
|
/**
|
|
* Gets AssessmentItemRef's Href by AssessmentItemRef Identifier.
|
|
* @param RunnerServiceContext $context
|
|
* @param string $itemRef
|
|
* @return string
|
|
*/
|
|
public function getItemHref(RunnerServiceContext $context, $itemRef)
|
|
{
|
|
$mapService = $this->getServiceLocator()->get(QtiRunnerMap::SERVICE_ID);
|
|
return $mapService->getItemHref($context, $itemRef);
|
|
}
|
|
|
|
/**
|
|
* Gets definition data of a particular item
|
|
* @param RunnerServiceContext $context
|
|
* @param $itemRef
|
|
* @return mixed
|
|
* @throws \common_Exception
|
|
*/
|
|
public function getItemData(RunnerServiceContext $context, $itemRef)
|
|
{
|
|
if (!$context instanceof QtiRunnerServiceContext) {
|
|
throw new InvalidArgumentTypeException(
|
|
'QtiRunnerService',
|
|
'getItemData',
|
|
0,
|
|
QtiRunnerServiceContext::class,
|
|
$context
|
|
);
|
|
}
|
|
|
|
return $this->loadItemData($itemRef, QtiJsonItemCompiler::ITEM_FILE_NAME);
|
|
}
|
|
|
|
/**
|
|
* Gets the state identifier for a particular item
|
|
* @param QtiRunnerServiceContext $context
|
|
* @param string $itemRef The item identifier
|
|
* @return string The state identifier
|
|
*/
|
|
protected function getStateId(QtiRunnerServiceContext $context, $itemRef)
|
|
{
|
|
return $this->buildStorageItemKey($context->getTestExecutionUri(), $itemRef);
|
|
}
|
|
|
|
/**
|
|
* @param string $deliveryExecutionUri
|
|
* @param string $itemRef
|
|
* @return string
|
|
*/
|
|
private function buildStorageItemKey($deliveryExecutionUri, $itemRef)
|
|
{
|
|
return $deliveryExecutionUri . $itemRef;
|
|
}
|
|
|
|
/**
|
|
* Gets the state of a particular item
|
|
* @param RunnerServiceContext $context
|
|
* @param string $itemRef
|
|
* @return array|null
|
|
* @throws \common_Exception
|
|
*/
|
|
public function getItemState(RunnerServiceContext $context, $itemRef)
|
|
{
|
|
if (!$context instanceof QtiRunnerServiceContext) {
|
|
throw new InvalidArgumentTypeException(
|
|
'QtiRunnerService',
|
|
'getItemState',
|
|
0,
|
|
QtiRunnerServiceContext::class,
|
|
$context
|
|
);
|
|
}
|
|
|
|
$serviceService = $this->getServiceManager()->get(StorageManager::SERVICE_ID);
|
|
$userUri = \common_session_SessionManager::getSession()->getUserUri();
|
|
$stateId = $this->getStateId($context, $itemRef);
|
|
$state = is_null($userUri) ? null : $serviceService->get($userUri, $stateId);
|
|
|
|
if ($state) {
|
|
$state = json_decode($state, true);
|
|
if (is_null($state)) {
|
|
throw new \common_exception_InconsistentData('Unable to decode the state for the item ' . $itemRef);
|
|
}
|
|
}
|
|
|
|
return $state;
|
|
}
|
|
|
|
/**
|
|
* Sets the state of a particular item
|
|
* @param RunnerServiceContext $context
|
|
* @param $itemRef
|
|
* @param $state
|
|
* @return boolean
|
|
* @throws \common_Exception
|
|
*/
|
|
public function setItemState(RunnerServiceContext $context, $itemRef, $state)
|
|
{
|
|
if (!$context instanceof QtiRunnerServiceContext) {
|
|
throw new InvalidArgumentTypeException(
|
|
'QtiRunnerService',
|
|
'setItemState',
|
|
0,
|
|
QtiRunnerServiceContext::class,
|
|
$context
|
|
);
|
|
}
|
|
|
|
$serviceService = $this->getServiceManager()->get(StorageManager::SERVICE_ID);
|
|
$userUri = \common_session_SessionManager::getSession()->getUserUri();
|
|
$stateId = $this->getStateId($context, $itemRef);
|
|
if (!isset($state)) {
|
|
$state = '';
|
|
}
|
|
|
|
return is_null($userUri) ? false : $serviceService->set($userUri, $stateId, json_encode($state));
|
|
}
|
|
|
|
/**
|
|
* @param RunnerServiceContext $context
|
|
* @param $toolStates
|
|
* @throws \oat\oatbox\service\exception\InvalidServiceManagerException
|
|
*/
|
|
public function setToolsStates(RunnerServiceContext $context, $toolStates)
|
|
{
|
|
if ($context instanceof QtiRunnerServiceContext && is_array($toolStates)) {
|
|
/** @var ToolsStateStorage $toolsStateStorage */
|
|
$toolsStateStorage = $this->getServiceLocator()->get(ToolsStateStorage::SERVICE_ID);
|
|
|
|
$toolsStateStorage->storeStates($context->getTestExecutionUri(), $toolStates);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param RunnerServiceContext $context
|
|
* @return array
|
|
* @throws \oat\oatbox\service\exception\InvalidServiceManagerException
|
|
* @throws \common_ext_ExtensionException
|
|
*/
|
|
public function getToolsStates(RunnerServiceContext $context)
|
|
{
|
|
$toolsStates = [];
|
|
|
|
// add those tools missing from the storage but presented on the config
|
|
$toolsEnabled = $this->getTestConfig()->getConfigValue('toolStateServerStorage');
|
|
|
|
if (count($toolsEnabled) === 0) {
|
|
return [];
|
|
}
|
|
if ($context instanceof QtiRunnerServiceContext) {
|
|
/** @var ToolsStateStorage $toolsStateStorage */
|
|
$toolsStateStorage = $this->getServiceLocator()->get(ToolsStateStorage::SERVICE_ID);
|
|
$toolsStates = $toolsStateStorage->getStates($context->getTestExecutionUri());
|
|
}
|
|
|
|
foreach ($toolsEnabled as $toolEnabled) {
|
|
if (!array_key_exists($toolEnabled, $toolsStates)) {
|
|
$toolsStates[$toolEnabled] = null;
|
|
}
|
|
}
|
|
|
|
return $toolsStates;
|
|
}
|
|
|
|
/**
|
|
* Parses the responses provided for a particular item
|
|
* @param RunnerServiceContext $context
|
|
* @param $itemRef
|
|
* @param $response
|
|
* @return mixed
|
|
* @throws \common_Exception
|
|
*/
|
|
public function parsesItemResponse(RunnerServiceContext $context, $itemRef, $response)
|
|
{
|
|
if (!$context instanceof QtiRunnerServiceContext) {
|
|
throw new InvalidArgumentTypeException(
|
|
'QtiRunnerService',
|
|
'storeItemResponse',
|
|
0,
|
|
QtiRunnerServiceContext::class,
|
|
$context
|
|
);
|
|
}
|
|
|
|
/** @var TestSession $session */
|
|
$session = $context->getTestSession();
|
|
$currentItem = $context->getCurrentAssessmentItemRef();
|
|
$responses = new State();
|
|
|
|
if ($currentItem === false) {
|
|
$msg = "Trying to store item variables but the state of the test session is INITIAL or CLOSED.\n";
|
|
$msg .= "Session state value: " . $session->getState() . "\n";
|
|
$msg .= "Session ID: " . $session->getSessionId() . "\n";
|
|
$msg .= "JSON Payload: " . mb_substr(json_encode($response), 0, 1000);
|
|
\common_Logger::e($msg);
|
|
}
|
|
|
|
$filler = new \taoQtiCommon_helpers_PciVariableFiller(
|
|
$currentItem,
|
|
$this->getServiceManager()->get(QtiFlysystemFileManager::SERVICE_ID)
|
|
);
|
|
|
|
if (is_array($response)) {
|
|
foreach ($response as $id => $responseData) {
|
|
try {
|
|
$var = $filler->fill($id, $responseData);
|
|
// Do not take into account QTI File placeholders.
|
|
if (\taoQtiCommon_helpers_Utils::isQtiFilePlaceHolder($var) === false) {
|
|
$responses->setVariable($var);
|
|
}
|
|
} catch (\OutOfRangeException $e) {
|
|
\common_Logger::d("Could not convert client-side value for variable '${id}'.");
|
|
} catch (\OutOfBoundsException $e) {
|
|
\common_Logger::d("Could not find variable with identifier '${id}' in current item.");
|
|
}
|
|
}
|
|
} else {
|
|
\common_Logger::e('Invalid json payload');
|
|
}
|
|
|
|
return $responses;
|
|
}
|
|
|
|
/**
|
|
* Checks if the provided responses are empty
|
|
* @param RunnerServiceContext $context
|
|
* @param $responses
|
|
* @return mixed
|
|
* @throws \common_Exception
|
|
*/
|
|
public function emptyResponse(RunnerServiceContext $context, $responses)
|
|
{
|
|
if (!$context instanceof QtiRunnerServiceContext) {
|
|
throw new InvalidArgumentTypeException(
|
|
'QtiRunnerService',
|
|
'storeItemResponse',
|
|
0,
|
|
QtiRunnerServiceContext::class,
|
|
$context
|
|
);
|
|
}
|
|
|
|
$similar = 0;
|
|
|
|
/** @var ResponseVariable $responseVariable */
|
|
foreach ($responses as $responseVariable) {
|
|
$value = $responseVariable->getValue();
|
|
$default = $responseVariable->getDefaultValue();
|
|
|
|
// Similar to default ?
|
|
if (TestRunnerUtils::isQtiValueNull($value) === true) {
|
|
if (TestRunnerUtils::isQtiValueNull($default) === true) {
|
|
$similar++;
|
|
}
|
|
} elseif ($value->equals($default) === true) {
|
|
$similar++;
|
|
}
|
|
}
|
|
|
|
$respCount = count($responses);
|
|
|
|
return $respCount > 0 && $similar === $respCount;
|
|
}
|
|
|
|
/**
|
|
* Stores the response of a particular item
|
|
* @param RunnerServiceContext $context
|
|
* @param $itemRef
|
|
* @param $responses
|
|
* @return boolean
|
|
* @throws \common_Exception
|
|
*/
|
|
public function storeItemResponse(RunnerServiceContext $context, $itemRef, $responses)
|
|
{
|
|
if (!$context instanceof QtiRunnerServiceContext) {
|
|
throw new InvalidArgumentTypeException(
|
|
'QtiRunnerService',
|
|
'storeItemResponse',
|
|
0,
|
|
QtiRunnerServiceContext::class,
|
|
$context
|
|
);
|
|
}
|
|
|
|
$session = $this->getCurrentAssessmentSession($context);
|
|
|
|
try {
|
|
\common_Logger::t('Responses sent from the client-side. The Response Processing will take place.');
|
|
|
|
if ($context->isAdaptive()) {
|
|
$session->beginItemSession();
|
|
$session->beginAttempt();
|
|
$session->endAttempt($responses);
|
|
|
|
$assessmentItem = $session->getAssessmentItem();
|
|
$assessmentItemIdentifier = $assessmentItem->getIdentifier();
|
|
$score = $session->getVariable('SCORE');
|
|
$output = $context->getLastCatItemOutput();
|
|
|
|
if ($score !== null) {
|
|
$output[$assessmentItemIdentifier] = new ItemResult(
|
|
$assessmentItemIdentifier,
|
|
new ResultVariable(
|
|
$score->getIdentifier(),
|
|
BaseType::getNameByConstant($score->getBaseType()),
|
|
$score->getValue()->getValue(),
|
|
null,
|
|
$score->getCardinality()
|
|
),
|
|
microtime(true)
|
|
);
|
|
} else {
|
|
\common_Logger::i("No 'SCORE' outcome variable for item '${assessmentItemIdentifier}' involved in an adaptive section.");
|
|
}
|
|
|
|
$context->persistLastCatItemOutput($output);
|
|
|
|
// Send results to TAO Results.
|
|
$resultTransmitter = new \taoQtiCommon_helpers_ResultTransmitter($context->getSessionManager()->getResultServer());
|
|
|
|
$hrefParts = explode('|', $assessmentItem->getHref());
|
|
$sessionId = $context->getTestSession()->getSessionId();
|
|
$itemIdentifier = $assessmentItem->getIdentifier();
|
|
|
|
// Deal with attempts.
|
|
$attempt = $context->getCatAttempts($itemIdentifier);
|
|
$transmissionId = "${sessionId}.${itemIdentifier}.${attempt}";
|
|
|
|
$attempt++;
|
|
|
|
foreach ($session->getAllVariables() as $var) {
|
|
if ($var->getIdentifier() === 'numAttempts') {
|
|
$var->setValue(new \qtism\common\datatypes\QtiInteger($attempt));
|
|
}
|
|
|
|
$variables[] = $var;
|
|
}
|
|
|
|
$resultTransmitter->transmitItemVariable($variables, $transmissionId, $hrefParts[0], $hrefParts[2]);
|
|
$context->persistCatAttempts($itemIdentifier, $attempt);
|
|
|
|
$context->getTestSession()->endAttempt(new State(), true);
|
|
} else {
|
|
// Non adaptive case.
|
|
$session->endAttempt($responses, true);
|
|
}
|
|
|
|
return true;
|
|
} catch (AssessmentTestSessionException $e) {
|
|
\common_Logger::w($e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Should we display feedbacks
|
|
* @param RunnerServiceContext $context
|
|
* @return boolean
|
|
* @throws \common_exception_InvalidArgumentType
|
|
*/
|
|
public function displayFeedbacks(RunnerServiceContext $context)
|
|
{
|
|
if (!$context instanceof QtiRunnerServiceContext) {
|
|
|
|
throw new InvalidArgumentTypeException(
|
|
'QtiRunnerService',
|
|
'displayFeedbacks',
|
|
0,
|
|
QtiRunnerServiceContext::class,
|
|
$context
|
|
);
|
|
}
|
|
|
|
/* @var TestSession $session */
|
|
$session = $context->getTestSession();
|
|
|
|
return $session->getCurrentSubmissionMode() !== SubmissionMode::SIMULTANEOUS;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get feedback definitions
|
|
*
|
|
* @param RunnerServiceContext $context
|
|
* @param string $itemRef the item reference
|
|
* @return array the feedbacks data
|
|
* @throws \common_Exception
|
|
* @throws \common_exception_InvalidArgumentType
|
|
* @deprecated since version 30.7.0, to be removed in 31.0.0. Use getItemVariableElementsData() instead
|
|
*/
|
|
public function getFeedbacks(RunnerServiceContext $context, $itemRef)
|
|
{
|
|
return $this->getItemVariableElementsData($context, $itemRef);
|
|
}
|
|
|
|
/**
|
|
* @param RunnerServiceContext $context
|
|
* @param $itemRef
|
|
* @return array
|
|
* @throws \common_Exception
|
|
* @throws \common_exception_InvalidArgumentType
|
|
*/
|
|
public function getItemVariableElementsData(RunnerServiceContext $context, $itemRef)
|
|
{
|
|
$this->assertQtiRunnerServiceContext($context);
|
|
|
|
return $this->loadItemData($itemRef, QtiJsonItemCompiler::VAR_ELT_FILE_NAME);
|
|
}
|
|
|
|
/**
|
|
* Does the given item has feedbacks
|
|
*
|
|
* @param RunnerServiceContext $context
|
|
* @param string $itemRef the item reference
|
|
* @return boolean
|
|
* @throws \common_Exception
|
|
* @throws \common_exception_InconsistentData
|
|
* @throws \common_exception_InvalidArgumentType
|
|
* @throws \tao_models_classes_FileNotFoundException
|
|
*/
|
|
public function hasFeedbacks(RunnerServiceContext $context, $itemRef)
|
|
{
|
|
$hasFeedbacks = false;
|
|
$displayFeedbacks = $this->displayFeedbacks($context);
|
|
if ($displayFeedbacks) {
|
|
$feedbacks = $this->getFeedbacks($context, $itemRef);
|
|
foreach ($feedbacks as $entry) {
|
|
if (isset($entry['feedbackRules'])) {
|
|
if (count($entry['feedbackRules']) > 0) {
|
|
$hasFeedbacks = true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return $hasFeedbacks;
|
|
}
|
|
/**
|
|
* Should we display feedbacks
|
|
* @param RunnerServiceContext $context
|
|
* @return array the item session
|
|
* @throws \common_exception_InvalidArgumentType
|
|
*/
|
|
public function getItemSession(RunnerServiceContext $context)
|
|
{
|
|
if (!$context instanceof QtiRunnerServiceContext) {
|
|
throw new InvalidArgumentTypeException(
|
|
'QtiRunnerService',
|
|
'getItemSession',
|
|
0,
|
|
QtiRunnerServiceContext::class,
|
|
$context
|
|
);
|
|
}
|
|
|
|
/* @var TestSession $session */
|
|
$session = $context->getTestSession();
|
|
|
|
$currentItem = $session->getCurrentAssessmentItemRef();
|
|
$currentOccurrence = $session->getCurrentAssessmentItemRefOccurence();
|
|
|
|
$itemSession = $session->getAssessmentItemSessionStore()->getAssessmentItemSession($currentItem, $currentOccurrence);
|
|
|
|
$stateOutput = new \taoQtiCommon_helpers_PciStateOutput();
|
|
|
|
foreach ($itemSession->getAllVariables() as $var) {
|
|
$stateOutput->addVariable($var);
|
|
}
|
|
|
|
$output = $stateOutput->getOutput();
|
|
|
|
// The current item answered state
|
|
$route = $session->getRoute();
|
|
$position = $route->getPosition();
|
|
$output['itemAnswered'] = TestRunnerUtils::isItemCompleted($route->getRouteItemAt($position), $itemSession);
|
|
|
|
return $output;
|
|
}
|
|
|
|
/**
|
|
* Moves the current position to the provided scoped reference.
|
|
* @param RunnerServiceContext $context
|
|
* @param $direction
|
|
* @param $scope
|
|
* @param $ref
|
|
* @return boolean
|
|
* @throws \common_Exception
|
|
*/
|
|
public function move(RunnerServiceContext $context, $direction, $scope, $ref)
|
|
{
|
|
$result = true;
|
|
|
|
if (!$context instanceof QtiRunnerServiceContext) {
|
|
throw new InvalidArgumentTypeException(
|
|
'QtiRunnerService',
|
|
'move',
|
|
0,
|
|
QtiRunnerServiceContext::class,
|
|
$context
|
|
);
|
|
}
|
|
|
|
try {
|
|
$result = QtiRunnerNavigation::move($direction, $scope, $context, $ref);
|
|
} catch (AssessmentTestSessionException $e) {
|
|
} finally {
|
|
if ($result && (!isset($e) || in_array($e->getCode(), self::TIMEOUT_EXCEPTION_CODES, true))) {
|
|
$this->continueInteraction($context);
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Skips the current position to the provided scoped reference
|
|
* @param RunnerServiceContext $context
|
|
* @param $scope
|
|
* @param $ref
|
|
* @return boolean
|
|
* @throws \common_Exception
|
|
*/
|
|
public function skip(RunnerServiceContext $context, $scope, $ref)
|
|
{
|
|
return $this->move($context, 'skip', $scope, $ref);
|
|
}
|
|
|
|
/**
|
|
* Handles a test timeout
|
|
* @param RunnerServiceContext $context
|
|
* @param $scope
|
|
* @param $ref
|
|
* @param $late
|
|
* @return boolean
|
|
* @throws \common_Exception
|
|
*/
|
|
public function timeout(RunnerServiceContext $context, $scope, $ref, $late = false)
|
|
{
|
|
if (!$context instanceof QtiRunnerServiceContext) {
|
|
throw new InvalidArgumentTypeException(
|
|
'QtiRunnerService',
|
|
'timeout',
|
|
0,
|
|
QtiRunnerServiceContext::class,
|
|
$context
|
|
);
|
|
}
|
|
|
|
/* @var TestSession $session */
|
|
$session = $context->getTestSession();
|
|
if ($context->isAdaptive()) {
|
|
\common_Logger::t("Select next item before timeout");
|
|
$context->selectAdaptiveNextItem();
|
|
}
|
|
try {
|
|
$session->closeTimer($ref, $scope);
|
|
if ($late) {
|
|
if ($scope == 'assessmentTest') {
|
|
$code = AssessmentTestSessionException::ASSESSMENT_TEST_DURATION_OVERFLOW;
|
|
} elseif ($scope == 'testPart') {
|
|
$code = AssessmentTestSessionException::TEST_PART_DURATION_OVERFLOW;
|
|
} elseif ($scope == 'assessmentSection') {
|
|
$code = AssessmentTestSessionException::ASSESSMENT_SECTION_DURATION_OVERFLOW;
|
|
} else {
|
|
$code = AssessmentTestSessionException::ASSESSMENT_ITEM_DURATION_OVERFLOW;
|
|
}
|
|
throw new AssessmentTestSessionException("Maximum duration of ${scope} '${ref}' not respected.", $code);
|
|
} else {
|
|
$session->checkTimeLimits(false, true, false);
|
|
}
|
|
} catch (AssessmentTestSessionException $e) {
|
|
$this->onTimeout($context, $e);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Exits the test before its end
|
|
* @param RunnerServiceContext $context
|
|
* @return boolean
|
|
* @throws \common_Exception
|
|
*/
|
|
public function exitTest(RunnerServiceContext $context)
|
|
{
|
|
if (!$context instanceof QtiRunnerServiceContext) {
|
|
throw new InvalidArgumentTypeException(
|
|
'QtiRunnerService',
|
|
'exitTest',
|
|
0,
|
|
QtiRunnerServiceContext::class,
|
|
$context
|
|
);
|
|
}
|
|
|
|
/* @var TestSession $session */
|
|
$session = $context->getTestSession();
|
|
$sessionId = $session->getSessionId();
|
|
\common_Logger::i("The user has requested termination of the test session '{$sessionId}'");
|
|
|
|
if ($context->isAdaptive()) {
|
|
\common_Logger::t("Select next item before test exit");
|
|
$context->selectAdaptiveNextItem();
|
|
}
|
|
|
|
$event = new TestExitEvent($session);
|
|
$this->getServiceManager()->get(EventManager::SERVICE_ID)->trigger($event);
|
|
|
|
$session->endTestSession();
|
|
|
|
$this->finish($context, $this->getStateAfterExit());
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Finishes the test
|
|
* @param RunnerServiceContext $context
|
|
* @param string $finalState
|
|
* @return boolean
|
|
* @throws \common_Exception
|
|
*/
|
|
public function finish(RunnerServiceContext $context, $finalState = DeliveryExecution::STATE_FINISHED)
|
|
{
|
|
if (!$context instanceof QtiRunnerServiceContext) {
|
|
throw new InvalidArgumentTypeException(
|
|
'QtiRunnerService',
|
|
'finish',
|
|
0,
|
|
QtiRunnerServiceContext::class,
|
|
$context
|
|
);
|
|
}
|
|
|
|
$executionUri = $context->getTestExecutionUri();
|
|
$userUri = \common_session_SessionManager::getSession()->getUserUri();
|
|
|
|
$executionService = ServiceProxy::singleton();
|
|
$deliveryExecution = $executionService->getDeliveryExecution($executionUri);
|
|
|
|
if ($deliveryExecution->getUserIdentifier() == $userUri) {
|
|
\common_Logger::i("Finishing the delivery execution {$executionUri}");
|
|
$result = $deliveryExecution->setState($finalState);
|
|
} else {
|
|
\common_Logger::w("Non owner {$userUri} tried to finish deliveryExecution {$executionUri}");
|
|
$result = false;
|
|
}
|
|
|
|
$this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->clearEvents($executionUri);
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Sets the test to paused state
|
|
* @param RunnerServiceContext $context
|
|
* @return boolean
|
|
* @throws \common_Exception
|
|
*/
|
|
public function pause(RunnerServiceContext $context)
|
|
{
|
|
if (!$context instanceof QtiRunnerServiceContext) {
|
|
throw new InvalidArgumentTypeException(
|
|
'QtiRunnerService',
|
|
'pause',
|
|
0,
|
|
QtiRunnerServiceContext::class,
|
|
$context
|
|
);
|
|
}
|
|
|
|
$context->getTestSession()->suspend();
|
|
$this->persist($context);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Resumes the test from paused state
|
|
* @param RunnerServiceContext $context
|
|
* @return boolean
|
|
* @throws \common_Exception
|
|
*/
|
|
public function resume(RunnerServiceContext $context)
|
|
{
|
|
if (!$context instanceof QtiRunnerServiceContext) {
|
|
throw new InvalidArgumentTypeException(
|
|
'QtiRunnerService',
|
|
'resume',
|
|
0,
|
|
QtiRunnerServiceContext::class,
|
|
$context
|
|
);
|
|
}
|
|
|
|
$context->getTestSession()->resume();
|
|
$this->persist($context);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Checks if the test is still valid
|
|
* @param RunnerServiceContext $context
|
|
* @return boolean
|
|
* @throws \common_Exception
|
|
* @throws QtiRunnerClosedException
|
|
*/
|
|
public function check(RunnerServiceContext $context)
|
|
{
|
|
$state = $context->getTestSession()->getState();
|
|
|
|
if ($state == AssessmentTestSessionState::CLOSED) {
|
|
throw new QtiRunnerClosedException();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Checks if an item has been completed
|
|
* @param RunnerServiceContext $context
|
|
* @param RouteItem $routeItem
|
|
* @param AssessmentItemSession $itemSession
|
|
* @param bool $partially (optional) Whether or not consider partially responded sessions as responded.
|
|
* @return bool
|
|
* @throws \common_Exception
|
|
*/
|
|
public function isItemCompleted(RunnerServiceContext $context, $routeItem, $itemSession, $partially = true)
|
|
{
|
|
if ($context instanceof QtiRunnerServiceContext && $context->isAdaptive()) {
|
|
$itemIdentifier = $context->getCurrentAssessmentItemRef()->getIdentifier();
|
|
$itemState = $this->getItemState($context, $itemIdentifier);
|
|
if ($itemState !== null) {
|
|
// as the item comes from a CAT section, it is simpler to load the responses from the state
|
|
$itemResponse = [];
|
|
foreach ($itemState as $key => $value) {
|
|
if (isset($value['response'])) {
|
|
$itemResponse[$key] = $value['response'];
|
|
}
|
|
}
|
|
$responses = $this->parsesItemResponse($context, $itemIdentifier, $itemResponse);
|
|
|
|
// fork of AssessmentItemSession::isResponded()
|
|
$excludedResponseVariables = ['numAttempts', 'duration'];
|
|
foreach ($responses as $var) {
|
|
if ($var instanceof ResponseVariable && in_array($var->getIdentifier(), $excludedResponseVariables) === false) {
|
|
$value = $var->getValue();
|
|
$defaultValue = $var->getDefaultValue();
|
|
|
|
if (Utils::isNull($value) === true) {
|
|
if (Utils::isNull($defaultValue) === (($partially) ? false : true)) {
|
|
return (($partially) ? true : false);
|
|
}
|
|
} else {
|
|
if ($value->equals($defaultValue) === (($partially) ? false : true)) {
|
|
return (($partially) ? true : false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return (($partially) ? false : true);
|
|
} else {
|
|
return TestRunnerUtils::isItemCompleted($routeItem, $itemSession, $partially);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if the test is in paused state
|
|
* @param RunnerServiceContext $context
|
|
* @return boolean
|
|
*/
|
|
public function isPaused(RunnerServiceContext $context)
|
|
{
|
|
return $context->getTestSession()->getState() == AssessmentTestSessionState::SUSPENDED;
|
|
}
|
|
|
|
/**
|
|
* Checks if the test is in terminated state
|
|
* @param RunnerServiceContext $context
|
|
* @return boolean
|
|
*/
|
|
public function isTerminated(RunnerServiceContext $context)
|
|
{
|
|
return $context->getTestSession()->getState() == AssessmentTestSessionState::CLOSED;
|
|
}
|
|
|
|
/**
|
|
* Get the base url to the item public directory
|
|
* @param RunnerServiceContext $context
|
|
* @param $itemRef
|
|
* @return string
|
|
* @throws \common_Exception
|
|
* @throws \common_exception_Error
|
|
* @throws \common_exception_InvalidArgumentType
|
|
*/
|
|
public function getItemPublicUrl(RunnerServiceContext $context, $itemRef)
|
|
{
|
|
if (!$context instanceof QtiRunnerServiceContext) {
|
|
throw new InvalidArgumentTypeException(
|
|
'QtiRunnerService',
|
|
'getItemPublicUrl',
|
|
0,
|
|
QtiRunnerServiceContext::class,
|
|
$context
|
|
);
|
|
}
|
|
|
|
$directoryIds = explode('|', $itemRef);
|
|
|
|
$userDataLang = \common_session_SessionManager::getSession()->getDataLanguage();
|
|
|
|
$directory = \tao_models_classes_service_FileStorage::singleton()->getDirectoryById($directoryIds[1]);
|
|
// do fallback in case userlanguage is not default language
|
|
if ($userDataLang != DEFAULT_LANG && !$directory->has($userDataLang) && $directory->has(DEFAULT_LANG)) {
|
|
$userDataLang = DEFAULT_LANG;
|
|
}
|
|
|
|
return $directory->getPublicAccessUrl() . $userDataLang . '/';
|
|
}
|
|
|
|
/**
|
|
* Comment the test
|
|
* @param RunnerServiceContext $context
|
|
* @param string $comment
|
|
* @return bool
|
|
*/
|
|
public function comment(RunnerServiceContext $context, $comment)
|
|
{
|
|
// prepare transmission Id for result server.
|
|
$testSession = $context->getTestSession();
|
|
$item = $testSession->getCurrentAssessmentItemRef()->getIdentifier();
|
|
$occurrence = $testSession->getCurrentAssessmentItemRefOccurence();
|
|
$sessionId = $testSession->getSessionId();
|
|
$transmissionId = "${sessionId}.${item}.${occurrence}";
|
|
|
|
/** @var DeliveryServerService $deliveryServerService */
|
|
$deliveryServerService = $this->getServiceManager()->get(DeliveryServerService::SERVICE_ID);
|
|
$resultStore = $deliveryServerService->getResultStoreWrapper($sessionId);
|
|
|
|
$transmitter = new \taoQtiCommon_helpers_ResultTransmitter($resultStore);
|
|
|
|
// build variable and send it.
|
|
$itemUri = TestRunnerUtils::getCurrentItemUri($testSession);
|
|
$testUri = $testSession->getTest()->getUri();
|
|
$variable = new ResponseVariable('comment', Cardinality::SINGLE, BaseType::STRING, new QtismString($comment));
|
|
$transmitter->transmitItemVariable($variable, $transmissionId, $itemUri, $testUri);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Continue the test interaction if possible
|
|
* @param RunnerServiceContext $context
|
|
* @return bool
|
|
*/
|
|
protected function continueInteraction(RunnerServiceContext $context)
|
|
{
|
|
$continue = false;
|
|
|
|
/* @var TestSession $session */
|
|
$session = $context->getTestSession();
|
|
|
|
if ($session->isRunning() === true && $session->isTimeout() === false) {
|
|
$event = new QtiContinueInteractionEvent($context, $this);
|
|
$this->getServiceManager()->get(EventManager::SERVICE_ID)->trigger($event);
|
|
|
|
TestRunnerUtils::beginCandidateInteraction($session);
|
|
$continue = true;
|
|
} else {
|
|
$this->finish($context);
|
|
}
|
|
|
|
return $continue;
|
|
}
|
|
|
|
/**
|
|
* Stuff to be undertaken when the Assessment Item presented to the candidate
|
|
* times out.
|
|
*
|
|
* @param RunnerServiceContext $context
|
|
* @param AssessmentTestSessionException $timeOutException The AssessmentTestSessionException object thrown to indicate the timeout.
|
|
*/
|
|
protected function onTimeout(RunnerServiceContext $context, AssessmentTestSessionException $timeOutException)
|
|
{
|
|
/* @var TestSession $session */
|
|
$session = $context->getTestSession();
|
|
|
|
$event = new TestTimeoutEvent($session, $timeOutException->getCode(), true);
|
|
$this->getServiceManager()->get(EventManager::SERVICE_ID)->trigger($event);
|
|
|
|
$isLinear = $session->getCurrentNavigationMode() === NavigationMode::LINEAR;
|
|
switch ($timeOutException->getCode()) {
|
|
case AssessmentTestSessionException::ASSESSMENT_TEST_DURATION_OVERFLOW:
|
|
\common_Logger::i('TIMEOUT: closing the assessment test session');
|
|
$session->moveThroughAndEndTestSession();
|
|
break;
|
|
|
|
case AssessmentTestSessionException::TEST_PART_DURATION_OVERFLOW:
|
|
if ($isLinear) {
|
|
\common_Logger::i('TIMEOUT: moving to the next test part');
|
|
$session->moveNextTestPart();
|
|
} else {
|
|
\common_Logger::i('TIMEOUT: closing the assessment test part');
|
|
$session->closeTestPart();
|
|
}
|
|
break;
|
|
|
|
case AssessmentTestSessionException::ASSESSMENT_SECTION_DURATION_OVERFLOW:
|
|
if ($isLinear) {
|
|
\common_Logger::i('TIMEOUT: moving to the next assessment section');
|
|
$session->moveNextAssessmentSection();
|
|
} else {
|
|
\common_Logger::i('TIMEOUT: closing the assessment section session');
|
|
$session->closeAssessmentSection();
|
|
}
|
|
break;
|
|
|
|
case AssessmentTestSessionException::ASSESSMENT_ITEM_DURATION_OVERFLOW:
|
|
if ($isLinear) {
|
|
\common_Logger::i('TIMEOUT: moving to the next item');
|
|
$session->moveNextAssessmentItem();
|
|
} else {
|
|
\common_Logger::i('TIMEOUT: closing the assessment item session');
|
|
$session->closeAssessmentItem();
|
|
}
|
|
break;
|
|
}
|
|
|
|
$event = new TestTimeoutEvent($session, $timeOutException->getCode(), false);
|
|
$this->getServiceManager()->get(EventManager::SERVICE_ID)->trigger($event);
|
|
|
|
$this->continueInteraction($context);
|
|
}
|
|
|
|
/**
|
|
* Build an array where each cell represent a time constraint (a.k.a. time limits)
|
|
* in force. Each cell is actually an array with two keys:
|
|
*
|
|
* * 'source': The identifier of the QTI component emitting the constraint (e.g. AssessmentTest, TestPart, AssessmentSection, AssessmentItemRef).
|
|
* * 'seconds': The number of remaining seconds until it times out.
|
|
*
|
|
* @param RunnerServiceContext $context
|
|
* @return array
|
|
*/
|
|
protected function buildTimeConstraints(RunnerServiceContext $context)
|
|
{
|
|
$constraints = [];
|
|
|
|
$session = $context->getTestSession();
|
|
foreach ($session->getRegularTimeConstraints() as $constraint) {
|
|
if ($constraint->getMaximumRemainingTime() != false || $constraint->getMinimumRemainingTime() != false) {
|
|
$constraints[] = $constraint;
|
|
}
|
|
}
|
|
|
|
return $constraints;
|
|
}
|
|
|
|
/**
|
|
* Stores trace variable related to an item, a test or a section
|
|
*
|
|
* @param RunnerServiceContext $context
|
|
* @param $itemUri
|
|
* @param $variableIdentifier
|
|
* @param $variableValue
|
|
* @return boolean
|
|
* @throws \common_Exception
|
|
*/
|
|
public function storeTraceVariable(RunnerServiceContext $context, $itemUri, $variableIdentifier, $variableValue)
|
|
{
|
|
$this->assertQtiRunnerServiceContext($context);
|
|
$metaVariable = $this->getTraceVariable($variableIdentifier, $variableValue);
|
|
return $this->storeVariable($context, $itemUri, $metaVariable);
|
|
}
|
|
|
|
/**
|
|
* Create a trace variable from variable identifier and value
|
|
*
|
|
* @param $variableIdentifier
|
|
* @param $variableValue
|
|
* @return \taoResultServer_models_classes_TraceVariable
|
|
* @throws \common_exception_InvalidArgumentType
|
|
*/
|
|
public function getTraceVariable($variableIdentifier, $variableValue)
|
|
{
|
|
if (!is_string($variableValue) && !is_numeric($variableValue)) {
|
|
$variableValue = json_encode($variableValue);
|
|
}
|
|
$metaVariable = new \taoResultServer_models_classes_TraceVariable();
|
|
$metaVariable->setIdentifier($variableIdentifier);
|
|
$metaVariable->setBaseType('string');
|
|
$metaVariable->setCardinality(Cardinality::getNameByConstant(Cardinality::SINGLE));
|
|
$metaVariable->setTrace($variableValue);
|
|
|
|
return $metaVariable;
|
|
}
|
|
|
|
/**
|
|
* Stores outcome variable related to an item, a test or a section
|
|
*
|
|
* @param RunnerServiceContext $context
|
|
* @param $itemUri
|
|
* @param $variableIdentifier
|
|
* @param $variableValue
|
|
* @return boolean
|
|
* @throws \common_Exception
|
|
*/
|
|
public function storeOutcomeVariable(RunnerServiceContext $context, $itemUri, $variableIdentifier, $variableValue)
|
|
{
|
|
$this->assertQtiRunnerServiceContext($context);
|
|
$metaVariable = $this->getOutcomeVariable($variableIdentifier, $variableValue);
|
|
return $this->storeVariable($context, $itemUri, $metaVariable);
|
|
}
|
|
|
|
/**
|
|
* Create an outcome variable from variable identifier and value
|
|
*
|
|
* @param $variableIdentifier
|
|
* @param $variableValue
|
|
* @return \taoResultServer_models_classes_OutcomeVariable
|
|
* @throws \common_exception_InvalidArgumentType
|
|
*/
|
|
public function getOutcomeVariable($variableIdentifier, $variableValue)
|
|
{
|
|
if (!is_string($variableValue) && !is_numeric($variableValue)) {
|
|
$variableValue = json_encode($variableValue);
|
|
}
|
|
$metaVariable = new \taoResultServer_models_classes_OutcomeVariable();
|
|
$metaVariable->setIdentifier($variableIdentifier);
|
|
$metaVariable->setBaseType('string');
|
|
$metaVariable->setCardinality(Cardinality::getNameByConstant(Cardinality::SINGLE));
|
|
$metaVariable->setValue($variableValue);
|
|
|
|
return $metaVariable;
|
|
}
|
|
|
|
/**
|
|
* Stores response variable related to an item, a test or a section
|
|
*
|
|
* @param RunnerServiceContext $context
|
|
* @param $itemUri
|
|
* @param $variableIdentifier
|
|
* @param $variableValue
|
|
* @return boolean
|
|
* @throws \common_Exception
|
|
*/
|
|
public function storeResponseVariable(RunnerServiceContext $context, $itemUri, $variableIdentifier, $variableValue)
|
|
{
|
|
$this->assertQtiRunnerServiceContext($context);
|
|
$metaVariable = $this->getResponseVariable($variableIdentifier, $variableValue);
|
|
return $this->storeVariable($context, $itemUri, $metaVariable);
|
|
}
|
|
|
|
/**
|
|
* Create a response variable from variable identifier and value
|
|
*
|
|
* @param $variableIdentifier
|
|
* @param $variableValue
|
|
* @return \taoResultServer_models_classes_ResponseVariable
|
|
* @throws \common_exception_InvalidArgumentType
|
|
*/
|
|
public function getResponseVariable($variableIdentifier, $variableValue)
|
|
{
|
|
if (!is_string($variableValue) && !is_numeric($variableValue)) {
|
|
$variableValue = json_encode($variableValue);
|
|
}
|
|
$metaVariable = new \taoResultServer_models_classes_ResponseVariable();
|
|
$metaVariable->setIdentifier($variableIdentifier);
|
|
$metaVariable->setBaseType('string');
|
|
$metaVariable->setCardinality(Cardinality::getNameByConstant(Cardinality::SINGLE));
|
|
$metaVariable->setValue($variableValue);
|
|
|
|
return $metaVariable;
|
|
}
|
|
|
|
/**
|
|
* Store a set of result variables to the result server
|
|
*
|
|
* @param QtiRunnerServiceContext $context
|
|
* @param string $itemUri This is the item uri
|
|
* @param \taoResultServer_models_classes_Variable[] $metaVariables
|
|
* @param null $itemId The assessment item ref id (optional)
|
|
* @return bool
|
|
* @throws \Exception
|
|
* @throws \common_exception_NotImplemented If the given $itemId is not the current assessment item ref
|
|
*/
|
|
public function storeVariables(
|
|
QtiRunnerServiceContext $context,
|
|
$itemUri,
|
|
$metaVariables,
|
|
$itemId = null
|
|
) {
|
|
$sessionId = $context->getTestSession()->getSessionId();
|
|
|
|
/** @var DeliveryServerService $deliveryServerService */
|
|
$deliveryServerService = $this->getServiceManager()->get(DeliveryServerService::SERVICE_ID);
|
|
$resultStore = $deliveryServerService->getResultStoreWrapper($sessionId);
|
|
|
|
$testUri = $context->getTestDefinitionUri();
|
|
|
|
if (!is_null($itemUri)) {
|
|
$resultStore->storeItemVariables($testUri, $itemUri, $metaVariables, $this->getTransmissionId($context, $itemId));
|
|
} else {
|
|
$resultStore->storeTestVariables($testUri, $metaVariables, $sessionId);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Store a result variable to the result server
|
|
*
|
|
* @param QtiRunnerServiceContext $context
|
|
* @param string $itemUri This is the item identifier
|
|
* @param \taoResultServer_models_classes_Variable $metaVariable
|
|
* @param null $itemId The assessment item ref id (optional)
|
|
* @return bool
|
|
* @throws \common_exception_NotImplemented If the given $itemId is not the current assessment item ref
|
|
*/
|
|
protected function storeVariable(
|
|
QtiRunnerServiceContext $context,
|
|
$itemUri,
|
|
\taoResultServer_models_classes_Variable $metaVariable,
|
|
$itemId = null
|
|
) {
|
|
$sessionId = $context->getTestSession()->getSessionId();
|
|
|
|
$testUri = $context->getTestDefinitionUri();
|
|
|
|
/** @var DeliveryServerService $deliveryServerService */
|
|
$deliveryServerService = $this->getServiceManager()->get(DeliveryServerService::SERVICE_ID);
|
|
$resultStore = $deliveryServerService->getResultStoreWrapper($sessionId);
|
|
|
|
if (!is_null($itemUri)) {
|
|
$resultStore->storeItemVariable($testUri, $itemUri, $metaVariable, $this->getTransmissionId($context, $itemId));
|
|
} else {
|
|
$resultStore->storeTestVariable($testUri, $metaVariable, $sessionId);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Build the transmission based on context and item ref id to store Item variables
|
|
*
|
|
* @param QtiRunnerServiceContext $context
|
|
* @param null $itemId The item ref identifier
|
|
* @return string The transmission id to store item variables
|
|
* @throws \common_exception_NotImplemented If the given $itemId is not the current assessment item ref
|
|
*/
|
|
protected function getTransmissionId(QtiRunnerServiceContext $context, $itemId = null)
|
|
{
|
|
if (is_null($itemId)) {
|
|
$itemId = $context->getCurrentAssessmentItemRef();
|
|
} elseif ($itemId != $context->getCurrentAssessmentItemRef()) {
|
|
throw new \common_exception_NotImplemented('Item variables can be stored only for the current item');
|
|
}
|
|
|
|
$sessionId = $context->getTestSession()->getSessionId();
|
|
$currentOccurrence = $context->getTestSession()->getCurrentAssessmentItemRefOccurence();
|
|
|
|
return $sessionId . '.' . $itemId . '.' . $currentOccurrence;
|
|
}
|
|
|
|
/**
|
|
* Check if the given RunnerServiceContext is a QtiRunnerServiceContext
|
|
*
|
|
* @param RunnerServiceContext $context
|
|
* @throws \common_exception_InvalidArgumentType
|
|
*/
|
|
public function assertQtiRunnerServiceContext(RunnerServiceContext $context)
|
|
{
|
|
if (!$context instanceof QtiRunnerServiceContext) {
|
|
throw new InvalidArgumentTypeException(
|
|
__CLASS__,
|
|
__FUNCTION__,
|
|
0,
|
|
QtiRunnerServiceContext::class,
|
|
$context
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Starts the timer for the current item in the TestSession
|
|
*
|
|
* @param RunnerServiceContext $context
|
|
* @param float $timestamp allow to start the timer at a specific time, or use current when it's null
|
|
* @return bool
|
|
* @throws \common_exception_InvalidArgumentType
|
|
*/
|
|
public function startTimer(RunnerServiceContext $context, $timestamp = null)
|
|
{
|
|
if (!$context instanceof QtiRunnerServiceContext) {
|
|
throw new InvalidArgumentTypeException(
|
|
'QtiRunnerService',
|
|
'startTimer',
|
|
0,
|
|
QtiRunnerServiceContext::class,
|
|
$context
|
|
);
|
|
}
|
|
|
|
/* @var TestSession $session */
|
|
$session = $context->getTestSession();
|
|
if ($session->getState() === AssessmentTestSessionState::INTERACTING) {
|
|
$session->startItemTimer($timestamp);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Ends the timer for the current item in the TestSession
|
|
*
|
|
* @param RunnerServiceContext $context
|
|
* @param float $duration The client side duration to adjust the timer
|
|
* @param float $timestamp allow to end the timer at a specific time, or use current when it's null
|
|
* @return bool
|
|
* @throws \common_exception_InvalidArgumentType
|
|
*/
|
|
public function endTimer(RunnerServiceContext $context, $duration = null, $timestamp = null)
|
|
{
|
|
if (!$context instanceof QtiRunnerServiceContext) {
|
|
throw new InvalidArgumentTypeException(
|
|
'QtiRunnerService',
|
|
'endTimer',
|
|
0,
|
|
QtiRunnerServiceContext::class,
|
|
$context
|
|
);
|
|
}
|
|
|
|
/* @var TestSession $session */
|
|
$session = $context->getTestSession();
|
|
$session->endItemTimer($duration, $timestamp);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Switch the received client store ids. Put the received id if different from the last stored.
|
|
* This enables us to check wether the stores has been changed during a test session.
|
|
* @param RunnerServiceContext $context
|
|
* @param string $receivedStoreId The identifier of the client side store
|
|
* @return string the identifier of the LAST saved client side store
|
|
* @throws \common_exception_InvalidArgumentType
|
|
*/
|
|
public function switchClientStoreId(RunnerServiceContext $context, $receivedStoreId)
|
|
{
|
|
if (!$context instanceof QtiRunnerServiceContext) {
|
|
throw new InvalidArgumentTypeException(
|
|
'QtiRunnerService',
|
|
'switchClientStoreId',
|
|
0,
|
|
QtiRunnerServiceContext::class,
|
|
$context
|
|
);
|
|
}
|
|
|
|
/* @var TestSession $session */
|
|
$session = $context->getTestSession();
|
|
$sessionId = $session->getSessionId();
|
|
|
|
$stateService = $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID);
|
|
$lastStoreId = $stateService->getStoreId($sessionId);
|
|
|
|
if ($lastStoreId == false || $lastStoreId != $receivedStoreId) {
|
|
$stateService->setStoreId($sessionId, $receivedStoreId);
|
|
}
|
|
|
|
return $lastStoreId;
|
|
}
|
|
|
|
/**
|
|
* Get Current Assessment Session.
|
|
*
|
|
* Depending on the context (adaptive or not), it will return an appropriate Assessment Object to deal with.
|
|
*
|
|
* In case of the context is not adaptive, an AssessmentTestSession corresponding to the current test $context is returned.
|
|
*
|
|
* Otherwise, an AssessmentItemSession to deal with is returned.
|
|
*
|
|
* @param \oat\taoQtiTest\models\runner\RunnerServiceContext $context
|
|
* @return \qtism\runtime\tests\AssessmentTestSession|\qtism\runtime\tests\AssessmentItemSession
|
|
*/
|
|
public function getCurrentAssessmentSession(RunnerServiceContext $context)
|
|
{
|
|
if ($context->isAdaptive()) {
|
|
return new AssessmentItemSession($context->getCurrentAssessmentItemRef(), new SessionManager());
|
|
} else {
|
|
return $context->getTestSession();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param TestSession $session
|
|
* @param string $qtiClassName
|
|
* @return null|string
|
|
*/
|
|
public function getTimeLimitsFromSession(TestSession $session, $qtiClassName)
|
|
{
|
|
$maxTimeSeconds = null;
|
|
$item = null;
|
|
switch ($qtiClassName) {
|
|
case 'assessmentTest':
|
|
$item = $session->getAssessmentTest();
|
|
break;
|
|
case 'testPart':
|
|
$item = $session->getCurrentTestPart();
|
|
break;
|
|
case 'assessmentSection':
|
|
$item = $session->getCurrentAssessmentSection();
|
|
break;
|
|
case 'assessmentItemRef':
|
|
$item = $session->getCurrentAssessmentItemRef();
|
|
break;
|
|
}
|
|
|
|
if ($item && $limits = $item->getTimeLimits()) {
|
|
$maxTimeSeconds = $limits->hasMaxTime()
|
|
? $limits->getMaxTime()->getSeconds(true)
|
|
: $maxTimeSeconds;
|
|
}
|
|
|
|
return $maxTimeSeconds;
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
public function deleteDeliveryExecutionData(DeliveryExecutionDeleteRequest $request)
|
|
{
|
|
/** @var StorageManager $storage */
|
|
$storage = $this->getServiceLocator()->get(StorageManager::SERVICE_ID);
|
|
$userUri = $request->getDeliveryExecution()->getUserIdentifier();
|
|
/** @var TestSessionService $testSessionService */
|
|
$testSessionService = $this->getServiceLocator()->get(TestSessionService::SERVICE_ID);
|
|
$session = $testSessionService->getTestSession($request->getDeliveryExecution(), false);
|
|
if ($session === null) {
|
|
$status = $this->deleteExecutionStates(
|
|
$request->getDeliveryExecution()->getIdentifier(),
|
|
$userUri,
|
|
$storage
|
|
);
|
|
} else {
|
|
$status = $this->deleteExecutionStatesBasedOnSession($request, $storage, $userUri, $session);
|
|
}
|
|
|
|
/** @var ToolsStateStorage $toolsStateStorage */
|
|
$toolsStateStorage = $this->getServiceLocator()->get(ToolsStateStorage::SERVICE_ID);
|
|
$toolsStateStorage->deleteStates($request->getDeliveryExecution()->getIdentifier());
|
|
|
|
return $status;
|
|
}
|
|
|
|
/**
|
|
* @param RunnerServiceContext $context
|
|
* @param $itemRef
|
|
* @return array|string
|
|
* @throws \common_Exception
|
|
* @throws \common_exception_InconsistentData
|
|
*/
|
|
public function getItemPortableElements(RunnerServiceContext $context, $itemRef)
|
|
{
|
|
|
|
$portableElementService = new PortableElementService();
|
|
$portableElementService->setServiceLocator($this->getServiceLocator());
|
|
|
|
$portableElements = [];
|
|
try {
|
|
$portableElements = $this->loadItemData($itemRef, QtiJsonItemCompiler::PORTABLE_ELEMENT_FILE_NAME);
|
|
foreach ($portableElements as $portableModel => &$elements) {
|
|
foreach ($elements as $typeIdentifier => &$versions) {
|
|
foreach ($versions as &$portableData) {
|
|
try {
|
|
$portableElementService->setBaseUrlToPortableData($portableData);
|
|
} catch (PortableElementNotFoundException $e) {
|
|
\common_Logger::w('the portable element version does not exist in delivery server');
|
|
} catch (PortableModelMissing $e) {
|
|
\common_Logger::w('the portable element model does not exist in delivery server');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (\tao_models_classes_FileNotFoundException $e) {
|
|
\common_Logger::i('old delivery that does not contain the compiled portable element data in the item ' . $itemRef);
|
|
}
|
|
return $portableElements;
|
|
}
|
|
|
|
/**
|
|
* @param $itemRef
|
|
* @return array|mixed|string
|
|
* @throws \common_Exception
|
|
*/
|
|
public function getItemMetadataElements($itemRef)
|
|
{
|
|
$metadataElements = [];
|
|
try {
|
|
$metadataElements = $this->loadItemData($itemRef, QtiJsonItemCompiler::METADATA_FILE_NAME);
|
|
} catch (\tao_models_classes_FileNotFoundException $e) {
|
|
\common_Logger::i('Old delivery that does not contain the compiled portable element data in the item ' . $itemRef . '. Original message: ' . $e->getMessage());
|
|
} catch (\Exception $e) {
|
|
\common_Logger::w('An exception caught during fetching item metadata elements. Original message: ' . $e->getMessage());
|
|
}
|
|
return $metadataElements;
|
|
}
|
|
|
|
/**
|
|
* @param $deUri
|
|
* @param $userUri
|
|
* @param $storage
|
|
* @return mixed
|
|
*/
|
|
protected function deleteExecutionStates($deUri, $userUri, StorageManager $storage)
|
|
{
|
|
$stateStorage = $storage->getStorage();
|
|
$persistence = common_persistence_KeyValuePersistence::getPersistence(
|
|
$stateStorage->getOption(tao_models_classes_service_StateStorage::OPTION_PERSISTENCE)
|
|
);
|
|
|
|
$driver = $persistence->getDriver();
|
|
if ($driver instanceof common_persistence_AdvKeyValuePersistence) {
|
|
$keys = $driver->keys(tao_models_classes_service_StateStorage::KEY_NAMESPACE . '*' . $deUri . '*');
|
|
foreach ($keys as $key) {
|
|
$driver->del($key);
|
|
}
|
|
|
|
return $storage->persist($userUri);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param DeliveryExecutionDeleteRequest $request
|
|
* @param StorageManager $storage
|
|
* @param $userUri
|
|
* @param AssessmentTestSession $session
|
|
* @return bool
|
|
* @throws \common_exception_NotFound
|
|
*/
|
|
protected function deleteExecutionStatesBasedOnSession(DeliveryExecutionDeleteRequest $request, StorageManager $storage, $userUri, AssessmentTestSession $session)
|
|
{
|
|
$itemsRefs = $this->getItemsRefs($request, $session);
|
|
foreach ($itemsRefs as $itemRef) {
|
|
$stateId = $this->buildStorageItemKey(
|
|
$request->getDeliveryExecution()->getIdentifier(),
|
|
$itemRef
|
|
);
|
|
if ($storage->has($userUri, $stateId)) {
|
|
$storage->del($userUri, $stateId);
|
|
}
|
|
}
|
|
|
|
return $storage->persist($userUri);
|
|
}
|
|
|
|
/**
|
|
* @param DeliveryExecutionDeleteRequest $request
|
|
* @param AssessmentTestSession $session
|
|
* @return array
|
|
*/
|
|
protected function getItemsRefs(DeliveryExecutionDeleteRequest $request, AssessmentTestSession $session)
|
|
{
|
|
try {
|
|
$itemsRefs = (new GetDeliveryExecutionsItems(
|
|
$this->getServiceLocator()->get(RuntimeService::SERVICE_ID),
|
|
$this->getServiceLocator()->get(CatService::SERVICE_ID),
|
|
\tao_models_classes_service_FileStorage::singleton(),
|
|
$request->getDeliveryExecution(),
|
|
$session
|
|
))->getItemsRefs();
|
|
} catch (\Exception $exception) {
|
|
$itemsRefs = [];
|
|
}
|
|
|
|
return $itemsRefs;
|
|
}
|
|
|
|
/**
|
|
* Get state of delivery execution after exit triggered by test taker
|
|
* @return string
|
|
*/
|
|
protected function getStateAfterExit()
|
|
{
|
|
return DeliveryExecution::STATE_FINISHED;
|
|
}
|
|
|
|
/**
|
|
* Returns that the Theme Switcher Plugin is enabled or not
|
|
*
|
|
* @return bool
|
|
* @throws \common_ext_ExtensionException
|
|
*/
|
|
private function isThemeSwitcherEnabled()
|
|
{
|
|
/** @var \common_ext_ExtensionsManager $extensionsManager */
|
|
$extensionsManager = $this->getServiceLocator()->get(\common_ext_ExtensionsManager::SERVICE_ID);
|
|
$config = $extensionsManager->getExtensionById("taoTests")->getConfig("test_runner_plugin_registry");
|
|
|
|
return array_key_exists(self::TOOL_ITEM_THEME_SWITCHER_KEY, $config)
|
|
&& $config[self::TOOL_ITEM_THEME_SWITCHER_KEY]["active"] === true;
|
|
}
|
|
|
|
/**
|
|
* Returns the ID of the current theme
|
|
*
|
|
* @return string
|
|
* @throws \common_exception_InconsistentData
|
|
*/
|
|
private function getCurrentThemeId()
|
|
{
|
|
/** @var ThemeService $themeService */
|
|
$themeService = $this->getServiceLocator()->get(ThemeService::SERVICE_ID);
|
|
|
|
return $themeService->getTheme()->getId();
|
|
}
|
|
}
|