From 99807354bb798c9583838557999a891f3ab0f89e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Wed, 4 Dec 2024 14:43:23 +0100 Subject: [PATCH 01/14] feat: validate responses against constraint --- actions/class.Runner.php | 2 + model/Container/TestQtiServiceProvider.php | 9 ++- .../QtiItemResponseRepository.php | 30 +++++++- .../QtiItemResponseValidator.php | 73 +++++++++++++++++++ .../QtiRunnerInvalidResponsesException.php | 48 ++++++++++++ .../classes/runner/time/QtiTimeConstraint.php | 4 +- 6 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 model/Infrastructure/QtiItemResponseValidator.php create mode 100644 models/classes/runner/QtiRunnerInvalidResponsesException.php diff --git a/actions/class.Runner.php b/actions/class.Runner.php index b29028c452..1a36e47181 100644 --- a/actions/class.Runner.php +++ b/actions/class.Runner.php @@ -47,6 +47,7 @@ use oat\taoQtiTest\model\Service\TimeoutService; use oat\taoQtiTest\model\Service\ToolsStateAwareInterface; use oat\taoQtiTest\models\cat\CatEngineNotFoundException; +use oat\taoQtiTest\models\classes\runner\QtiRunnerInvalidResponsesException; use oat\taoQtiTest\models\container\QtiTestDeliveryContainer; use oat\taoQtiTest\models\runner\communicator\CommunicationService; use oat\taoQtiTest\models\runner\communicator\QtiCommunicationService; @@ -274,6 +275,7 @@ protected function getStatusCodeFromException(Exception $exception): int case QtiRunnerEmptyResponsesException::class: case QtiRunnerClosedException::class: case QtiRunnerPausedException::class: + case QtiRunnerInvalidResponsesException::class; return 200; case common_exception_NotImplemented::class: diff --git a/model/Container/TestQtiServiceProvider.php b/model/Container/TestQtiServiceProvider.php index f1a482c501..64491af8b1 100644 --- a/model/Container/TestQtiServiceProvider.php +++ b/model/Container/TestQtiServiceProvider.php @@ -34,6 +34,7 @@ use oat\taoQtiTest\model\Domain\Model\QtiTestRepositoryInterface; use oat\taoQtiTest\model\Domain\Model\ToolsStateRepositoryInterface; use oat\taoQtiTest\model\Infrastructure\QtiItemResponseRepository; +use oat\taoQtiTest\model\Infrastructure\QtiItemResponseValidator; use oat\taoQtiTest\model\Infrastructure\QtiToolsStateRepository; use oat\taoQtiTest\model\Infrastructure\QtiTestRepository; use oat\taoQtiTest\model\Service\ConcurringSessionService; @@ -63,7 +64,9 @@ public function __invoke(ContainerConfigurator $configurator): void ->public() ->args( [ - service(QtiRunnerService::SERVICE_ID) + service(QtiRunnerService::SERVICE_ID), + service(FeatureFlagChecker::class), + service(QtiItemResponseValidator::class), ] ); @@ -176,5 +179,9 @@ public function __invoke(ContainerConfigurator $configurator): void service(TimerAdjustmentServiceInterface::SERVICE_ID), ] ); + + $services + ->set(QtiItemResponseValidator::class, QtiItemResponseValidator::class) + ->public(); } } diff --git a/model/Infrastructure/QtiItemResponseRepository.php b/model/Infrastructure/QtiItemResponseRepository.php index da359db323..19530764cd 100644 --- a/model/Infrastructure/QtiItemResponseRepository.php +++ b/model/Infrastructure/QtiItemResponseRepository.php @@ -24,22 +24,33 @@ namespace oat\taoQtiTest\model\Infrastructure; +use oat\tao\model\featureFlag\FeatureFlagChecker; use oat\taoQtiTest\model\Domain\Model\ItemResponse; use oat\taoQtiTest\model\Domain\Model\ItemResponseRepositoryInterface; +use oat\taoQtiTest\models\classes\runner\QtiRunnerInvalidResponsesException; use oat\taoQtiTest\models\runner\QtiRunnerEmptyResponsesException; use oat\taoQtiTest\models\runner\QtiRunnerItemResponseException; use oat\taoQtiTest\models\runner\QtiRunnerService; +use oat\taoQtiTest\models\runner\QtiRunnerServiceContext; use oat\taoQtiTest\models\runner\RunnerServiceContext; +use qtism\runtime\tests\AssessmentItemSessionException; use taoQtiTest_helpers_TestRunnerUtils as TestRunnerUtils; class QtiItemResponseRepository implements ItemResponseRepositoryInterface { /** @var QtiRunnerService */ private $runnerService; - - public function __construct(QtiRunnerService $runnerService) - { + private FeatureFlagChecker $featureFlagChecker; + private QtiItemResponseValidator $itemResponseValidator; + + public function __construct( + QtiRunnerService $runnerService, + FeatureFlagChecker $featureFlagChecker, + QtiItemResponseValidator $itemResponseValidator + ) { $this->runnerService = $runnerService; + $this->featureFlagChecker = $featureFlagChecker; + $this->itemResponseValidator = $itemResponseValidator; } public function save(ItemResponse $itemResponse, RunnerServiceContext $serviceContext): void @@ -110,6 +121,19 @@ private function saveItemResponses(ItemResponse $itemResponse, RunnerServiceCont $itemResponse->getResponse() ); + if ($this->featureFlagChecker->isEnabled('FEATURE_FLAG_RESPONSE_VALIDATOR')) { + try { + $this->itemResponseValidator->validate($serviceContext, $responses); + } catch (AssessmentItemSessionException $e) { + throw new QtiRunnerInvalidResponsesException($e->getMessage()); + } catch (QtiRunnerEmptyResponsesException $e) { + throw new QtiRunnerEmptyResponsesException($e->getMessage()); + } + + $this->runnerService->storeItemResponse($serviceContext, $itemDefinition, $responses); + return; + } + if ( $this->runnerService->getTestConfig()->getConfigValue('enableAllowSkipping') && !TestRunnerUtils::doesAllowSkipping($serviceContext->getTestSession()) diff --git a/model/Infrastructure/QtiItemResponseValidator.php b/model/Infrastructure/QtiItemResponseValidator.php new file mode 100644 index 0000000000..80b65a6e62 --- /dev/null +++ b/model/Infrastructure/QtiItemResponseValidator.php @@ -0,0 +1,73 @@ +getAllowSkip($serviceContext) && $responses->containsNullOnly()) { + return; + } + + if (!$this->getAllowSkip($serviceContext) && $responses->containsNullOnly()) { + throw new QtiRunnerEmptyResponsesException(); + } + + if ($this->getResponseValidation($serviceContext)) { + $serviceContext->getTestSession()->getCurrentAssessmentItemSession()->checkResponseValidityConstraints($responses); + } + + } + + private function getResponseValidation(RunnerServiceContext $serviceContext): bool + { + return $serviceContext->getTestSession() + ->getRoute() + ->current() + ->getItemSessionControl() + ->getItemSessionControl() + ->mustValidateResponses(); + } + + private function getAllowSkip(RunnerServiceContext $serviceContext): bool + { + return $serviceContext->getTestSession() + ->getRoute() + ->current() + ->getItemSessionControl() + ->getItemSessionControl() + ->doesAllowSkipping(); + } +} diff --git a/models/classes/runner/QtiRunnerInvalidResponsesException.php b/models/classes/runner/QtiRunnerInvalidResponsesException.php new file mode 100644 index 0000000000..d025681334 --- /dev/null +++ b/models/classes/runner/QtiRunnerInvalidResponsesException.php @@ -0,0 +1,48 @@ + + */ + +namespace oat\taoQtiTest\models\classes\runner; + +class QtiRunnerInvalidResponsesException extends \common_Exception implements \common_exception_UserReadableException +{ + /** + * Create a new QtiRunnerEmptyResponseException object. + * + * @param string $message the message + */ + public function __construct($message = 'A response to this item is invalid', $code = 200) + { + parent::__construct($message, $code); + } + + /** + * Returns a translated human-readable message destinated to the end-user. + * + * @return string A human-readable message. + */ + public function getUserMessage() + { + return __('A response to this item is invalid.'); + } +} diff --git a/models/classes/runner/time/QtiTimeConstraint.php b/models/classes/runner/time/QtiTimeConstraint.php index abf00371ea..48261123c3 100644 --- a/models/classes/runner/time/QtiTimeConstraint.php +++ b/models/classes/runner/time/QtiTimeConstraint.php @@ -231,7 +231,7 @@ private function durationToMs($duration) * Serialize the constraint the expected way by the TestContext and the TestMap * @return array */ - public function jsonSerialize() + public function jsonSerialize(): array { $source = $this->getSource(); $timeLimits = $source->getTimeLimits(); @@ -271,6 +271,6 @@ public function jsonSerialize() ]; } } - return null; + return []; } } From b417074c44e8cfb35e5246c1e04fb16d34770a37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Wed, 4 Dec 2024 15:21:30 +0100 Subject: [PATCH 02/14] feat: PHPCBF RESULT --- model/Infrastructure/QtiItemResponseRepository.php | 4 ++-- model/Infrastructure/QtiItemResponseValidator.php | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/model/Infrastructure/QtiItemResponseRepository.php b/model/Infrastructure/QtiItemResponseRepository.php index 19530764cd..ceb875f654 100644 --- a/model/Infrastructure/QtiItemResponseRepository.php +++ b/model/Infrastructure/QtiItemResponseRepository.php @@ -44,8 +44,8 @@ class QtiItemResponseRepository implements ItemResponseRepositoryInterface private QtiItemResponseValidator $itemResponseValidator; public function __construct( - QtiRunnerService $runnerService, - FeatureFlagChecker $featureFlagChecker, + QtiRunnerService $runnerService, + FeatureFlagChecker $featureFlagChecker, QtiItemResponseValidator $itemResponseValidator ) { $this->runnerService = $runnerService; diff --git a/model/Infrastructure/QtiItemResponseValidator.php b/model/Infrastructure/QtiItemResponseValidator.php index 80b65a6e62..3914afb48b 100644 --- a/model/Infrastructure/QtiItemResponseValidator.php +++ b/model/Infrastructure/QtiItemResponseValidator.php @@ -46,9 +46,10 @@ public function validate(RunnerServiceContext $serviceContext, State $responses) } if ($this->getResponseValidation($serviceContext)) { - $serviceContext->getTestSession()->getCurrentAssessmentItemSession()->checkResponseValidityConstraints($responses); + $serviceContext->getTestSession() + ->getCurrentAssessmentItemSession() + ->checkResponseValidityConstraints($responses); } - } private function getResponseValidation(RunnerServiceContext $serviceContext): bool From 01422e363925b731d26f985bbf15896c4205745d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Wed, 4 Dec 2024 16:25:13 +0100 Subject: [PATCH 03/14] feat: fix getStatusCodeFromException --- actions/class.Runner.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/class.Runner.php b/actions/class.Runner.php index 1a36e47181..dcc1d2b7f3 100644 --- a/actions/class.Runner.php +++ b/actions/class.Runner.php @@ -275,7 +275,7 @@ protected function getStatusCodeFromException(Exception $exception): int case QtiRunnerEmptyResponsesException::class: case QtiRunnerClosedException::class: case QtiRunnerPausedException::class: - case QtiRunnerInvalidResponsesException::class; + case QtiRunnerInvalidResponsesException::class: return 200; case common_exception_NotImplemented::class: From 6e725797cec5f3e52a812e33d9378d65163823da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Thu, 5 Dec 2024 10:06:22 +0100 Subject: [PATCH 04/14] feat: PluginManager Script --- model/Container/TestQtiServiceProvider.php | 12 +++ model/Service/PluginManagerService.php | 92 ++++++++++++++++++++++ scripts/tools/TestRunnerPluginManager.php | 85 ++++++++++++++++++++ 3 files changed, 189 insertions(+) create mode 100644 model/Service/PluginManagerService.php create mode 100644 scripts/tools/TestRunnerPluginManager.php diff --git a/model/Container/TestQtiServiceProvider.php b/model/Container/TestQtiServiceProvider.php index 64491af8b1..830a3d3c1b 100644 --- a/model/Container/TestQtiServiceProvider.php +++ b/model/Container/TestQtiServiceProvider.php @@ -42,6 +42,7 @@ use oat\taoQtiTest\model\Service\ListItemsService; use oat\taoQtiTest\model\Service\MoveService; use oat\taoQtiTest\model\Service\PauseService; +use oat\taoQtiTest\model\Service\PluginManagerService; use oat\taoQtiTest\model\Service\SkipService; use oat\taoQtiTest\model\Service\StoreTraceVariablesService; use oat\taoQtiTest\model\Service\TimeoutService; @@ -50,6 +51,7 @@ use oat\taoQtiTest\models\TestModelService; use oat\taoQtiTest\models\TestSessionService; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use common_ext_ExtensionsManager as ExtensionsManager; use function Symfony\Component\DependencyInjection\Loader\Configurator\service; @@ -183,5 +185,15 @@ public function __invoke(ContainerConfigurator $configurator): void $services ->set(QtiItemResponseValidator::class, QtiItemResponseValidator::class) ->public(); + + $services + ->set(PluginManagerService::class, PluginManagerService::class) + ->args( + [ + service(Ontology::SERVICE_ID), + service(ExtensionsManager::SERVICE_ID), + ] + ) + ->public(); } } diff --git a/model/Service/PluginManagerService.php b/model/Service/PluginManagerService.php new file mode 100644 index 0000000000..d0a46c66f1 --- /dev/null +++ b/model/Service/PluginManagerService.php @@ -0,0 +1,92 @@ + 'enable-allow-skipping', + 'validateResponses' => 'enable-validate-responses', + ]; + private Ontology $ontology; + private ExtensionsManager $extensionsManager; + private common_ext_Extension $extension; + private array $config; + + public function __construct(Ontology $ontology, ExtensionsManager $extensionsManager) + { + $this->ontology = $ontology; + $this->extensionsManager = $extensionsManager; + $this->extension = $this->extensionsManager->getExtensionById('taoQtiTest'); + $this->config = $this->extension->getConfig('testRunner') ?? []; + } + + /** + * @param string[] $disablePlugins + * @param Report $report + * @throws \common_exception_Error + */ + public function disablePlugin(array $disablePlugins, Report $report): void + { + foreach ($disablePlugins as $plugin) { + if (array_key_exists($plugin, self::PLUGIN_MAP)) { + $report->add(new Report(Report::TYPE_INFO, 'Plugin ' . $plugin . ' has been disabled')); + $this->config[self::PLUGIN_MAP[$plugin]] = false; + } + + if (array_key_exists($plugin, $this->config)) { + $report->add(new Report(Report::TYPE_INFO, 'Plugin ' . $plugin . ' has been disabled')); + $this->config[$plugin] = false; + } + } + $this->extension->setConfig('testRunner', $this->config); + } + + public function enablePlugin(array $enablePlugins, Report $report): void + { + $config = $this->getConfig(); + + foreach ($enablePlugins as $plugin) { + if (array_key_exists($plugin, self::PLUGIN_MAP)) { + $report->add(new Report(Report::TYPE_INFO, 'Plugin ' . $plugin . ' has been enabled')); + $config[self::PLUGIN_MAP[$plugin]] = true; + } + + if (array_key_exists($plugin, $config)) { + $report->add(new Report(Report::TYPE_INFO, 'Plugin ' . $plugin . ' has been disabled')); + $config[$plugin] = true; + } + } + $this->extension->setConfig('testRunner', $config); + } + + private function getConfig(): array + { + return $this->extensionsManager->getExtensionById('taoQtiTest')->getConfig('testRunner'); + } +} diff --git a/scripts/tools/TestRunnerPluginManager.php b/scripts/tools/TestRunnerPluginManager.php new file mode 100644 index 0000000000..a45199d72f --- /dev/null +++ b/scripts/tools/TestRunnerPluginManager.php @@ -0,0 +1,85 @@ + [ + 'prefix' => 'd', + 'longPrefix' => 'disable', + 'required' => false, + 'description' => 'List of plugins to be disabled, separated by comma', + ], + 'enablePlugins' => [ + 'prefix' => 'e', + 'longPrefix' => 'enable', + 'required' => false, + 'description' => 'List of plugins to be enabled, separated by comma', + ], + ]; + } + + protected function provideDescription() + { + return 'Manage test runner plugins'; + } + + protected function run() + { + if (empty($this->getOption('disablePlugins')) && empty($this->getOption('enablePlugins'))) { + return new Report(Report::TYPE_ERROR, 'No action provided'); + } + + $report = new Report(Report::TYPE_INFO, 'Plugins have been managed successfully'); + + $disablePlugins = $this->getOption('disablePlugins') + ? explode(',', $this->getOption('disablePlugins')) + : []; + + $enablePlugins = $this->getOption('enablePlugins') !== null + ? explode(',', $this->getOption('enablePlugins')) + : []; + + if (!empty($disablePlugins)) { + $this->getPluiginManagerService()->disablePlugin($disablePlugins, $report); + } + + if (!empty($enablePlugins)) { + $this->getPluiginManagerService()->enablePlugin($enablePlugins, $report); + } + + return $report; + } + + private function getPluiginManagerService(): PluginManagerService + { + return $this->getServiceManager()->getContainer()->get(PluginManagerService::class); + } +} From ae910345f1b55e8d4bf2c2f260dac96200222043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Thu, 5 Dec 2024 10:11:35 +0100 Subject: [PATCH 05/14] feat: QtiRunnerInvalidResponsesException --- .../QtiRunnerInvalidResponsesException.php | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/models/classes/runner/QtiRunnerInvalidResponsesException.php b/models/classes/runner/QtiRunnerInvalidResponsesException.php index d025681334..bf700df7af 100644 --- a/models/classes/runner/QtiRunnerInvalidResponsesException.php +++ b/models/classes/runner/QtiRunnerInvalidResponsesException.php @@ -15,32 +15,21 @@ * 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) 2017 (original work) Open Assessment Technologies SA ; - */ - -/** - * @author Bertrand Chevrier + * Copyright (c) 2024 (original work) Open Assessment Technologies SA ; */ namespace oat\taoQtiTest\models\classes\runner; -class QtiRunnerInvalidResponsesException extends \common_Exception implements \common_exception_UserReadableException +use common_Exception; +use common_exception_UserReadableException; + +class QtiRunnerInvalidResponsesException extends common_Exception implements common_exception_UserReadableException { - /** - * Create a new QtiRunnerEmptyResponseException object. - * - * @param string $message the message - */ public function __construct($message = 'A response to this item is invalid', $code = 200) { parent::__construct($message, $code); } - /** - * Returns a translated human-readable message destinated to the end-user. - * - * @return string A human-readable message. - */ public function getUserMessage() { return __('A response to this item is invalid.'); From bfed65a7dbab863539d977ed0fc3f6a8f57ee7fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Thu, 5 Dec 2024 11:14:11 +0100 Subject: [PATCH 06/14] feat: qti-sdk dev changes --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5af7d92a34..206477890b 100644 --- a/composer.json +++ b/composer.json @@ -62,7 +62,7 @@ "ext-zip": "*", "oat-sa/lib-test-cat": "2.4.0", "oat-sa/oatbox-extension-installer": "~1.1||dev-master", - "qtism/qtism": ">=0.28.3", + "qtism/qtism": "dev-fix/AUT-3994/ResponseValidityConstraint as 0.27.7", "oat-sa/generis": ">=16.0.0", "oat-sa/tao-core": ">=54.26.0", "oat-sa/extension-tao-item": ">=12.4.0", From 5392a3cd908c4f2ff34d7e9a17036f114b3adf80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Thu, 5 Dec 2024 11:29:59 +0100 Subject: [PATCH 07/14] feat: qti-sdk dev changes --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 206477890b..0337fdc748 100644 --- a/composer.json +++ b/composer.json @@ -62,7 +62,7 @@ "ext-zip": "*", "oat-sa/lib-test-cat": "2.4.0", "oat-sa/oatbox-extension-installer": "~1.1||dev-master", - "qtism/qtism": "dev-fix/AUT-3994/ResponseValidityConstraint as 0.27.7", + "qtism/qtism": "dev-fix/AUT-3994/ResponseValidityConstraint as 0.28.7", "oat-sa/generis": ">=16.0.0", "oat-sa/tao-core": ">=54.26.0", "oat-sa/extension-tao-item": ">=12.4.0", From a32519620022ac2e28263b8eb271a8391e0d9f0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Thu, 5 Dec 2024 11:33:39 +0100 Subject: [PATCH 08/14] feat: qti-sdk dev changes --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 0337fdc748..1002cf4449 100644 --- a/composer.json +++ b/composer.json @@ -62,7 +62,7 @@ "ext-zip": "*", "oat-sa/lib-test-cat": "2.4.0", "oat-sa/oatbox-extension-installer": "~1.1||dev-master", - "qtism/qtism": "dev-fix/AUT-3994/ResponseValidityConstraint as 0.28.7", + "qtism/qtism": "dev-fix/AUT-3994/ResponseValidityConstraint as 0.28.6", "oat-sa/generis": ">=16.0.0", "oat-sa/tao-core": ">=54.26.0", "oat-sa/extension-tao-item": ">=12.4.0", From 11096bdb85f17eaf0a10f0df135f7a1c462987aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Marsza=C5=82?= Date: Thu, 5 Dec 2024 14:39:05 +0100 Subject: [PATCH 09/14] feat: qti-sdk dev changes --- .../QtiItemResponseRepository.php | 5 +- .../QtiItemResponseValidator.php | 22 ++- model/Service/PluginManagerService.php | 2 +- .../QtiItemResponseRepositoryTest.php | 157 ++++++++++++++++++ .../QtiItemResponseValidatorTest.php | 145 ++++++++++++++++ 5 files changed, 314 insertions(+), 17 deletions(-) create mode 100644 test/unit/model/Infrastructure/QtiItemResponseRepositoryTest.php create mode 100644 test/unit/model/Infrastructure/QtiItemResponseValidatorTest.php diff --git a/model/Infrastructure/QtiItemResponseRepository.php b/model/Infrastructure/QtiItemResponseRepository.php index ceb875f654..5f5d4abd2b 100644 --- a/model/Infrastructure/QtiItemResponseRepository.php +++ b/model/Infrastructure/QtiItemResponseRepository.php @@ -31,7 +31,6 @@ use oat\taoQtiTest\models\runner\QtiRunnerEmptyResponsesException; use oat\taoQtiTest\models\runner\QtiRunnerItemResponseException; use oat\taoQtiTest\models\runner\QtiRunnerService; -use oat\taoQtiTest\models\runner\QtiRunnerServiceContext; use oat\taoQtiTest\models\runner\RunnerServiceContext; use qtism\runtime\tests\AssessmentItemSessionException; use taoQtiTest_helpers_TestRunnerUtils as TestRunnerUtils; @@ -123,11 +122,9 @@ private function saveItemResponses(ItemResponse $itemResponse, RunnerServiceCont if ($this->featureFlagChecker->isEnabled('FEATURE_FLAG_RESPONSE_VALIDATOR')) { try { - $this->itemResponseValidator->validate($serviceContext, $responses); + $this->itemResponseValidator->validate($serviceContext->getTestSession(), $responses); } catch (AssessmentItemSessionException $e) { throw new QtiRunnerInvalidResponsesException($e->getMessage()); - } catch (QtiRunnerEmptyResponsesException $e) { - throw new QtiRunnerEmptyResponsesException($e->getMessage()); } $this->runnerService->storeItemResponse($serviceContext, $itemDefinition, $responses); diff --git a/model/Infrastructure/QtiItemResponseValidator.php b/model/Infrastructure/QtiItemResponseValidator.php index 3914afb48b..3cfb136199 100644 --- a/model/Infrastructure/QtiItemResponseValidator.php +++ b/model/Infrastructure/QtiItemResponseValidator.php @@ -27,6 +27,7 @@ use oat\taoQtiTest\models\runner\RunnerServiceContext; use qtism\runtime\common\State; use qtism\runtime\tests\AssessmentItemSessionException; +use qtism\runtime\tests\AssessmentTestSession; class QtiItemResponseValidator { @@ -35,37 +36,34 @@ class QtiItemResponseValidator * @throws common_exception_Error * @throws QtiRunnerEmptyResponsesException */ - public function validate(RunnerServiceContext $serviceContext, State $responses): void + public function validate(AssessmentTestSession $testSession, State $responses): void { - if ($this->getAllowSkip($serviceContext) && $responses->containsNullOnly()) { + if ($this->getAllowSkip($testSession) && $responses->containsNullOnly()) { return; } - if (!$this->getAllowSkip($serviceContext) && $responses->containsNullOnly()) { + if (!$this->getAllowSkip($testSession) && $responses->containsNullOnly()) { throw new QtiRunnerEmptyResponsesException(); } - if ($this->getResponseValidation($serviceContext)) { - $serviceContext->getTestSession() - ->getCurrentAssessmentItemSession() + if ($this->getResponseValidation($testSession)) { + $testSession->getCurrentAssessmentItemSession() ->checkResponseValidityConstraints($responses); } } - private function getResponseValidation(RunnerServiceContext $serviceContext): bool + private function getResponseValidation(AssessmentTestSession $testSession): bool { - return $serviceContext->getTestSession() - ->getRoute() + return $testSession->getRoute() ->current() ->getItemSessionControl() ->getItemSessionControl() ->mustValidateResponses(); } - private function getAllowSkip(RunnerServiceContext $serviceContext): bool + private function getAllowSkip(AssessmentTestSession $testSession): bool { - return $serviceContext->getTestSession() - ->getRoute() + return $testSession->getRoute() ->current() ->getItemSessionControl() ->getItemSessionControl() diff --git a/model/Service/PluginManagerService.php b/model/Service/PluginManagerService.php index d0a46c66f1..555f4d0710 100644 --- a/model/Service/PluginManagerService.php +++ b/model/Service/PluginManagerService.php @@ -29,7 +29,7 @@ class PluginManagerService { - CONST PLUGIN_MAP = [ + private const PLUGIN_MAP = [ 'allowSkipping' => 'enable-allow-skipping', 'validateResponses' => 'enable-validate-responses', ]; diff --git a/test/unit/model/Infrastructure/QtiItemResponseRepositoryTest.php b/test/unit/model/Infrastructure/QtiItemResponseRepositoryTest.php new file mode 100644 index 0000000000..a40a9929b3 --- /dev/null +++ b/test/unit/model/Infrastructure/QtiItemResponseRepositoryTest.php @@ -0,0 +1,157 @@ +runnerServiceMock = $this->createMock(QtiRunnerService::class); + $this->featureFlagCheckerMock = $this->createMock(FeatureFlagChecker::class); + $this->itemResponseValidatorMock = $this->createMock(QtiItemResponseValidator::class); + + $this->subject = new QtiItemResponseRepository( + $this->runnerServiceMock, + $this->featureFlagCheckerMock, + $this->itemResponseValidatorMock + ); + } + + /** + * @dataProvider saveDataProvider + */ + public function testSave( + array $state, + array $response, + float $duration, + float $timestamp, + string $itemHref, + string $responseIdentifier, + int $storeItemResponseCount, + bool $shouldThrowException + ): void { + $itemResponse = new ItemResponse('itemIdentifier', + $state, + $response, + $duration, + $timestamp + ); + + $runnerServiceContextMock = $this->createMock(QtiRunnerServiceContext::class); + $extendedAssessmentItemRefMock = $this->createMock(ExtendedAssessmentItemRef::class); + $assessmentItemSession = $this->createMock(AssessmentItemSession::class); + $stateMock = $this->createMock(State::class); + + $extendedAssessmentItemRefMock->expects($this->once()) + ->method('getIdentifier') + ->willReturn($responseIdentifier); + + $this->runnerServiceMock->expects($this->once()) + ->method('isTerminated') + ->with($runnerServiceContextMock) + ->willReturn(false); + + $this->runnerServiceMock->expects($this->once()) + ->method('endTimer'); + + $this->runnerServiceMock->expects($this->once()) + ->method('getItemHref') + ->willReturn($itemHref); + + $this->runnerServiceMock->expects($this->once()) + ->method('setItemState'); + + $runnerServiceContextMock + ->method('getCurrentAssessmentItemRef') + ->willReturn($extendedAssessmentItemRefMock); + + $this->runnerServiceMock->expects($this->once()) + ->method('setItemState') + ->with($runnerServiceContextMock, $responseIdentifier, $state); + + $this->runnerServiceMock->expects($this->once()) + ->method('parsesItemResponse') + ->willReturn($stateMock); + + $this->featureFlagCheckerMock->expects($this->once()) + ->method('isEnabled') + ->willReturn(true); + + $this->itemResponseValidatorMock->expects($this->once()) + ->method('validate'); + + $this->runnerServiceMock->expects($this->exactly($storeItemResponseCount)) + ->method('storeItemResponse'); + + if ($shouldThrowException) { + $this->itemResponseValidatorMock->expects($this->once()) + ->method('validate') + ->willThrowException(new AssessmentItemSessionException('invalid', $assessmentItemSession, AssessmentItemSessionException::DURATION_OVERFLOW)); + $this->expectException(QtiRunnerInvalidResponsesException::class); + } + + $this->subject->save($itemResponse, $runnerServiceContextMock); + } + + public function saveDataProvider() + { + return [ + 'happyPath with feature flag enabled' => [ + 'state' => ['state'], + 'response' => ['response'], + 'duration' => 1.0, + 'timestamp' => 2.0, + 'itemHref' => 'itemHref', + 'responseIdentifier' => 'itemIdentifier', + 'storeItemResponseCount' => 1, + 'shouldThrowException' => false + ], + 'validation throw an error, flag enabled' => [ + 'state' => ['state'], + 'response' => ['response'], + 'duration' => 1.0, + 'timestamp' => 2.0, + 'itemHref' => 'itemHref', + 'responseIdentifier' => 'itemIdentifier', + 'storeItemResponseCount' => 0, + 'shouldThrowException' => true + ] + ]; + } + +} diff --git a/test/unit/model/Infrastructure/QtiItemResponseValidatorTest.php b/test/unit/model/Infrastructure/QtiItemResponseValidatorTest.php new file mode 100644 index 0000000000..8b946ca178 --- /dev/null +++ b/test/unit/model/Infrastructure/QtiItemResponseValidatorTest.php @@ -0,0 +1,145 @@ +subject = new QtiItemResponseValidator(); + $this->routeMock = $this->createMock(Route::class); + $this->routeItem = $this->createMock(RouteItem::class); + $this->routeItemSessionControl = $this->createMock(RouteItemSessionControl::class); + $this->itemSessionControl = $this->createMock(ItemSessionControl::class); + + $this->routeMock + ->method('current') + ->willReturn($this->routeItem); + + $this->routeItem + ->method('getItemSessionControl') + ->willReturn($this->routeItemSessionControl); + + $this->routeItemSessionControl + ->method('getItemSessionControl') + ->willReturn($this->itemSessionControl); + + + } + + /** + * @throws \common_exception_Error + * @throws \oat\taoQtiTest\models\runner\QtiRunnerEmptyResponsesException + * @throws \qtism\runtime\tests\AssessmentItemSessionException + */ + public function testValidateAllowedToSkip() + { + $assessmentTestSession = $this->createMock(AssessmentTestSession::class); + $responses = $this->createMock(State::class); + + $assessmentTestSession + ->method('getRoute') + ->willReturn($this->routeMock); + + $this->itemSessionControl->expects($this->never()) + ->method('mustValidateResponses'); + + $this->itemSessionControl->expects($this->once()) + ->method('doesAllowSkipping') + ->willReturn(true); + + $responses + ->method('containsNullOnly') + ->willReturn(true); + + $this->subject->validate($assessmentTestSession, $responses); + } + + public function testValidateNotAllowedToSkip(): void + { + $assessmentTestSession = $this->createMock(AssessmentTestSession::class); + $responses = $this->createMock(State::class); + + $assessmentTestSession + ->method('getRoute') + ->willReturn($this->routeMock); + + $this->itemSessionControl->expects($this->never()) + ->method('mustValidateResponses'); + + $this->itemSessionControl->expects($this->exactly(2)) + ->method('doesAllowSkipping') + ->willReturn(false); + + $responses + ->method('containsNullOnly') + ->willReturn(true); + + $this->expectException(QtiRunnerEmptyResponsesException::class); + + $this->subject->validate($assessmentTestSession, $responses); + } + + public function testValidateNotAllowedToSkipValidateResponses(): void + { + $assessmentTestSession = $this->createMock(AssessmentTestSession::class); + $responses = $this->createMock(State::class); + $assessmentItemSession = $this->createMock(AssessmentItemSession::class); + + $assessmentTestSession + ->method('getRoute') + ->willReturn($this->routeMock); + + $this->itemSessionControl->expects($this->once()) + ->method('mustValidateResponses') + ->willReturn(true); + + $this->itemSessionControl->expects($this->exactly(2)) + ->method('doesAllowSkipping') + ->willReturn(false); + + $responses + ->method('containsNullOnly') + ->willReturn(false); + + $assessmentTestSession->expects($this->once()) + ->method('getCurrentAssessmentItemSession') + ->willReturn($assessmentItemSession); + + $assessmentItemSession->expects($this->once()) + ->method('checkResponseValidityConstraints'); + + $this->subject->validate($assessmentTestSession, $responses); + } +} From b8cc98a75505c302a1caa9daf1233b7d272808dd Mon Sep 17 00:00:00 2001 From: Aleksej Tikhanovich Date: Mon, 16 Dec 2024 18:45:11 +0100 Subject: [PATCH 10/14] fix: update text for validate responses --- .../creator/templates/itemref-props.tpl | 6 ++-- .../creator/templates/section-props.tpl | 32 +++++++++---------- .../creator/templates/testpart-props.tpl | 6 ++-- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/views/js/controller/creator/templates/itemref-props.tpl b/views/js/controller/creator/templates/itemref-props.tpl index 24d4906426..ed56c37c5c 100644 --- a/views/js/controller/creator/templates/itemref-props.tpl +++ b/views/js/controller/creator/templates/itemref-props.tpl @@ -266,8 +266,8 @@ {{#if validateResponsesVisible}}
-
- +
+
diff --git a/views/js/controller/creator/templates/section-props.tpl b/views/js/controller/creator/templates/section-props.tpl index f33983caa7..b6ab2c2d2f 100644 --- a/views/js/controller/creator/templates/section-props.tpl +++ b/views/js/controller/creator/templates/section-props.tpl @@ -277,10 +277,10 @@
-
+
-
+
{{#if itemSessionShowFeedback}}
-
+
-
+