ILIAS  trunk Revision v11.0_alpha-2638-g80c1d007f79
class.TestScreenGUI.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
22 
27 use ILIAS\Data\Link;
31 use ILIAS\UI\Component\Launcher\Factory as LauncherFactory;
38 use ILIAS\Style\Content\Service as ContentStyle;
39 
46 {
47  public const DEFAULT_CMD = 'testScreen';
48 
49  private readonly \ilTestPassesSelector $test_passes_selector;
50  private readonly int $ref_id;
51  private readonly MainSettings $main_settings;
52  private readonly \ilTestSession $test_session;
53  private readonly DataFactory $data_factory;
54  private \ilTestPasswordChecker $password_checker;
55 
56  public function __construct(
57  private readonly \ilObjTest $object,
58  private readonly \ilObjUser $user,
59  private readonly UIFactory $ui_factory,
60  private readonly UIRenderer $ui_renderer,
61  private readonly \ilLanguage $lng,
62  private readonly Refinery $refinery,
63  private readonly \ilCtrlInterface $ctrl,
64  private readonly \ilGlobalTemplateInterface $tpl,
65  private readonly ContentStyle $content_style,
66  private readonly HTTPServices $http,
67  private readonly TabsManager $tabs_manager,
68  private readonly \ilAccessHandler $access,
69  private readonly \ilTestAccess $test_access,
70  private readonly \ilDBInterface $database,
71  private readonly \ilRbacSystem $rbac_system
72  ) {
73  $this->ref_id = $this->object->getRefId();
74  $this->main_settings = $this->object->getMainSettings();
75  $this->data_factory = new DataFactory();
76 
77  $this->test_session = (new \ilTestSessionFactory($this->object, $this->database, $this->user))->getSession();
78 
79  $this->test_passes_selector = new \ilTestPassesSelector($this->database, $this->object);
80  $this->test_passes_selector->setActiveId($this->test_session->getActiveId());
81  $this->test_passes_selector->setLastFinishedPass($this->test_session->getLastFinishedPass());
82  $this->password_checker = new \ilTestPasswordChecker($this->rbac_system, $this->user, $this->object, $this->lng);
83  }
84 
85  public function executeCommand(): void
86  {
87  if ($this->access->checkAccess('read', '', $this->ref_id)) {
88  $this->{$this->ctrl->getCmd(self::DEFAULT_CMD)}();
89  return;
90  }
91 
92  $this->tabs_manager->activateTab(TabsManager::TAB_ID_TEST);
93 
94  if (!$this->object->getMainSettings()->getAdditionalSettings()->getHideInfoTab()) {
95  $this->ctrl->redirectByClass([\ilRepositoryGUI::class, \ilObjTestGUI::class, \ilInfoScreenGUI::class]);
96  }
97 
98  $this->tpl->setOnScreenMessage('failure', sprintf(
99  $this->lng->txt('msg_no_perm_read_item'),
100  $this->object->getTitle()
101  ), true);
102  $this->ctrl->setParameterByClass(\ilRepositoryGUI::class, 'ref_id', ROOT_FOLDER_ID);
103  $this->ctrl->redirectByClass(\ilRepositoryGUI::class);
104  }
105 
106  public function testScreen(): void
107  {
108  $this->tabs_manager->activateTab(TabsManager::TAB_ID_TEST);
109  $this->tpl->setPermanentLink($this->object->getType(), $this->ref_id);
110 
111  $elements = [];
112 
113  if ($this->areSkillLevelThresholdsMissing()) {
114  $elements = [$this->getSkillLevelThresholdsMissingInfo()];
115  }
116  $elements = $this->handleRenderMessageBox($elements);
117  $elements = $this->handleRenderIntroduction($elements);
118 
119  $this->tpl->setContent(
120  $this->ui_renderer->render(
121  $this->testCanBeStarted() ? $this->handleRenderLauncher($elements) : $elements
122  )
123  );
124  }
125 
126  private function handleRenderMessageBox(array $elements): array
127  {
128  $message_box_message = '';
129  $message_box_message_elements = [];
130 
131  $exam_conditions_enabled = $this->main_settings->getIntroductionSettings()->getExamConditionsCheckboxEnabled();
132  $password_enabled = $this->main_settings->getAccessSettings()->getPasswordEnabled();
133  $test_behaviour_settings = $this->main_settings->getTestBehaviourSettings();
134 
135  if ($exam_conditions_enabled && $password_enabled) {
136  $message_box_message_elements[] = $this->lng->txt('tst_launcher_status_message_conditions_and_password');
137  } elseif ($exam_conditions_enabled) {
138  $message_box_message_elements[] = $this->lng->txt('tst_launcher_status_message_conditions');
139  } elseif ($password_enabled) {
140  $message_box_message_elements[] = $this->lng->txt('tst_launcher_status_message_password');
141  }
142 
143  if ($test_behaviour_settings->getProcessingTimeEnabled() && !$this->isUserOutOfProcessingTime()) {
144  $message_box_message_elements[] = sprintf(
145  $this->lng->txt('tst_time_limit_message'),
146  $test_behaviour_settings->getProcessingTimeAsMinutes()
147  );
148  }
149 
150  $nr_of_tries = $this->object->getNrOfTries();
151 
152  if ($nr_of_tries !== 0) {
153  $message_box_message_elements[] = sprintf($this->lng->txt('tst_attempt_limit_message'), $nr_of_tries);
154  }
155 
156  if ($this->object->isStartingTimeEnabled() && !$this->object->startingTimeReached()) {
157  $message_box_message_elements[] = sprintf(
158  $this->lng->txt('detail_starting_time_not_reached'),
159  \ilDatePresentation::formatDate(new \ilDateTime($this->object->getStartingTime(), IL_CAL_UNIX))
160  );
161  }
162 
163  if ($this->object->isEndingTimeEnabled() && !$this->object->endingTimeReached()) {
164  $message_box_message_elements[] = sprintf(
165  $this->lng->txt('tst_exam_ending_time_message'),
166  \ilDatePresentation::formatDate(new \ilDateTime($this->object->getEndingTime(), IL_CAL_UNIX))
167  );
168  }
169 
170  foreach ($message_box_message_elements as $message_box_message_element) {
171  $message_box_message .= ' ' . $message_box_message_element;
172  }
173 
174  if (!empty($message_box_message)) {
175  $elements[] = $this->ui_factory->messageBox()->info($message_box_message);
176  }
177 
178  return $elements;
179  }
180 
181  private function handleRenderIntroduction(array $elements): array
182  {
183  $introduction = $this->object->getIntroduction();
184 
185  if (
186  $this->main_settings->getIntroductionSettings()->getIntroductionEnabled() &&
187  !empty($introduction)
188  ) {
189  $this->content_style->gui()->addCss($this->tpl, $this->ref_id);
190  $elements[] = $this->ui_factory->panel()->standard(
191  $this->lng->txt('tst_introduction'),
192  $this->ui_factory->legacy()->content($introduction),
193  );
194  }
195 
196  return $elements;
197  }
198 
199  private function handleRenderLauncher(array $elements): array
200  {
201  $elements[] = $this->getLauncher();
202  return $elements;
203  }
204 
205  private function getLauncher(): Launcher
206  {
207  $launcher_factory = $this->ui_factory->launcher();
208 
209  if ($this->object->isStartingTimeEnabled() && !$this->object->startingTimeReached()) {
210  return $launcher_factory
211  ->inline($this->data_factory->link('', $this->data_factory->uri($this->http->request()->getUri()->__toString())))
212  ->withButtonLabel(sprintf(
213  $this->lng->txt('detail_starting_time_not_reached'),
214  \ilDatePresentation::formatDate(new \ilDateTime($this->object->getStartingTime(), IL_CAL_UNIX))
215  ), false)
216  ;
217  }
218 
219  if ($this->object->isEndingTimeEnabled() && $this->object->endingTimeReached()) {
220  return $launcher_factory
221  ->inline($this->data_factory->link('', $this->data_factory->uri($this->http->request()->getUri()->__toString())))
222  ->withButtonLabel(sprintf(
223  $this->lng->txt('detail_ending_time_reached'),
224  \ilDatePresentation::formatDate(new \ilDateTime($this->object->getEndingTime(), IL_CAL_UNIX))
225  ), false)
226  ;
227  }
228 
229  if ($this->isUserOutOfProcessingTime()) {
230  return $launcher_factory
231  ->inline($this->data_factory->link('', $this->data_factory->uri($this->http->request()->getUri()->__toString())))
232  ->withButtonLabel($this->lng->txt('tst_out_of_time_message'), false)
233  ;
234  }
235 
236  $participant_access = $this->test_access->isParticipantAllowed(
237  $this->object->getId(),
238  $this->user->getId()
239  );
240 
241  if ($participant_access === ParticipantAccess::NOT_INVITED) {
242  return $launcher_factory
243  ->inline($this->data_factory->link('', $this->data_factory->uri($this->http->request()->getUri()->__toString())))
244  ->withButtonLabel($this->lng->txt('tst_exam_not_assigned_participant_disclaimer'), false)
245  ;
246  }
247 
248  if ($participant_access !== ParticipantAccess::ALLOWED) {
249  return $launcher_factory
250  ->inline($this->data_factory->link('', $this->data_factory->uri($this->http->request()->getUri()->__toString())))
251  ->withButtonLabel($participant_access->getAccessForbiddenMessage($this->lng), false)
252  ;
253  }
254 
255  if (!$this->hasAvailablePasses()) {
256  return $launcher_factory
257  ->inline($this->data_factory->link('', $this->data_factory->uri($this->http->request()->getUri()->__toString())))
258  ->withButtonLabel($this->lng->txt('tst_launcher_button_label_passes_limit_reached'), false);
259  }
260 
261  if ($this->blockUserAfterHavingPassed()) {
262  return $launcher_factory
263  ->inline($this->data_factory->link('', $this->data_factory->uri($this->http->request()->getUri()->__toString())))
264  ->withButtonLabel($this->lng->txt('tst_already_passed_cannot_retake'), false)
265  ;
266  }
267 
268  $next_pass_allowed_timestamp = 0;
269  if (!$this->object->isNextPassAllowed($this->test_passes_selector, $next_pass_allowed_timestamp)) {
270  return $launcher_factory
271  ->inline($this->data_factory->link('', $this->data_factory->uri($this->http->request()->getUri()->__toString())))
272  ->withButtonLabel(
273  sprintf(
274  $this->lng->txt('wait_for_next_pass_hint_msg'),
275  \ilDatePresentation::formatDate(new \ilDateTime($next_pass_allowed_timestamp, IL_CAL_UNIX)),
276  ),
277  false
278  )
279  ;
280  }
281 
282  if ($this->lastPassSuspended()) {
283  return $launcher_factory->inline($this->getResumeLauncherLink());
284  }
285 
286  if ($this->isModalLauncherNeeded()) {
287  return $this->buildModalLauncher();
288  }
289  return $launcher_factory->inline($this->getStartLauncherLink());
290  }
291 
292  private function getResumeLauncherLink(): Link
293  {
294  $url = $this->ctrl->getLinkTarget(
295  (new \ilTestPlayerFactory($this->object))->getPlayerGUI(),
297  );
298  return $this->data_factory->link($this->lng->txt('tst_resume_test'), $this->data_factory->uri(ILIAS_HTTP_PATH . '/' . $url));
299  }
300 
301  private function buildModalLauncher(): Launcher
302  {
303  $launcher = $this->ui_factory->launcher()->inline($this->getModalLauncherLink())
304  ->withInputs(
305  $this->ui_factory->input()->field()->group($this->getModalLauncherInputs()),
306  function (Result $result) {
307  $this->evaluateLauncherModalForm($result);
308  },
310  )->withModalSubmitLabel($this->lng->txt('continue'));
311 
312  $request = $this->http->request();
313  $key = 'launcher_id';
314  if (array_key_exists($key, $request->getQueryParams())
315  && $request->getQueryParams()[$key] === 'exam_modal') {
316  $launcher = $launcher->withRequest($request);
317  }
318  return $launcher;
319  }
320 
321  private function getModalLauncherLink(): Link
322  {
323  $uri = $this->data_factory->uri($this->http->request()->getUri()->__toString())->withParameter('launcher_id', 'exam_modal');
324  return $this->data_factory->link($this->lng->txt('tst_exam_start'), $uri);
325  }
326 
327  private function getModalLauncherInputs(): array
328  {
329  if ($this->main_settings->getIntroductionSettings()->getExamConditionsCheckboxEnabled()) {
330  $modal_inputs['exam_conditions'] = $this->ui_factory->input()->field()->checkbox(
331  $this->lng->txt('tst_exam_conditions'),
332  $this->lng->txt('tst_exam_conditions_label')
333  )->withRequired(true);
334  }
335 
336  if ($this->main_settings->getAccessSettings()->getPasswordEnabled()) {
337  $modal_inputs['exam_password'] = $this->ui_factory->input()->field()->password(
338  $this->lng->txt('tst_exam_password'),
339  $this->lng->txt('tst_exam_password_label')
340  )->withRevelation(true)
341  ->withRequired(true)
342  ->withAdditionalTransformation(
343  $this->refinery->custom()->transformation(
344  static function (Password $value): string {
345  return $value->toString();
346  }
347  )
348  );
349  }
350 
351  if ($this->user->isAnonymous()) {
352  $access_code_input = $this->ui_factory->input()->field()->text(
353  $this->lng->txt('tst_exam_access_code'),
354  $this->lng->txt('tst_exam_access_code_label')
355  );
356 
357  $access_code_from_session = $this->test_session->getAccessCodeFromSession();
358  if ($access_code_from_session) {
359  $access_code_input = $access_code_input->withValue($access_code_from_session);
360  }
361 
362  $modal_inputs['exam_access_code'] = $access_code_input;
363  }
364 
365  if ($this->main_settings->getParticipantFunctionalitySettings()->getUsePreviousAnswerAllowed()
366  && $this->test_passes_selector->getLastFinishedPass() >= 0) {
367  $modal_inputs['exam_use_previous_answers'] = $this->ui_factory->input()->field()->checkbox(
368  $this->lng->txt('tst_exam_use_previous_answers'),
369  $this->lng->txt('tst_exam_use_previous_answers_label')
370  );
371  }
372 
373  return $modal_inputs ?? [];
374  }
375 
377  {
378  $exam_conditions_enabled = $this->main_settings->getIntroductionSettings()->getExamConditionsCheckboxEnabled();
379  $password_enabled = $this->main_settings->getAccessSettings()->getPasswordEnabled();
380 
381  if ($exam_conditions_enabled && $password_enabled) {
382  $modal_message_box_message = $this->lng->txt('tst_exam_modal_message_conditions_and_password');
383  } elseif ($exam_conditions_enabled) {
384  $modal_message_box_message = $this->lng->txt('tst_exam_modal_message_conditions');
385  } elseif ($password_enabled) {
386  $modal_message_box_message = $this->lng->txt('tst_exam_modal_message_password');
387  }
388 
389  return isset($modal_message_box_message) ? $this->ui_factory->messageBox()->info($modal_message_box_message) : null;
390  }
391 
392  private function getStartLauncherLink(): Link
393  {
394  $url = $this->ctrl->getLinkTarget(
395  (new \ilTestPlayerFactory($this->object))->getPlayerGUI(),
397  );
398  return $this->data_factory->link($this->lng->txt('tst_exam_start'), $this->data_factory->uri(ILIAS_HTTP_PATH . '/' . $url));
399  }
400 
401  private function evaluateLauncherModalForm(Result $result): void
402  {
403  if ($result->isOK()) {
404  $conditions_met = true;
405  $message = '';
406  $access_settings_password = $this->main_settings->getAccessSettings()->getPassword();
407  $anonymous = $this->user->isAnonymous();
408  foreach ($result->value() as $key => $value) {
409 
410  switch ($key) {
411  case 'exam_conditions':
412  $exam_conditions_value = (bool) $value;
413  if (!$exam_conditions_value) {
414  $conditions_met = false;
415  $message .= $this->lng->txt('tst_exam_conditions_not_checked_message') . '<br>';
416  }
417  break;
418  case 'exam_password':
419  $password = $value;
420  $exam_password_valid = ($password === $access_settings_password);
421  if (!$exam_password_valid) {
422  $conditions_met = false;
423  $message .= $this->lng->txt('tst_exam_password_invalid_message') . '<br>';
424  if ($this->object->getTestLogger()->isLoggingEnabled()
425  && !$this->object->getAnonymity()) {
426  $logger = $this->object->getTestLogger();
427  $logger->logParticipantInteraction(
428  $logger->getInteractionFactory()->buildParticipantInteraction(
429  $this->ref_id,
430  null,
431  $this->user->getId(),
432  $_SERVER['REMOTE_ADDR'],
433  TestParticipantInteractionTypes::WRONG_TEST_PASSWORD_PROVIDED,
434  []
435  )
436  );
437  }
438  }
439  $this->password_checker->setUserEnteredPassword($password);
440  break;
441  case 'exam_access_code':
442  if ($anonymous && !empty($value)) {
443  $this->test_session->setAccessCodeToSession($value);
444  } else {
445  $this->test_session->unsetAccessCodeInSession();
446  }
447  break;
448  case 'exam_use_previous_answers':
449  $exam_use_previous_answers_value = (string) (int) $value;
450  break;
451  }
452  }
453 
454  if ($message !== '') {
455  $this->tpl->setOnScreenMessage(\ilGlobalTemplateInterface::MESSAGE_TYPE_FAILURE, $message, true);
456  }
457 
458  if (empty($result->value())) {
459  $this->tpl->setOnScreenMessage(
461  $this->lng->txt('tst_exam_required_fields_not_filled_message'),
462  true
463  );
464  } elseif ($conditions_met) {
465  if (
466  !$anonymous &&
467  $this->main_settings->getParticipantFunctionalitySettings()->getUsePreviousAnswerAllowed()
468  ) {
469  $this->user->setPref('tst_use_previous_answers', $exam_use_previous_answers_value ?? '0');
470  $this->user->update();
471  }
472 
473  if (isset($password) && $password === $access_settings_password) {
474  \ilSession::set('tst_password_' . $this->object->getTestId(), $password);
475  } else {
476  \ilSession::set('tst_password_' . $this->object->getTestId(), '');
477  $this->test_session->setPasswordChecked(false);
478  }
479 
480  $this->ctrl->redirectByClass(
481  (new \ilTestPlayerFactory($this->object))->getPlayerGUI()::class,
483  );
484  }
485  } else {
486  $this->tpl->setOnScreenMessage(
488  $this->lng->txt('tst_exam_required_fields_not_filled_message'),
489  true
490  );
491  }
492  }
493 
494  private function testCanBeStarted(): bool
495  {
496  if ($this->object->getOfflineStatus()
497  || !$this->object->isComplete($this->object->getQuestionSetConfig())) {
498  return false;
499  }
500 
501  return true;
502  }
503 
504  private function isUserOutOfProcessingTime(): bool
505  {
506  $test_behaviour_settings = $this->object->getMainSettings()->getTestBehaviourSettings();
507  if (!$test_behaviour_settings->getProcessingTimeEnabled()
508  || $test_behaviour_settings->getResetProcessingTime()) {
509  return false;
510  }
511 
512  $active_id = $this->test_passes_selector->getActiveId();
513  $last_started_pass = $this->test_session->getLastStartedPass();
514  return $last_started_pass !== null
515  && $this->object->isMaxProcessingTimeReached(
516  $this->object->getStartingTimeOfUser($active_id, $last_started_pass),
517  $active_id
518  );
519  }
520 
521  private function blockUserAfterHavingPassed(): bool
522  {
523  if ($this->main_settings->getTestBehaviourSettings()->getBlockAfterPassedEnabled()) {
524  return $this->test_passes_selector->getLastFinishedPass() >= 0
525  && $this->test_passes_selector->hasTestPassedOnce($this->test_session->getActiveId());
526  }
527 
528  return false;
529  }
530 
531  private function hasAvailablePasses(): bool
532  {
533  $nr_of_tries = $this->object->getNrOfTries();
534 
535  return $nr_of_tries === 0 || (count($this->test_passes_selector->getExistingPasses()) <= $nr_of_tries && count($this->test_passes_selector->getClosedPasses()) < $nr_of_tries);
536  }
537 
538  private function lastPassSuspended(): bool
539  {
540  return (count($this->test_passes_selector->getExistingPasses()) - count($this->test_passes_selector->getClosedPasses())) === 1;
541  }
542 
543  private function isModalLauncherNeeded(): bool
544  {
545  return (
546  $this->main_settings->getIntroductionSettings()->getExamConditionsCheckboxEnabled()
547  || $this->main_settings->getAccessSettings()->getPasswordEnabled()
548  || $this->main_settings->getParticipantFunctionalitySettings()->getUsePreviousAnswerAllowed()
549  && $this->test_passes_selector->getLastFinishedPass() >= 0
550  || $this->user->isAnonymous()
551  );
552  }
553 
555  {
556  $message = $this->lng->txt('tst_skl_level_thresholds_missing');
557 
558  $link_target = $this->buildLinkTarget(
560  );
561 
562  $link = $this->ui_factory->link()->standard(
563  $this->lng->txt('tst_skl_level_thresholds_link'),
564  $link_target
565  );
566 
567  return $this->ui_factory->messageBox()->failure($message)->withLinks([$link]);
568  }
569 
570  private function areSkillLevelThresholdsMissing(): bool
571  {
572  if (!$this->object->isSkillServiceEnabled()) {
573  return false;
574  }
575 
576  $questionContainerId = $this->object->getId();
577 
578  $assignmentList = new \ilAssQuestionSkillAssignmentList($this->database);
579  $assignmentList->setParentObjId($questionContainerId);
580  $assignmentList->loadFromDb();
581 
582  foreach ($assignmentList->getUniqueAssignedSkills() as $data) {
583  foreach ($data['skill']->getLevelData() as $level) {
584  $threshold = new \ilTestSkillLevelThreshold($this->database);
585  $threshold->setTestId($this->object->getTestId());
586  $threshold->setSkillBaseId($data['skill_base_id']);
587  $threshold->setSkillTrefId($data['skill_tref_id']);
588  $threshold->setSkillLevelId($level['id']);
589 
590  if (!$threshold->dbRecordExists()) {
591  return true;
592  }
593  }
594  }
595 
596  return false;
597  }
598 
599  private function buildLinkTarget(?string $cmd = null): string
600  {
601  $target = array_merge(['ilRepositoryGUI', 'ilObjTestGUI'], ['ilTestSkillAdministrationGUI', 'ilTestSkillLevelThresholdsGUI']);
602  return $this->ctrl->getLinkTargetByClass($target, $cmd);
603  }
604 }
isOK()
Get to know if the result is ok.
value()
Get the encapsulated value.
const ROOT_FOLDER_ID
Definition: constants.php:32
$url
Definition: shib_logout.php:68
A password is used as part of credentials for authentication.
Definition: Password.php:30
$http
Definition: deliver.php:30
const IL_CAL_UNIX
while($session_entry=$r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) return null
static http()
Fetches the global http state from ILIAS.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
$_SERVER['HTTP_HOST']
Definition: raiseError.php:26
readonly ilTestPassesSelector $test_passes_selector
global $lng
Definition: privfeed.php:31
static formatDate(ilDateTime $date, bool $a_skip_day=false, bool $a_include_wd=false, bool $include_seconds=false, ?ilObjUser $user=null,)
$message
Definition: xapiexit.php:31
__construct(private readonly \ilObjTest $object, private readonly \ilObjUser $user, private readonly UIFactory $ui_factory, private readonly UIRenderer $ui_renderer, private readonly \ilLanguage $lng, private readonly Refinery $refinery, private readonly \ilCtrlInterface $ctrl, private readonly \ilGlobalTemplateInterface $tpl, private readonly ContentStyle $content_style, private readonly HTTPServices $http, private readonly TabsManager $tabs_manager, private readonly \ilAccessHandler $access, private readonly \ilTestAccess $test_access, private readonly \ilDBInterface $database, private readonly \ilRbacSystem $rbac_system)
static set(string $a_var, $a_val)
Set a value.