From ad8ed4ef62cc65d1630d6708358d992b6f3c2035 Mon Sep 17 00:00:00 2001 From: Aaron Bidzan Date: Thu, 9 Oct 2025 11:37:33 +0200 Subject: [PATCH 1/5] Adds auto open of exam modal if test password is wrong --- .../src/Presentation/class.TestScreenGUI.php | 154 ++++++++---------- 1 file changed, 69 insertions(+), 85 deletions(-) diff --git a/components/ILIAS/Test/src/Presentation/class.TestScreenGUI.php b/components/ILIAS/Test/src/Presentation/class.TestScreenGUI.php index c5033369bc96..19d21afb0baf 100755 --- a/components/ILIAS/Test/src/Presentation/class.TestScreenGUI.php +++ b/components/ILIAS/Test/src/Presentation/class.TestScreenGUI.php @@ -27,6 +27,7 @@ use ILIAS\Data\Link; use ILIAS\Data\Result; use ILIAS\Data\Password; +use ILIAS\UI\Implementation\Component\Launcher\Inline; use ILIAS\UI\Component\Launcher\Launcher; use ILIAS\UI\Component\Launcher\Factory as LauncherFactory; use ILIAS\UI\Component\MessageBox\MessageBox; @@ -315,6 +316,7 @@ function (Result $result) { && $request->getQueryParams()[$key] === 'exam_modal') { $launcher = $launcher->withRequest($request); } + return $launcher; } @@ -330,7 +332,15 @@ private function getModalLauncherInputs(): array $modal_inputs['exam_conditions'] = $this->ui_factory->input()->field()->checkbox( $this->lng->txt('tst_exam_conditions'), $this->lng->txt('tst_exam_conditions_label') - )->withRequired(true); + )->withRequired(true) + ->withAdditionalTransformation( + $this->refinery->custom()->constraint( + function (bool $value): bool { + return $value === true; + }, + fn($lng_closure, $value) => $this->lng->txt('tst_exam_conditions_not_checked_message'), + ) + ); } if ($this->main_settings->getAccessSettings()->getPasswordEnabled()) { @@ -340,10 +350,12 @@ private function getModalLauncherInputs(): array )->withRevelation(true) ->withRequired(true) ->withAdditionalTransformation( - $this->refinery->custom()->transformation( - static function (Password $value): string { - return $value->toString(); - } + $this->refinery->custom()->constraint( + function (Password $value): bool { + $access_settings_password = $this->main_settings->getAccessSettings()->getPassword(); + return $value->toString() === $access_settings_password; + }, + fn($lng_closure, $value) => $this->lng->txt('tst_exam_password_invalid_message'), ) ); } @@ -352,6 +364,13 @@ static function (Password $value): string { $access_code_input = $this->ui_factory->input()->field()->text( $this->lng->txt('tst_exam_access_code'), $this->lng->txt('tst_exam_access_code_label') + )->withAdditionalTransformation( + $this->refinery->custom()->constraint( + function (string $value): bool { + return !empty($value); + }, + fn($lng_closure, $value) => $this->lng->txt('tst_exam_access_code_required_message'), + ) ); $access_code_from_session = $this->test_session->getAccessCodeFromSession(); @@ -400,95 +419,60 @@ private function getStartLauncherLink(): Link private function evaluateLauncherModalForm(Result $result): void { - if ($result->isOK()) { - $conditions_met = true; - $message = ''; - $access_settings_password = $this->main_settings->getAccessSettings()->getPassword(); - $anonymous = $this->user->isAnonymous(); - foreach ($result->value() as $key => $value) { - - switch ($key) { - case 'exam_conditions': - $exam_conditions_value = (bool) $value; - if (!$exam_conditions_value) { - $conditions_met = false; - $message .= $this->lng->txt('tst_exam_conditions_not_checked_message') . '
'; - } - break; - case 'exam_password': - $password = $value; - $exam_password_valid = ($password === $access_settings_password); - if (!$exam_password_valid) { - $conditions_met = false; - $message .= $this->lng->txt('tst_exam_password_invalid_message') . '
'; - if ($this->object->getTestLogger()->isLoggingEnabled() - && !$this->object->getAnonymity()) { - $logger = $this->object->getTestLogger(); - $logger->logParticipantInteraction( - $logger->getInteractionFactory()->buildParticipantInteraction( - $this->ref_id, - null, - $this->user->getId(), - $_SERVER['REMOTE_ADDR'], - TestParticipantInteractionTypes::WRONG_TEST_PASSWORD_PROVIDED, - [] - ) - ); - } - } - $this->password_checker->setUserEnteredPassword($password); - break; - case 'exam_access_code': - if ($anonymous && !empty($value)) { - $this->test_session->setAccessCodeToSession($value); - } else { - $this->test_session->unsetAccessCodeInSession(); - } - break; - case 'exam_use_previous_answers': - $exam_use_previous_answers_value = (string) (int) $value; - break; - } - } + if ($result->isError()) { + $this->tpl->setOnScreenMessage( + \ilGlobalTemplateInterface::MESSAGE_TYPE_FAILURE, + $this->lng->txt('tst_exam_required_fields_not_filled_message'), + true + ); + return; + } - if ($message !== '') { - $this->tpl->setOnScreenMessage(\ilGlobalTemplateInterface::MESSAGE_TYPE_FAILURE, $message, true); + $anonymous = $this->user->isAnonymous(); + foreach ($result->value() as $key => $value) { + switch ($key) { + case 'exam_access_code': + if ($anonymous && !empty($value)) { + $this->test_session->setAccessCodeToSession($value); + } else { + $this->test_session->unsetAccessCodeInSession(); + } + break; + case 'exam_use_previous_answers': + $exam_use_previous_answers_value = (string) (int) $value; + break; } + } - if (empty($result->value())) { - $this->tpl->setOnScreenMessage( - \ilGlobalTemplateInterface::MESSAGE_TYPE_FAILURE, - $this->lng->txt('tst_exam_required_fields_not_filled_message'), - true - ); - } elseif ($conditions_met) { - if ( - !$anonymous && - $this->main_settings->getParticipantFunctionalitySettings()->getUsePreviousAnswerAllowed() - ) { - $this->user->setPref('tst_use_previous_answers', $exam_use_previous_answers_value ?? '0'); - $this->user->update(); - } - - if (isset($password) && $password === $access_settings_password) { - \ilSession::set('tst_password_' . $this->object->getTestId(), $password); - } else { - \ilSession::set('tst_password_' . $this->object->getTestId(), ''); - $this->test_session->setPasswordChecked(false); - } - - $this->ctrl->redirectByClass( - (new \ilTestPlayerFactory($this->object))->getPlayerGUI()::class, - \ilTestPlayerCommands::INIT_TEST - ); - } - } else { + if (empty($result->value())) { $this->tpl->setOnScreenMessage( \ilGlobalTemplateInterface::MESSAGE_TYPE_FAILURE, $this->lng->txt('tst_exam_required_fields_not_filled_message'), true ); + return; } + + if ( + !$anonymous && + $this->main_settings->getParticipantFunctionalitySettings()->getUsePreviousAnswerAllowed() + ) { + $this->user->setPref('tst_use_previous_answers', $exam_use_previous_answers_value ?? '0'); + $this->user->update(); + } + + $password = $result->value()['exam_password']->toString() ?? ''; + if ($password === $this->main_settings->getAccessSettings()->getPassword()) { + \ilSession::set('tst_password_' . $this->object->getTestId(), $password); + } else { + \ilSession::set('tst_password_' . $this->object->getTestId(), ''); + $this->test_session->setPasswordChecked(false); + } + + $this->ctrl->redirectByClass( + (new \ilTestPlayerFactory($this->object))->getPlayerGUI()::class, + \ilTestPlayerCommands::INIT_TEST + ); } private function testCanBeStarted(): bool From 4147170debc2a0a6e1635442c41235f07057b7f9 Mon Sep 17 00:00:00 2001 From: Aaron Bidzan Date: Thu, 9 Oct 2025 12:02:54 +0200 Subject: [PATCH 2/5] Adds further code improvements --- .../src/Presentation/class.TestScreenGUI.php | 48 ++++++++----------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/components/ILIAS/Test/src/Presentation/class.TestScreenGUI.php b/components/ILIAS/Test/src/Presentation/class.TestScreenGUI.php index 19d21afb0baf..e974cc4851e6 100755 --- a/components/ILIAS/Test/src/Presentation/class.TestScreenGUI.php +++ b/components/ILIAS/Test/src/Presentation/class.TestScreenGUI.php @@ -366,9 +366,7 @@ function (Password $value): bool { $this->lng->txt('tst_exam_access_code_label') )->withAdditionalTransformation( $this->refinery->custom()->constraint( - function (string $value): bool { - return !empty($value); - }, + fn(string $value): bool => !empty($value), fn($lng_closure, $value) => $this->lng->txt('tst_exam_access_code_required_message'), ) ); @@ -381,11 +379,17 @@ function (string $value): bool { $modal_inputs['exam_access_code'] = $access_code_input; } - if ($this->main_settings->getParticipantFunctionalitySettings()->getUsePreviousAnswerAllowed() - && $this->test_passes_selector->getLastFinishedPass() >= 0) { + if ( + $this->test_passes_selector->getLastFinishedPass() >= 0 + && $this->main_settings->getParticipantFunctionalitySettings()->getUsePreviousAnswerAllowed() + ) { $modal_inputs['exam_use_previous_answers'] = $this->ui_factory->input()->field()->checkbox( $this->lng->txt('tst_exam_use_previous_answers'), $this->lng->txt('tst_exam_use_previous_answers_label') + )->withAdditionalTransformation( + $this->refinery->custom()->transformation( + fn($value) => (string) (int) ($value ?? false) + ) ); } @@ -419,7 +423,7 @@ private function getStartLauncherLink(): Link private function evaluateLauncherModalForm(Result $result): void { - if ($result->isError()) { + if (empty($result->value()) || $result->isError()) { $this->tpl->setOnScreenMessage( \ilGlobalTemplateInterface::MESSAGE_TYPE_FAILURE, $this->lng->txt('tst_exam_required_fields_not_filled_message'), @@ -429,35 +433,23 @@ private function evaluateLauncherModalForm(Result $result): void } $anonymous = $this->user->isAnonymous(); - foreach ($result->value() as $key => $value) { - switch ($key) { - case 'exam_access_code': - if ($anonymous && !empty($value)) { - $this->test_session->setAccessCodeToSession($value); - } else { - $this->test_session->unsetAccessCodeInSession(); - } - break; - case 'exam_use_previous_answers': - $exam_use_previous_answers_value = (string) (int) $value; - break; + if (array_key_exists('exam_access_code', $result->value())) { + $value = $result->value()['exam_access_code']; + if ($anonymous && !empty($value)) { + $this->test_session->setAccessCodeToSession($value); + } else { + $this->test_session->unsetAccessCodeInSession(); } } - if (empty($result->value())) { - $this->tpl->setOnScreenMessage( - \ilGlobalTemplateInterface::MESSAGE_TYPE_FAILURE, - $this->lng->txt('tst_exam_required_fields_not_filled_message'), - true - ); - return; - } - if ( !$anonymous && $this->main_settings->getParticipantFunctionalitySettings()->getUsePreviousAnswerAllowed() ) { - $this->user->setPref('tst_use_previous_answers', $exam_use_previous_answers_value ?? '0'); + $this->user->setPref( + 'tst_use_previous_answers', + $result->value()['exam_use_previous_answers'] ?? '0' + ); $this->user->update(); } From 207c98d0b90f55049d51f0eb6d688a1f0dfde65d Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Tue, 28 Oct 2025 15:15:29 +0100 Subject: [PATCH 3/5] Adds improvements resulting from review --- .../Test/classes/class.ilTestSession.php | 12 ++++------- .../src/Presentation/class.TestScreenGUI.php | 21 +++++++------------ 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/components/ILIAS/Test/classes/class.ilTestSession.php b/components/ILIAS/Test/classes/class.ilTestSession.php index 813d083d24cb..138b22af9c1c 100755 --- a/components/ILIAS/Test/classes/class.ilTestSession.php +++ b/components/ILIAS/Test/classes/class.ilTestSession.php @@ -456,15 +456,11 @@ public function createNewAccessCode(): string public function isAccessCodeUsed(string $code): bool { - $query = "SELECT anonymous_id FROM tst_active WHERE test_fi = %s AND anonymous_id = %s"; - - $result = $this->db->queryF( - $query, - ['integer', 'text'], + return $this->db->queryF( + 'SELECT anonymous_id FROM tst_active WHERE test_fi = %s AND anonymous_id = %s' , + [ilDBConstants::T_INTEGER, ilDBConstants::T_TEXT], [$this->getTestId(), $code] - ); - - return ($result->numRows() > 0); + )->rowCount() > 0; } private function buildAccessCode(): string diff --git a/components/ILIAS/Test/src/Presentation/class.TestScreenGUI.php b/components/ILIAS/Test/src/Presentation/class.TestScreenGUI.php index e974cc4851e6..798c693c33a0 100755 --- a/components/ILIAS/Test/src/Presentation/class.TestScreenGUI.php +++ b/components/ILIAS/Test/src/Presentation/class.TestScreenGUI.php @@ -335,10 +335,8 @@ private function getModalLauncherInputs(): array )->withRequired(true) ->withAdditionalTransformation( $this->refinery->custom()->constraint( - function (bool $value): bool { - return $value === true; - }, - fn($lng_closure, $value) => $this->lng->txt('tst_exam_conditions_not_checked_message'), + static fn(bool $value): bool => $value, + $this->lng->txt('tst_exam_conditions_not_checked_message'), ) ); } @@ -351,11 +349,8 @@ function (bool $value): bool { ->withRequired(true) ->withAdditionalTransformation( $this->refinery->custom()->constraint( - function (Password $value): bool { - $access_settings_password = $this->main_settings->getAccessSettings()->getPassword(); - return $value->toString() === $access_settings_password; - }, - fn($lng_closure, $value) => $this->lng->txt('tst_exam_password_invalid_message'), + fn(Password $value): bool => $value->toString() === $this->main_settings->getAccessSettings()->getPassword(), + $this->lng->txt('tst_exam_password_invalid_message'), ) ); } @@ -366,8 +361,8 @@ function (Password $value): bool { $this->lng->txt('tst_exam_access_code_label') )->withAdditionalTransformation( $this->refinery->custom()->constraint( - fn(string $value): bool => !empty($value), - fn($lng_closure, $value) => $this->lng->txt('tst_exam_access_code_required_message'), + fn(string $value): bool => $value === '' || $this->test_session->isAccessCodeUsed($value), + $this->lng->txt('tst_exam_access_code_required_message'), ) ); @@ -388,7 +383,7 @@ function (Password $value): bool { $this->lng->txt('tst_exam_use_previous_answers_label') )->withAdditionalTransformation( $this->refinery->custom()->transformation( - fn($value) => (string) (int) ($value ?? false) + static fn(bool $value): string => $value ? '1' : '0' ) ); } @@ -423,7 +418,7 @@ private function getStartLauncherLink(): Link private function evaluateLauncherModalForm(Result $result): void { - if (empty($result->value()) || $result->isError()) { + if ($result->isError()) { $this->tpl->setOnScreenMessage( \ilGlobalTemplateInterface::MESSAGE_TYPE_FAILURE, $this->lng->txt('tst_exam_required_fields_not_filled_message'), From 2466cae5b46b396fcee4eda7117a219b420361c6 Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Tue, 28 Oct 2025 15:31:04 +0100 Subject: [PATCH 4/5] Fixes php-cs-fixer issue --- components/ILIAS/Test/classes/class.ilTestSession.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/ILIAS/Test/classes/class.ilTestSession.php b/components/ILIAS/Test/classes/class.ilTestSession.php index 138b22af9c1c..45b2e9a1a0c1 100755 --- a/components/ILIAS/Test/classes/class.ilTestSession.php +++ b/components/ILIAS/Test/classes/class.ilTestSession.php @@ -457,7 +457,7 @@ public function createNewAccessCode(): string public function isAccessCodeUsed(string $code): bool { return $this->db->queryF( - 'SELECT anonymous_id FROM tst_active WHERE test_fi = %s AND anonymous_id = %s' , + 'SELECT anonymous_id FROM tst_active WHERE test_fi = %s AND anonymous_id = %s', [ilDBConstants::T_INTEGER, ilDBConstants::T_TEXT], [$this->getTestId(), $code] )->rowCount() > 0; From 72a259fbc907dcbab40b23c8f03867c53bfbd67a Mon Sep 17 00:00:00 2001 From: Matheus Zych Date: Thu, 5 Feb 2026 09:57:04 +0100 Subject: [PATCH 5/5] Removes error message from background --- .../ILIAS/Test/src/Presentation/class.TestScreenGUI.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/components/ILIAS/Test/src/Presentation/class.TestScreenGUI.php b/components/ILIAS/Test/src/Presentation/class.TestScreenGUI.php index 798c693c33a0..9c00a1a76842 100755 --- a/components/ILIAS/Test/src/Presentation/class.TestScreenGUI.php +++ b/components/ILIAS/Test/src/Presentation/class.TestScreenGUI.php @@ -419,11 +419,6 @@ private function getStartLauncherLink(): Link private function evaluateLauncherModalForm(Result $result): void { if ($result->isError()) { - $this->tpl->setOnScreenMessage( - \ilGlobalTemplateInterface::MESSAGE_TYPE_FAILURE, - $this->lng->txt('tst_exam_required_fields_not_filled_message'), - true - ); return; }