581 lines
20 KiB
PHP
581 lines
20 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
|
|
*
|
|
*/
|
|
|
|
namespace oat\taoQtiTest\models\runner\session;
|
|
|
|
use oat\oatbox\service\ServiceManager;
|
|
use oat\taoQtiTest\models\runner\config\QtiRunnerConfig;
|
|
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\cat\CatService;
|
|
use oat\taoTests\models\runner\time\TimePoint;
|
|
use qtism\common\datatypes\QtiDuration;
|
|
use qtism\data\AssessmentSection;
|
|
use qtism\data\TestPart;
|
|
use qtism\runtime\tests\AssessmentItemSession;
|
|
use qtism\runtime\tests\AssessmentTestPlace;
|
|
use qtism\runtime\tests\AssessmentTestSessionException;
|
|
use qtism\runtime\tests\RouteItem;
|
|
use qtism\runtime\tests\TimeConstraint;
|
|
use qtism\runtime\tests\TimeConstraintCollection;
|
|
use taoQtiTest_helpers_TestSession;
|
|
use oat\oatbox\log\LoggerAwareTrait;
|
|
|
|
/**
|
|
* TestSession override
|
|
*
|
|
* @author Bertrand Chevrier <bertrand@taotesting.com>
|
|
*/
|
|
class TestSession extends taoQtiTest_helpers_TestSession implements UserUriAware
|
|
{
|
|
use LoggerAwareTrait;
|
|
|
|
/**
|
|
* The Timer bound to the test session
|
|
* @var QtiTimer
|
|
*/
|
|
protected $timer;
|
|
|
|
/**
|
|
* The target from which compute the durations
|
|
* @var int
|
|
*/
|
|
protected $timerTarget;
|
|
|
|
/**
|
|
* A temporary cache for computed durations
|
|
* @var array
|
|
*/
|
|
protected $durationCache = [];
|
|
|
|
/**
|
|
* The URI (Uniform Resource Identifier) of the user the Test Session belongs to.
|
|
*
|
|
* @var string
|
|
*/
|
|
private $userUri;
|
|
|
|
/**
|
|
* Get the URI (Uniform Resource Identifier) of the user the Test Session belongs to.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getUserUri()
|
|
{
|
|
if (is_null($this->userUri)) {
|
|
return \common_session_SessionManager::getSession()->getUserUri();
|
|
}
|
|
return $this->userUri;
|
|
}
|
|
|
|
/**
|
|
* Set the URI (Uniform Resource Identifier) of the user the Test Session belongs to.
|
|
*
|
|
* @param string $userUri
|
|
*/
|
|
public function setUserUri($userUri)
|
|
{
|
|
$this->userUri = $userUri;
|
|
}
|
|
|
|
/**
|
|
* Gets the Timer bound to the test session
|
|
* @return QtiTimer
|
|
*/
|
|
public function getTimer()
|
|
{
|
|
if (!$this->timer) {
|
|
$qtiTimerFactory = $this->getServiceLocator()->get(QtiTimerFactory::SERVICE_ID);
|
|
$this->timer = $qtiTimerFactory->getTimer($this->getSessionId(), $this->getUserUri());
|
|
}
|
|
return $this->timer;
|
|
}
|
|
|
|
/**
|
|
* Gets the target from which compute the durations
|
|
* @return int
|
|
*/
|
|
public function getTimerTarget()
|
|
{
|
|
if (is_null($this->timerTarget)) {
|
|
$testConfig = $this->getServiceLocator()->get(QtiRunnerConfig::SERVICE_ID);
|
|
$config = $testConfig->getConfigValue('timer');
|
|
switch (strtolower($config['target'])) {
|
|
case 'client':
|
|
$target = TimePoint::TARGET_CLIENT;
|
|
break;
|
|
|
|
case 'server':
|
|
default:
|
|
$target = TimePoint::TARGET_SERVER;
|
|
}
|
|
|
|
$this->setTimerTarget($target);
|
|
}
|
|
return $this->timerTarget;
|
|
}
|
|
|
|
/**
|
|
* Set the target from which compute the durations
|
|
* @param int $timerTarget
|
|
*/
|
|
public function setTimerTarget($timerTarget)
|
|
{
|
|
$this->timerTarget = intval($timerTarget);
|
|
}
|
|
|
|
/**
|
|
* Gets the tags describing a particular item with an assessment test
|
|
* @param RouteItem $routeItem
|
|
* @return array
|
|
*/
|
|
public function getItemTags(RouteItem $routeItem)
|
|
{
|
|
$test = $routeItem->getAssessmentTest();
|
|
$testPart = $routeItem->getTestPart();
|
|
$sections = $routeItem->getAssessmentSections();
|
|
$sections->rewind();
|
|
$sectionId = key(current($sections));
|
|
$itemRef = $routeItem->getAssessmentItemRef();
|
|
$itemId = $itemRef->getIdentifier();
|
|
$occurrence = $routeItem->getOccurence();
|
|
|
|
$tags = [
|
|
$itemId,
|
|
$itemId . '#' . $occurrence,
|
|
$sectionId,
|
|
$testPart->getIdentifier(),
|
|
$test->getIdentifier(),
|
|
];
|
|
|
|
if ($this->isRunning() === true) {
|
|
$tags[] = $this->getItemAttemptTag($routeItem);
|
|
}
|
|
|
|
return $tags;
|
|
}
|
|
|
|
/**
|
|
* Gets the item tags for its last occurrence
|
|
* @param RouteItem $routeItem
|
|
* @return string
|
|
*/
|
|
public function getItemAttemptTag(RouteItem $routeItem)
|
|
{
|
|
$itemRef = $routeItem->getAssessmentItemRef();
|
|
$itemId = $itemRef->getIdentifier();
|
|
$occurrence = $routeItem->getOccurence();
|
|
$itemSession = $this->getAssessmentItemSessionStore()->getAssessmentItemSession($itemRef, $occurrence);
|
|
return $itemId . '#' . $occurrence . '-' . $itemSession['numAttempts']->getValue();
|
|
}
|
|
|
|
/**
|
|
* Initializes the timer for the current item in the TestSession
|
|
*
|
|
* @param $timestamp
|
|
* @throws \oat\taoTests\models\runner\time\InvalidDataException
|
|
*/
|
|
public function initItemTimer($timestamp = null)
|
|
{
|
|
if (is_null($timestamp)) {
|
|
$timestamp = microtime(true);
|
|
}
|
|
|
|
// try to close existing time range if any, in order to be sure the test will start or restart a new range.
|
|
// if the range is already closed, a message will be added to the log
|
|
$tags = $this->getItemTags($this->getCurrentRouteItem());
|
|
$this->getTimer()->end($tags, $timestamp)->save();
|
|
}
|
|
|
|
/**
|
|
* Starts the timer for the current item in the TestSession
|
|
*
|
|
* @param $timestamp
|
|
*/
|
|
public function startItemTimer($timestamp = null)
|
|
{
|
|
if (is_null($timestamp)) {
|
|
$timestamp = microtime(true);
|
|
}
|
|
$tags = $this->getItemTags($this->getCurrentRouteItem());
|
|
$this->getTimer()->start($tags, $timestamp)->save();
|
|
}
|
|
|
|
/**
|
|
* Ends the timer for the current item in the TestSession.
|
|
* Sets the client duration for the current item in the TestSession.
|
|
*
|
|
* @param float $duration The client duration, or null to force server duration to be used as client duration
|
|
* @param $timestamp
|
|
* @throws \oat\taoTests\models\runner\time\InvalidStorageException
|
|
* @throws \oat\taoTests\models\runner\time\TimeException
|
|
*/
|
|
public function endItemTimer($duration = null, $timestamp = null)
|
|
{
|
|
if (is_null($timestamp)) {
|
|
$timestamp = microtime(true);
|
|
}
|
|
$timer = $this->getTimer();
|
|
$currentItem = $this->getCurrentRouteItem();
|
|
|
|
if ($currentItem) {
|
|
$tags = $this->getItemTags($currentItem);
|
|
} else {
|
|
$tags = [];
|
|
}
|
|
|
|
$timer->end($tags, $timestamp);
|
|
|
|
if (is_numeric($duration) || is_null($duration)) {
|
|
if (!is_null($duration)) {
|
|
$duration = floatval($duration);
|
|
}
|
|
try {
|
|
$timer->adjust($tags, $duration);
|
|
} catch (\oat\taoTests\models\runner\time\TimeException $e) {
|
|
$this->logAlert($e->getMessage() . '; Test session identifier: ' . $this->getSessionId());
|
|
}
|
|
}
|
|
$constraints = $this->getTimeConstraints();
|
|
|
|
$maxTime = 0;
|
|
/** @var QtiTimeConstraint $constraint */
|
|
foreach ($constraints as $constraint) {
|
|
if (($maximumTime = $constraint->getAdjustedMaxTime()) !== null) {
|
|
$maxTime = $maximumTime->getSeconds(true);
|
|
}
|
|
}
|
|
$this->getTimer()->getConsumedExtraTime($tags, $maxTime);
|
|
$this->updateCurrentDurationCache();
|
|
$timer->save();
|
|
}
|
|
|
|
/**
|
|
* Gets the timer duration for a particular identifier
|
|
* @param string|array $identifier
|
|
* @param int $target
|
|
* @return QtiDuration
|
|
* @throws \oat\taoTests\models\runner\time\TimeException
|
|
*/
|
|
public function getTimerDuration($identifier, $target = 0)
|
|
{
|
|
if (!$target) {
|
|
$target = $this->getTimerTarget();
|
|
}
|
|
|
|
$durationKey = $this->getDurationKey($identifier, $target);
|
|
|
|
if (!isset($this->durationCache[$durationKey])) {
|
|
$this->updateDurationCache($identifier, $target);
|
|
}
|
|
|
|
return $this->durationCache[$durationKey];
|
|
}
|
|
|
|
/**
|
|
* Gets the timer duration key for a particular identifier
|
|
* @param string|array $identifier
|
|
* @param int $target
|
|
* @return string
|
|
*/
|
|
protected function getDurationKey($identifier, int $target): string
|
|
{
|
|
$durationKey = $target . '-';
|
|
if (is_array($identifier)) {
|
|
sort($identifier);
|
|
$durationKey .= implode('-', $identifier);
|
|
} else {
|
|
$durationKey .= $identifier;
|
|
}
|
|
|
|
return $durationKey;
|
|
}
|
|
|
|
/**
|
|
* Updates the duration cache for a particular identifier
|
|
* @param string|array $identifier
|
|
* @param int $target
|
|
* @return float
|
|
* @throws \oat\taoTests\models\runner\time\TimeException
|
|
*/
|
|
protected function updateDurationCache($identifier, int $target): float
|
|
{
|
|
$duration = round($this->getTimer()->compute($identifier, $target), 6);
|
|
$durationKey = $this->getDurationKey($identifier, $target);
|
|
|
|
$this->durationCache[$durationKey] = new QtiDuration('PT' . $duration . 'S');
|
|
|
|
return $duration;
|
|
}
|
|
|
|
/**
|
|
* Updates the duration cache for all identifiers from the current context
|
|
* @throws \oat\taoTests\models\runner\time\TimeException
|
|
*/
|
|
protected function updateCurrentDurationCache()
|
|
{
|
|
$target = $this->getTimerTarget();
|
|
$routeItem = $this->getCurrentRouteItem();
|
|
$sources = [
|
|
$routeItem->getAssessmentTest(),
|
|
$this->getCurrentTestPart(),
|
|
$this->getCurrentAssessmentSection(),
|
|
$routeItem->getAssessmentItemRef(),
|
|
];
|
|
|
|
foreach ($sources as $source) {
|
|
$this->updateDurationCache($source->getIdentifier(), $target);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the total duration for the current item in the TestSession
|
|
* @param int $target
|
|
* @return QtiDuration
|
|
* @throws \oat\taoTests\models\runner\time\InconsistentCriteriaException
|
|
*/
|
|
public function computeItemTime($target = 0)
|
|
{
|
|
$currentItem = $this->getCurrentAssessmentItemRef();
|
|
return $this->getTimerDuration($currentItem->getIdentifier(), $target);
|
|
}
|
|
|
|
/**
|
|
* Gets the total duration for the current section in the TestSession
|
|
* @param int $target
|
|
* @return QtiDuration
|
|
* @throws \oat\taoTests\models\runner\time\InconsistentCriteriaException
|
|
*/
|
|
public function computeSectionTime($target = 0)
|
|
{
|
|
$routeItem = $this->getCurrentRouteItem();
|
|
$sections = $routeItem->getAssessmentSections();
|
|
$sections->rewind();
|
|
return $this->getTimerDuration(key(current($sections)), $target);
|
|
}
|
|
|
|
/**
|
|
* Gets the total duration for the current test part in the TestSession
|
|
* @param int $target
|
|
* @return QtiDuration
|
|
* @throws \oat\taoTests\models\runner\time\InconsistentCriteriaException
|
|
*/
|
|
public function computeTestPartTime($target = 0)
|
|
{
|
|
$routeItem = $this->getCurrentRouteItem();
|
|
$testPart = $routeItem->getTestPart();
|
|
return $this->getTimerDuration($testPart->getIdentifier(), $target);
|
|
}
|
|
|
|
/**
|
|
* Gets the total duration for the whole assessment test
|
|
* @param int $target
|
|
* @return QtiDuration
|
|
* @throws \oat\taoTests\models\runner\time\InconsistentCriteriaException
|
|
*/
|
|
public function computeTestTime($target = 0)
|
|
{
|
|
$routeItem = $this->getCurrentRouteItem();
|
|
$test = $routeItem->getAssessmentTest();
|
|
return $this->getTimerDuration($test->getIdentifier(), $target);
|
|
}
|
|
|
|
/**
|
|
* Update the durations involved in the AssessmentTestSession to mirror the durations at the current time.
|
|
* This method can be useful for stateless systems that make use of QtiSm.
|
|
*/
|
|
public function updateDuration()
|
|
{
|
|
// not needed anymore
|
|
\common_Logger::t('Call to disabled updateDuration()');
|
|
}
|
|
|
|
/**
|
|
* Gets a TimeConstraint from a particular source
|
|
* @param $source
|
|
* @param $navigationMode
|
|
* @param $considerMinTime
|
|
* @param $applyExtraTime
|
|
* @return TimeConstraint
|
|
* @throws \oat\taoTests\models\runner\time\InconsistentCriteriaException
|
|
*/
|
|
protected function getTimeConstraint($source, $navigationMode, $considerMinTime, $applyExtraTime = true)
|
|
{
|
|
$constraint = new QtiTimeConstraint(
|
|
$source,
|
|
$this->getTimerDuration($source->getIdentifier()),
|
|
$navigationMode,
|
|
$considerMinTime,
|
|
$applyExtraTime,
|
|
$this->getTimerTarget()
|
|
);
|
|
$constraint->setTimer($this->getTimer());
|
|
return $constraint;
|
|
}
|
|
|
|
/**
|
|
* Builds the time constraints running for the current testPart or/and current assessmentSection
|
|
* or/and assessmentItem. Takes care of the extra time if needed.
|
|
*
|
|
* @param integer $places A composition of values (use | operator) from the AssessmentTestPlace enumeration. If the null value is given, all places will be taken into account.
|
|
* @param boolean $applyExtraTime Allow to take care of extra time
|
|
* @return TimeConstraintCollection A collection of TimeConstraint objects.
|
|
* @qtism-test-duration-update
|
|
*/
|
|
protected function buildTimeConstraints($places = null, $applyExtraTime = true)
|
|
{
|
|
if ($places === null) {
|
|
// Get the constraints from all places in the Assessment Test.
|
|
$places = (AssessmentTestPlace::ASSESSMENT_TEST | AssessmentTestPlace::TEST_PART | AssessmentTestPlace::ASSESSMENT_SECTION | AssessmentTestPlace::ASSESSMENT_ITEM);
|
|
}
|
|
|
|
$constraints = new TimeConstraintCollection();
|
|
$navigationMode = $this->getCurrentNavigationMode();
|
|
$routeItem = $this->getCurrentRouteItem();
|
|
$considerMinTime = $this->mustConsiderMinTime();
|
|
|
|
if (($places & AssessmentTestPlace::ASSESSMENT_TEST) && ($routeItem instanceof RouteItem)) {
|
|
$constraints[] = $this->getTimeConstraint($routeItem->getAssessmentTest(), $navigationMode, $considerMinTime, $applyExtraTime);
|
|
}
|
|
|
|
$currentTestPart = $this->getCurrentTestPart();
|
|
if (($places & AssessmentTestPlace::TEST_PART) && ($currentTestPart instanceof TestPart)) {
|
|
$constraints[] = $this->getTimeConstraint($currentTestPart, $navigationMode, $considerMinTime, $applyExtraTime);
|
|
}
|
|
|
|
$currentAssessmentSection = $this->getCurrentAssessmentSection();
|
|
if (($places & AssessmentTestPlace::ASSESSMENT_SECTION) && ($currentAssessmentSection instanceof AssessmentSection)) {
|
|
$constraints[] = $this->getTimeConstraint($currentAssessmentSection, $navigationMode, $considerMinTime, $applyExtraTime);
|
|
}
|
|
|
|
if (($places & AssessmentTestPlace::ASSESSMENT_ITEM) && ($routeItem instanceof RouteItem)) {
|
|
$constraints[] = $this->getTimeConstraint($routeItem->getAssessmentItemRef(), $navigationMode, $considerMinTime, $applyExtraTime);
|
|
}
|
|
|
|
return $constraints;
|
|
}
|
|
|
|
/**
|
|
* Get the time constraints running for the current testPart or/and current assessmentSection
|
|
* or/and assessmentItem. The extra time is taken into account.
|
|
*
|
|
* @param integer $places A composition of values (use | operator) from the AssessmentTestPlace enumeration. If the null value is given, all places will be taken into account.
|
|
* @return TimeConstraintCollection A collection of TimeConstraint objects.
|
|
* @qtism-test-duration-update
|
|
*/
|
|
public function getTimeConstraints($places = null)
|
|
{
|
|
return $this->buildTimeConstraints($places, true);
|
|
}
|
|
|
|
/**
|
|
* Get the regular time constraints running for the current testPart or/and current assessmentSection
|
|
* or/and assessmentItem, without taking care of the extra time.
|
|
*
|
|
* @param integer $places A composition of values (use | operator) from the AssessmentTestPlace enumeration. If the null value is given, all places will be taken into account.
|
|
* @return TimeConstraintCollection A collection of TimeConstraint objects.
|
|
* @qtism-test-duration-update
|
|
*/
|
|
public function getRegularTimeConstraints($places = null)
|
|
{
|
|
return $this->buildTimeConstraints($places, false);
|
|
}
|
|
|
|
/**
|
|
* Whether or not the current Assessment Item to be presented to the candidate is timed-out. By timed-out
|
|
* we mean:
|
|
*
|
|
* * current Assessment Test level time limits are not respected OR,
|
|
* * current Test Part level time limits are not respected OR,
|
|
* * current Assessment Section level time limits are not respected OR,
|
|
* * current Assessment Item level time limits are not respected.
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function isTimeout()
|
|
{
|
|
try {
|
|
$this->checkTimeLimits(false, true, false);
|
|
} catch (AssessmentTestSessionException $e) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* AssessmentTestSession implementations must override this method in order
|
|
* to submit item results from a given $assessmentItemSession to the appropriate
|
|
* data source.
|
|
*
|
|
* This method is triggered each time response processing takes place.
|
|
*
|
|
* @param AssessmentItemSession $itemSession The lastly updated AssessmentItemSession.
|
|
* @param integer $occurrence The occurrence number of the item bound to $assessmentItemSession.
|
|
* @throws AssessmentTestSessionException With error code RESULT_SUBMISSION_ERROR if an error occurs while transmitting results.
|
|
*/
|
|
public function submitItemResults(AssessmentItemSession $itemSession, $occurrence = 0)
|
|
{
|
|
$itemRef = $itemSession->getAssessmentItem();
|
|
|
|
// Ensure that specific results from adaptive placeholders are not recorded.
|
|
$catService = ServiceManager::getServiceManager()->get(CatService::SERVICE_ID);
|
|
if (!$catService->isAdaptivePlaceholder($itemRef)) {
|
|
$identifier = $itemRef->getIdentifier();
|
|
$duration = $this->getTimerDuration($identifier);
|
|
|
|
$itemDurationVar = $itemSession->getVariable('duration');
|
|
$sessionDuration = $itemDurationVar->getValue();
|
|
\common_Logger::t("Force duration of item '${identifier}' to ${duration} instead of ${sessionDuration}");
|
|
$itemSession->getVariable('duration')->setValue($duration);
|
|
|
|
parent::submitItemResults($itemSession, $occurrence);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* QTISM endTestSession method overriding.
|
|
*
|
|
* It consists of including an additional processing when the test ends,
|
|
* in order to send the LtiOutcome
|
|
*
|
|
* @see http://www.imsglobal.org/lis/ Outcome Management Service
|
|
* @throws \taoQtiTest_helpers_TestSessionException If the session is already ended or if an error occurs whil transmitting/processing the result.
|
|
*/
|
|
public function endTestSession()
|
|
{
|
|
// try to close existing time range if any, in order to be sure the test will be closed with a consistent timer.
|
|
// if the range is already closed, a message will be added to the log
|
|
if ($this->isRunning() === true) {
|
|
$route = $this->getRoute();
|
|
if ($route->valid()) {
|
|
$routeItem = $this->getCurrentRouteItem();
|
|
}
|
|
if (isset($routeItem)) {
|
|
$this->endItemTimer();
|
|
}
|
|
}
|
|
|
|
parent::endTestSession();
|
|
}
|
|
}
|