Skip to content

Commit

Permalink
Backport/rfe 748/45.0.6.2 (#2433)
Browse files Browse the repository at this point in the history
* chore: emtpy commit

* chore: update tao-test-runner-qti to branch feature/TR-5858/detect-delivery-concurrency

* chore: install the preventConcurrency plugin

* refactor: use plugin definition defined in the install script

* chore: update signature to allow return null

Co-authored-by: Gabriel Felipe Soares <[email protected]>
Signed-off-by: Jean-Sébastien CONAN <[email protected]>

* chore: remove auto-generated comment

Co-authored-by: Héctor Arroyo <[email protected]>
Signed-off-by: Jean-Sébastien CONAN <[email protected]>

* feat: Pause previous deliveries when the test taker launches a new one (#2421)

* chore: add annotations for things we need to finish/change to achieve task goals

* feat: Detect concurrent sessions by the same user [WiP]

* feat: Trigger pause for other executions

* chore: Clenup

* feat: Store in the session info about why the test was paused

* chore: Remove dead code

* chore: Move PAUSE_REASON_CONCURRENT_TEST constant out of a
 controller

* chore: Cleanup

* feat: Move logic to ltiDeliveryProvider

* chore: move logic outside taoQtiTest

* chore: Rollback unneeded change

* chore: Cleanup

* chore: do not allow to execute the same test in different tabs

* chore: check for the test session conflict for timers

* feat: Pause timers when move redirects to the paused screen

* chore: Cleanup

* chore: Cleanup

* chore: Cleanup

* feat: Send a runNumberId with a unique ID that can be used by the FE to know if the local storage was initialized by a prior run

* chore: Typo

* feat: Force restoreTimerFromClient for suspended sessions on init, return the 'pause' payload when trying to move a paused test

* feat: Wrap detection of concurrent tests behind a feature flag

* chore: Rollback now-unneeded change

* chore: Remove unneeded imports

* chore: Remove constant used to hold a feature flag name literal

* doc: Describe FEATURE_FLAG_PAUSE_CONCURRENT_SESSIONS in the readme

* doc: Describe FEATURE_FLAG_PAUSE_CONCURRENT_SESSIONS in the readme

* chore: Simplify class.Runner.php

Co-authored-by: Gabriel Felipe Soares <[email protected]>
Signed-off-by: Héctor Arroyo <[email protected]>

* chore: Add missing typehint to class.Runner.php

Co-authored-by: Gabriel Felipe Soares <[email protected]>
Signed-off-by: Héctor Arroyo <[email protected]>

* chore: Rollback unneeded change

* chore: refactoring and moving common code to same place

---------

Signed-off-by: Héctor Arroyo <[email protected]>
Co-authored-by: Gabriel Felipe Soares <[email protected]>
Co-authored-by: Andrei Shapiro <[email protected]>

* fix: use string instead of object

* fix: avoid session invalid index error

* chore: remove unnecessary code

* chore: fix psr issues

* chore: fix psr issues

* fix: remove unnecessary code

* test: Unit tests for ConcurringSessionService

* chore: Typehint

* chore: Cleanup

* chore: Cleanup

* chore: Replace double quotes with single ones for strings not needing var interpolation unit test

Signed-off-by: Héctor Arroyo <[email protected]>

* chore: fix identifier issue

* fix: make service work with KV driver

* chore: remove unnecessary code and simplify tests

* feat: update translations

* fix: code typo in scanned translations

* fix: code typo in arb locale

* chore: update tao-test-runner-qti to version 4.1.0

---------

Signed-off-by: Jean-Sébastien CONAN <[email protected]>
Signed-off-by: Héctor Arroyo <[email protected]>
Signed-off-by: Héctor Arroyo <[email protected]>
Co-authored-by: Héctor Arroyo <[email protected]>
Co-authored-by: jsconan <[email protected]>
Co-authored-by: Gabriel Felipe Soares <[email protected]>
  • Loading branch information
4 people authored Jan 23, 2024
1 parent afc05d4 commit 8e9c1bb
Show file tree
Hide file tree
Showing 147 changed files with 5,505 additions and 549 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,4 @@ Environment variables
| Var | Description |
|-----------------------------------------------|-------------------------------------------------------------------------------------|
| FEATURE_FLAG_FORCE_DISPLAY_TEST_ITEM_FEEDBACK | Even if itemSessionControl `showFeedback` option is false, show item feedback modal |
| FEATURE_FLAG_PAUSE_CONCURRENT_SESSIONS | When set, forces restoring the timers state from the browser storage on test init |
92 changes: 71 additions & 21 deletions actions/class.Runner.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* 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-2020 (original work) Open Assessment Technologies SA ;
* Copyright (c) 2016-2023 (original work) Open Assessment Technologies SA.
*/

/**
Expand All @@ -25,10 +25,10 @@
*/

use oat\libCat\exception\CatEngineConnectivityException;
use oat\tao\model\routing\AnnotationReader\security;
use oat\taoDelivery\model\execution\DeliveryExecutionInterface;
use oat\taoDelivery\model\execution\DeliveryExecutionService;
use oat\taoDelivery\model\RuntimeService;
use oat\taoQtiTest\model\Service\ConcurringSessionService;
use oat\taoQtiTest\model\Service\ExitTestCommand;
use oat\taoQtiTest\model\Service\ExitTestService;
use oat\taoQtiTest\model\Service\ItemContextAwareInterface;
Expand Down Expand Up @@ -59,6 +59,7 @@
use oat\taoQtiTest\models\runner\QtiRunnerServiceContext;
use oat\taoQtiTest\models\runner\RunnerToolStates;
use oat\taoQtiTest\models\runner\StorageManager;
use qtism\runtime\tests\AssessmentTestSessionState;
use taoQtiTest_helpers_TestRunnerUtils as TestRunnerUtils;
use oat\oatbox\session\SessionService;

Expand Down Expand Up @@ -143,7 +144,7 @@ protected function getSessionId()
/**
* Gets the test service context
* @return QtiRunnerServiceContext
* @throws \common_Exception
* @throws common_Exception
*/
protected function getServiceContext()
{
Expand All @@ -162,11 +163,18 @@ protected function getServiceContext()
$delivery = $execution->getDelivery();
$container = $this->getRuntimeService()->getDeliveryContainer($delivery->getUri());
if (!$container instanceof QtiTestDeliveryContainer) {
throw new common_Exception('Non QTI test container ' . get_class($container) . ' in qti test runner');
throw new common_Exception(
'Non QTI test container ' . get_class($container) . ' in qti test runner'
);
}
$testDefinition = $container->getSourceTest($execution);
$testCompilation = $container->getPrivateDirId($execution) . '|' . $container->getPublicDirId($execution);
$this->serviceContext = $this->getRunnerService()->getServiceContext($testDefinition, $testCompilation, $testExecution);

$this->serviceContext = $this->getRunnerService()->getServiceContext(
$testDefinition,
$testCompilation,
$testExecution
);
}

return $this->serviceContext;
Expand Down Expand Up @@ -234,7 +242,9 @@ protected function getErrorResponse($e = null, $prevResponse = [])
/** @var QtiRunnerMessageService $messageService */
$messageService = $this->getServiceManager()->get(QtiRunnerMessageService::SERVICE_ID);
try {
// phpcs:disable Generic.Files.LineLength
$response['message'] = __($messageService->getStateMessage($this->serviceContext->getTestSession()));
// phpcs:enable Generic.Files.LineLength
} catch (common_exception_Error $e) {
$response['message'] = null;
}
Expand Down Expand Up @@ -287,7 +297,6 @@ public function init()
$this->validateSecurityToken();

try {
/** @var QtiRunnerServiceContext $serviceContext */
$serviceContext = $this->getRunnerService()->initServiceContext($this->getServiceContext());
$this->returnJson($this->getInitResponse($serviceContext));
} catch (Exception $e) {
Expand Down Expand Up @@ -392,14 +401,18 @@ public function getItem()
$response['success'] = false;

$userIdentifier = common_session_SessionManager::getSession()->getUser()->getIdentifier();
common_Logger::e("Unable to retrieve item with identifier '${itemIdentifier}' for user '${userIdentifier}'.");
common_Logger::e(
"Unable to retrieve item with identifier '${itemIdentifier}' for user '${userIdentifier}'."
);
}

$this->getRunnerService()->startTimer($serviceContext);
} catch (common_Exception $e) {
$userIdentifier = common_session_SessionManager::getSession()->getUser()->getIdentifier();
$msg = __CLASS__ . "::getItem(): Unable to retrieve item with identifier '${itemIdentifier}' for user '${userIdentifier}'.\n";
$msg .= "Exception of type '" . get_class($e) . "' was thrown in '" . $e->getFile() . "' l." . $e->getLine() . " with message '" . $e->getMessage() . "'.";
$msg = __CLASS__ . "::getItem(): Unable to retrieve item with identifier '${itemIdentifier}' for "
. "user '${userIdentifier}'.\n";
$msg .= "Exception of type '" . get_class($e) . "' was thrown in '" . $e->getFile() . "' l." . $e->getLine()
. " with message '" . $e->getMessage() . "'.";

if ($e instanceof common_exception_Unauthorized) {
// Log as debug as not being authorized is not a "real" system error.
Expand Down Expand Up @@ -480,7 +493,7 @@ protected function getItemData($itemIdentifier)
* Save the actual item state.
* Requires params itemIdentifier and itemState
* @return boolean true if saved
* @throws \common_Exception
* @throws common_Exception
*/
protected function saveItemState()
{
Expand All @@ -497,7 +510,7 @@ protected function saveItemState()
* End the item timer and save the duration
* Requires params itemDuration and optionaly consumedExtraTime
* @return boolean true if saved
* @throws \common_Exception
* @throws common_Exception
*/
protected function endItemTimer()
{
Expand All @@ -514,7 +527,7 @@ protected function endItemTimer()
* Requires params itemDuration and optionally consumedExtraTime
* @param boolean $emptyAllowed if we allow empty responses
* @return boolean true if saved
* @throws \common_Exception
* @throws common_Exception
* @throws QtiRunnerEmptyResponsesException if responses are empty, emptyAllowed is false and no allowSkipping
*/
protected function saveItemResponses($emptyAllowed = true)
Expand All @@ -528,11 +541,17 @@ protected function saveItemResponses($emptyAllowed = true)
$itemResponse = $this->getItemResponse();

if ($serviceContext->getCurrentAssessmentItemRef()->getIdentifier() !== $itemIdentifier) {
throw new QtiRunnerItemResponseException(__('Item response identifier does not match current item'));
throw new QtiRunnerItemResponseException(
__('Item response identifier does not match current item')
);
}

if (!is_null($itemResponse) && !empty($itemDefinition)) {
$responses = $this->getRunnerService()->parsesItemResponse($serviceContext, $itemDefinition, $itemResponse);
$responses = $this->getRunnerService()->parsesItemResponse(
$serviceContext,
$itemDefinition,
$itemResponse
);

//still verify allowSkipping & empty responses
if (
Expand Down Expand Up @@ -564,7 +583,10 @@ public function submitItem()
// as we need to store the item state whatever the test state is
$this->validateSecurityToken();
$serviceContext = $this->getServiceContext();
$itemRef = $this->getRunnerService()->getItemHref($serviceContext, $this->getRequestParameter('itemDefinition'));
$itemRef = $this->getRunnerService()->getItemHref(
$serviceContext,
$this->getRequestParameter('itemDefinition')
);

if (!$this->getRunnerService()->isTerminated($serviceContext)) {
$this->endItemTimer();
Expand Down Expand Up @@ -597,6 +619,9 @@ public function submitItem()

/**
* Moves the current position to the provided scoped reference: item, section, part
*
* This action is called when the user sends a test response, but not when
* the test starts.
*/
public function move()
{
Expand All @@ -617,10 +642,10 @@ public function move()

$response = $moveService($moveCommand);

common_Logger::d('Test session state : ' . $this->getServiceContext()->getTestSession()->getState());

$this->returnJson($response->toArray());
} catch (common_Exception $e) {
$this->checkExceptionForTestSessionConflict($e);

$this->returnJson(
$this->getErrorResponse($e),
$this->getStatusCodeFromException($e)
Expand Down Expand Up @@ -652,6 +677,8 @@ public function skip()

$this->returnJson($response->toArray());
} catch (common_Exception $e) {
$this->checkExceptionForTestSessionConflict($e);

$this->returnJson(
$this->getErrorResponse($e),
$this->getStatusCodeFromException($e)
Expand Down Expand Up @@ -685,6 +712,8 @@ public function timeout()

$this->returnJson($response->toArray());
} catch (common_Exception $e) {
$this->checkExceptionForTestSessionConflict($e);

$this->returnJson(
$this->getErrorResponse($e),
$this->getStatusCodeFromException($e)
Expand Down Expand Up @@ -750,10 +779,7 @@ public function pause()

$this->setItemContextToCommand($command);

/** @var PauseService $pause */
$pause = $this->getPsrContainer()->get(PauseService::class);

$response = $pause($command);
$response = $this->getPauseService()($command);

$this->returnJson($response->toArray());
} catch (common_Exception $e) {
Expand Down Expand Up @@ -1081,6 +1107,11 @@ private function getRuntimeService(): RuntimeService
return $this->getServiceLocator()->get(RuntimeService::SERVICE_ID);
}

private function getPauseService(): PauseService
{
return $this->getPsrContainer()->get(PauseService::class);
}

private function getItemDuration(): ?float
{
if (!$this->hasRequestParameter('itemDuration')) {
Expand Down Expand Up @@ -1147,4 +1178,23 @@ private function createErrorResponseFromException(Exception $exception): void
$this->getStatusCodeFromException($exception)
);
}

private function checkExceptionForTestSessionConflict($exception): void
{
if (!$exception instanceof common_exception_Unauthorized) {
return;
}

try {
if ($this->getServiceContext()->getTestSession()->getState() === AssessmentTestSessionState::INTERACTING) {
$this->getConcurringSessionService()->setConcurringSession($this->getSessionId());
}
} catch (common_Exception $exception) {
}
}

private function getConcurringSessionService(): ConcurringSessionService
{
return $this->getPsrContainer()->get(ConcurringSessionService::class);
}
}
Loading

0 comments on commit 8e9c1bb

Please sign in to comment.