ILIAS  release_10 Revision v10.1-43-ga1241a92c2f
class.ilTestPlayerAbstractGUI.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
32 
38 {
39  public const PRESENTATION_MODE_VIEW = 'view';
40  public const PRESENTATION_MODE_EDIT = 'edit';
41 
42  protected const FINISH_TEST_CMD = 'finishTest';
43 
45 
47  public bool $ending_time_reached;
48  public int $ref_id;
49 
53  private array $cached_question_guis = [];
54 
58  private array $cached_question_objects = [];
59 
62  protected ?ilTestSession $test_session = null;
63  protected ?ilSetting $ass_settings = null;
64  protected ?ilTestSequence $test_sequence = null;
66 
68 
69  protected const DISCARD_MODAL = "discard_modal";
70  protected const LOCKS_CHANGED_MODAL = "locks_changed_modal";
71  protected const LOCKS_UNCHANGED_MODAL = "locks_unchanged_modal";
72 
77  protected array $modal_signals = [];
78 
79  public function __construct(ilObjTest $object)
80  {
81  parent::__construct($object);
82  $this->ref_id = $this->testrequest->getRefId();
83  $this->password_checker = new ilTestPasswordChecker($this->rbac_system, $this->user, $this->object, $this->lng);
84  }
85 
86  public function executeCommand()
87  {
88  $this->tabs->clearTargets();
89 
90  $cmd = $this->ctrl->getCmd();
91  $next_class = $this->ctrl->getNextClass($this);
92 
93  if (($read_access = $this->checkReadAccess()) !== true) {
94  if ($cmd === 'autosave') {
95  echo $this->lng->txt('autosave_failed') . ': ' . $read_access;
96  exit;
97  }
98  $this->tpl->setOnScreenMessage('failure', $read_access, true);
99  $this->ctrl->redirectByClass([ilRepositoryGUI::class, ilObjTestGUI::class, TestScreenGUI::class]);
100  }
101 
102  $this->ctrl->saveParameter($this, "sequence");
103  $this->ctrl->saveParameter($this, "pmode");
104  $this->ctrl->saveParameter($this, "active_id");
105 
106  $this->initAssessmentSettings();
107 
108  $testSessionFactory = new ilTestSessionFactory($this->object, $this->db, $this->user);
109  $this->test_session = $testSessionFactory->getSession($this->testrequest->int('active_id'));
110 
111  $this->ensureExistingTestSession($this->test_session);
112  $this->checkTestSessionUser($this->test_session);
113 
114  $this->initProcessLocker($this->test_session->getActiveId());
115 
116  $test_sequence_factory = new ilTestSequenceFactory($this->object, $this->db, $this->questionrepository);
117  $this->test_sequence = $test_sequence_factory->getSequenceByTestSession($this->test_session);
118  $this->test_sequence->loadFromDb();
119  $this->test_sequence->loadQuestions();
120 
121  $this->question_related_objectives_list = new ilTestQuestionRelatedObjectivesList();
122 
124 
126 
127  $instance_name = $this->settings->get('short_inst_name') ?? '';
128  if (trim($instance_name) === '') {
129  $instance_name = 'ILIAS';
130  }
131  $this->global_screen->tool()->context()->current()->addAdditionalData(
133  $instance_name
134  );
135  $this->global_screen->tool()->context()->current()->addAdditionalData(
137  $this->object->getKioskMode()
138  );
139  $this->global_screen->tool()->context()->current()->addAdditionalData(
141  $this->object->getTitle()
142  );
143  $this->global_screen->tool()->context()->current()->addAdditionalData(
145  $this->getTestPlayerTitle()
146  );
147 
148  switch ($next_class) {
149  case 'ilassquestionpagegui':
150  $this->checkTestExecutable();
151 
152  $question_id = $this->test_sequence->getQuestionForSequence($this->getCurrentSequenceElement());
153 
154  $page_gui = new ilAssQuestionPageGUI($question_id);
155  $ret = $this->ctrl->forwardCommand($page_gui);
156  break;
157 
158  case 'iltestsubmissionreviewgui':
159  $this->checkTestExecutable();
160  $this->handleCheckTestPassValid(true);
161 
162  $gui = new ilTestSubmissionReviewGUI($this, $this->object, $this->test_session);
163  $gui->setObjectiveOrientedContainer($this->getObjectiveOrientedContainer());
164  $ret = $this->ctrl->forwardCommand($gui);
165  break;
166 
167  case 'ilassquestionhintrequestgui':
168  $this->checkTestExecutable();
169 
170  $question_gui = $this->object->createQuestionGUI(
171  "",
172  $this->test_sequence->getQuestionForSequence($this->getCurrentSequenceElement())
173  );
174 
175  $questionHintTracking = new ilAssQuestionHintTracking(
176  $question_gui->getObject()->getId(),
177  $this->test_session->getActiveId(),
178  $this->test_session->getPass()
179  );
180 
181  $gui = new ilAssQuestionHintRequestGUI(
182  $this,
184  $question_gui,
185  $questionHintTracking,
186  $this->ctrl,
187  $this->lng,
188  $this->tpl,
189  $this->tabs,
190  $this->global_screen
191  );
192 
193  // fau: testNav - save the 'answer changed' status for viewing hint requests
195  // fau.
196  $ret = $this->ctrl->forwardCommand($gui);
197 
198  break;
199 
200  case 'ilassspecfeedbackpagegui':
201  case 'ilassgenfeedbackpagegui':
202  $id = $this->testrequest->int('pg_id');
203  if ($this->ctrl->getCmd() !== 'displayMediaFullscreen'
204  || $id === 0) {
205  break;
206  }
207 
208  (new ilPageObjectGUI(
209  $next_class === 'ilassgenfeedbackpagegui' ? 'qfbg' : 'qfbs',
210  $id
211  ))->displayMediaFullscreen();
212  break;
213 
214  case 'iltestpasswordprotectiongui':
215  $this->checkTestExecutable();
216 
217  $gui = new ilTestPasswordProtectionGUI(
218  $this->ctrl,
219  $this->tpl,
220  $this->lng,
221  $this,
222  $this->password_checker,
223  $this->testrequest,
224  $this->global_screen
225  );
226  $ret = $this->ctrl->forwardCommand($gui);
227  break;
228 
229  default:
230  if ($cmd !== 'autosave' && ilTestPlayerCommands::isTestExecutionCommand($cmd)) {
231  $this->checkTestExecutable();
232  }
233 
234  if ($cmd === 'outQuestionSummary'
235  || $cmd === 'submitSolution') {
236  $this->handleCheckTestPassValid(true);
237  }
238 
239  if ($cmd === 'showQuestion') {
240  $testPassesSelector = new ilTestPassesSelector($this->db, $this->object);
241  $testPassesSelector->setActiveId($this->test_session->getActiveId());
242  $testPassesSelector->setLastFinishedPass($this->test_session->getLastFinishedPass());
243 
244  if (!$testPassesSelector->openPassExists()) {
245  $this->tpl->setOnScreenMessage('info', $this->lng->txt('tst_pass_finished'), true);
246  $this->ctrl->redirectByClass([ilRepositoryGUI::class, ilObjTestGUI::class, TestScreenGUI::class]);
247  }
248  }
249 
250  $cmd .= 'Cmd';
251  $ret = $this->$cmd();
252  break;
253  }
254  return $ret;
255  }
256 
257  abstract protected function buildTestPassQuestionList();
258  abstract protected function populateQuestionOptionalMessage();
259 
260  protected function checkReadAccess(): bool|string
261  {
262  if (!$this->rbac_system->checkAccess('read', $this->object->getRefId())) {
263  return $this->lng->txt('cannot_execute_test');
264  }
265 
266  $participant_access = (new ilTestAccess($this->object->getRefId()))->isParticipantAllowed(
267  $this->object->getId(),
268  $this->user->getId()
269  );
270  if ($participant_access !== ParticipantAccess::ALLOWED) {
271  return $participant_access->getAccessForbiddenMessage($this->lng);
272  }
273 
274  return true;
275  }
276 
277  protected function checkTestExecutable()
278  {
279  $executable = $this->object->isExecutable($this->test_session, $this->test_session->getUserId());
280 
281  if (!$executable['executable']) {
282  $this->tpl->setOnScreenMessage('info', $executable['errormessage'], true);
283  $this->ctrl->redirectByClass([ilRepositoryGUI::class, ilObjTestGUI::class, TestScreenGUI::class]);
284  }
285  }
286 
287  protected function checkTestSessionUser(ilTestSession $test_session): void
288  {
289  if ($test_session->getUserId() != $this->user->getId()) {
290  throw new ilTestException('active id given does not relate to current user!');
291  }
292  }
293 
294  protected function ensureExistingTestSession(ilTestSession $test_session): void
295  {
296  if ($test_session->getActiveId()) {
297  return;
298  }
299 
300  $test_session->setUserId($this->user->getId());
301 
302  if ($test_session->isAnonymousUser()) {
303  if (!$test_session->doesAccessCodeInSessionExists()) {
304  return;
305  }
306 
307  $test_session->setAnonymousId($test_session->getAccessCodeFromSession());
308  }
309 
310  $test_session->saveToDb();
311  }
312 
313  protected function initProcessLocker($activeId)
314  {
315  $ilDB = $this->db;
316  $process_lockerFactory = new ilTestProcessLockerFactory($this->ass_settings, $ilDB);
317  $this->process_locker = $process_lockerFactory->withContextId((int) $activeId)->getLocker();
318  }
319 
326  public function saveTagsCmd()
327  {
328  $tagging_gui = new ilTaggingGUI();
329  $tagging_gui->setObject($this->object->getId(), $this->object->getType());
330  $tagging_gui->saveInput();
331  $this->ctrl->redirectByClass([ilRepositoryGUI::class, ilObjTestGUI::class, ilInfoScreenGUI::class]);
332  }
333 
337  public function updateWorkingTime()
338  {
339  if (ilSession::get("active_time_id") != null) {
340  $this->object->updateWorkingTime(ilSession::get("active_time_id"));
341  }
342 
344  "active_time_id",
345  $this->object->startWorkingTime(
346  $this->test_session->getActiveId(),
347  $this->test_session->getPass()
348  )
349  );
350  }
351 
352  public function removeIntermediateSolution(): void
353  {
354  $question_id = $this->getCurrentQuestionId();
355 
356  $this->getQuestionInstance($question_id)->removeIntermediateSolution(
357  $this->test_session->getActiveId(),
358  $this->test_session->getPass()
359  );
360  }
361 
362  public function saveQuestionSolution(
363  bool $authorized = true,
364  bool $force = false
365  ): bool {
366  $this->updateWorkingTime();
367 
368  $formtimestamp = $this->testrequest->int('formtimestamp');
369  if (!$force
370  && ilSession::get('formtimestamp') !== null
371  && $formtimestamp === ilSession::get('formtimestamp')) {
372  return false;
373  }
374 
375  ilSession::set('formtimestamp', $formtimestamp);
376 
377  /*
378  #21097 - exceed maximum passes
379  this is a battle of conditions; e.g. ilTestPlayerAbstractGUI::autosaveOnTimeLimitCmd forces saving of results.
380  However, if an admin has finished the pass in the meantime, a new pass should not be created.
381  */
382  if ($force && $this->isNrOfTriesReached()) {
383  $force = false;
384  }
385 
386  $question_obj = $this->buildQuestionObject();
387 
388  if ($question_obj === null) {
389  return false;
390  }
391 
392  if ($this->canSaveResult() || $force) {
393  $saved = $this->save($question_obj, $authorized);
394  }
395 
396  if (!$saved
397  || ($question_obj instanceof QuestionPartiallySaveable
398  && !$question_obj->validateSolutionSubmit())) {
399 
400  $this->ctrl->setParameter($this, 'save_error', '1');
401  ilSession::set('previouspost', $this->testrequest->getParsedBody());
402  }
403 
404  return $saved;
405  }
406 
407  private function buildQuestionObject(): ?assQuestion
408  {
409  $q_id = $this->test_sequence->getQuestionForSequence($this->testrequest->int('sequence'));
410 
411  if ($this->isParticipantsAnswerFixed($q_id)) {
412  // should only be reached by firebugging the disabled form in ui
413  throw new ilTestException('not allowed request');
414  }
415 
416  if ($q_id === null) {
417  return null;
418  }
419 
420  return $this->getQuestionInstance($q_id);
421  }
422 
423  private function save(assQuestion $question_obj, bool $authorized): bool
424  {
425  $active_id = $this->test_session->getActiveId();
426  $pass = ilObjTest::_getPass($active_id);
427  if (!$question_obj->persistWorkingState(
428  $active_id,
429  $pass,
430  $authorized
431  )) {
432  return false;
433  }
434 
435  if ($authorized && $this->test_session->isObjectiveOriented()) {
436  $objectivesAdapter = ilLOTestQuestionAdapter::getInstance($this->test_session);
437  $objectivesAdapter->updateQuestionResult($this->test_session, $question_obj);
438  }
439 
440  if ($authorized && $this->object->isSkillServiceToBeConsidered()) {
441  $this->handleSkillTriggering($this->test_session);
442  }
443 
444  if ($authorized && $this->logger->isLoggingEnabled()
445  && !$this->getObject()->getAnonymity()
446  && ($interaction = $question_obj->answerToParticipantInteraction(
447  $this->logger->getAdditionalInformationGenerator(),
448  $this->getObject()->getRefId(),
449  $active_id,
450  $pass,
451  $this->logger->isIPLoggingEnabled() ? $_SERVER['REMOTE_ADDR'] : '',
452  TestParticipantInteractionTypes::ANSWER_SUBMITTED
453  )) !== null) {
454  $this->logger->logParticipantInteraction($interaction);
455  }
456  return true;
457  }
458 
459  protected function canSaveResult(): bool
460  {
461  return !$this->object->endingTimeReached() && !$this->isMaxProcessingTimeReached() && !$this->isNrOfTriesReached();
462  }
463 
464  public function suspendTestCmd()
465  {
466  $this->ctrl->redirectByClass(TestScreenGUI::class, TestScreenGUI::DEFAULT_CMD);
467  }
468 
469  public function isMaxProcessingTimeReached(): bool
470  {
471  $active_id = $this->test_session->getActiveId();
472  $starting_time = $this->object->getStartingTimeOfUser($active_id);
473  if ($starting_time === false) {
474  return false;
475  } else {
476  return $this->object->isMaxProcessingTimeReached($starting_time, $active_id);
477  }
478  }
479 
480  protected function determineInlineScoreDisplay(): bool
481  {
482  $show_question_inline_score = false;
483  if ($this->object->getAnswerFeedbackPoints()) {
484  $show_question_inline_score = true;
485  return $show_question_inline_score;
486  }
487  return $show_question_inline_score;
488  }
489 
490  protected function populateTestNavigationToolbar(ilTestNavigationToolbarGUI $toolbar_gui): void
491  {
492  $this->tpl->setCurrentBlock('test_nav_toolbar');
493  $this->tpl->setVariable('TEST_NAV_TOOLBAR', $toolbar_gui->getHTML());
494  $this->tpl->parseCurrentBlock();
495 
496  if ($this->finish_test_modal === null) {
497  return;
498  }
499 
500  $this->tpl->setCurrentBlock('finish_test_modal');
501  $this->tpl->setVariable(
502  'FINISH_TEST_MODAL',
503  $this->ui_renderer->render(
504  $this->finish_test_modal->withOnLoad($this->finish_test_modal->getShowSignal())
505  )
506  );
507  $this->tpl->parseCurrentBlock();
508  }
509 
510  protected function populateQuestionNavigation($sequence_element, $primary_next): void
511  {
512  if (!$this->isFirstQuestionInSequence($sequence_element)) {
513  $this->populatePreviousButtons();
514  }
515 
516  if (!$this->isLastQuestionInSequence($sequence_element)) {
517  $this->populateNextButtons($primary_next);
518  }
519  }
520 
521  protected function populatePreviousButtons(): void
522  {
525  }
526 
527  protected function populateNextButtons($primary_next): void
528  {
529  $this->populateUpperNextButtonBlock($primary_next);
530  $this->populateLowerNextButtonBlock($primary_next);
531  }
532 
533  protected function populateLowerNextButtonBlock($primary_next): void
534  {
535  $button = $this->buildNextButtonInstance($primary_next);
536 
537  $this->tpl->setCurrentBlock("next_bottom");
538  $this->tpl->setVariable("BTN_NEXT_BOTTOM", $this->ui_renderer->render($button));
539  $this->tpl->parseCurrentBlock();
540  }
541 
542  protected function populateUpperNextButtonBlock($primaryNext)
543  {
544  $button = $this->buildNextButtonInstance($primaryNext);
545 
546  $this->tpl->setCurrentBlock("next");
547  $this->tpl->setVariable("BTN_NEXT", $this->ui_renderer->render($button));
548  $this->tpl->parseCurrentBlock();
549  }
550 
551  protected function populateLowerPreviousButtonBlock()
552  {
553  $button = $this->buildPreviousButtonInstance();
554 
555  $this->tpl->setCurrentBlock("prev_bottom");
556  $this->tpl->setVariable("BTN_PREV_BOTTOM", $this->ui_renderer->render($button));
557  $this->tpl->parseCurrentBlock();
558  }
559 
560  protected function populateUpperPreviousButtonBlock()
561  {
562  $button = $this->buildPreviousButtonInstance();
563 
564  $this->tpl->setCurrentBlock("prev");
565  $this->tpl->setVariable("BTN_PREV", $this->ui_renderer->render($button));
566  $this->tpl->parseCurrentBlock();
567  }
568 
573  private function buildNextButtonInstance($primaryNext)
574  {
575  $target = $this->ctrl->getLinkTarget($this, ilTestPlayerCommands::NEXT_QUESTION);
576  if ($primaryNext) {
577  $button = $this->ui_factory->button()->primary(
578  $this->lng->txt('next_question') . '<span class="glyphicon glyphicon-arrow-right"></span> ',
579  ''
580  )->withUnavailableAction(true)
582  } else {
583  $button = $this->ui_factory->button()->standard(
584  $this->lng->txt('next_question') . '<span class="glyphicon glyphicon-arrow-right"></span> ',
585  ''
586  )->withUnavailableAction(true)
588  }
589  return $button;
590  }
591 
596  private function buildPreviousButtonInstance()
597  {
598  $target = $this->ctrl->getLinkTarget($this, ilTestPlayerCommands::PREVIOUS_QUESTION);
599  $button = $this->ui_factory->button()->standard(
600  '<span class="glyphicon glyphicon-arrow-left"></span> ' . $this->lng->txt('previous_question'),
601  ''
602  )->withUnavailableAction(true)
604  return $button;
605  }
606 
607  private function getOnLoadCodeForNavigationButtons(string $target, string $cmd): Closure
608  {
609  return static function (string $id) use ($target, $cmd): string {
610  return "document.getElementById('{$id}').addEventListener('click', "
611  . "(e) => {il.TestPlayerQuestionEditControl.checkNavigation('{$target}', '{$cmd}', e);}"
612  . "); "
613  . "document.getElementById('{$id}').removeAttribute('disabled');";
614  };
615  }
616 
620  protected function populateSpecificFeedbackBlock(assQuestionGUI $question_gui): bool
621  {
622  $solutionValues = $question_gui->getObject()->getSolutionValues(
623  $this->test_session->getActiveId(),
624  null
625  );
626 
627  $feedback = $question_gui->getSpecificFeedbackOutput(
628  $question_gui->getObject()->fetchIndexedValuesFromValuePairs($solutionValues)
629  );
630 
631  if (!empty($feedback)) {
632  $this->tpl->setCurrentBlock("specific_feedback");
633  $this->tpl->setVariable("SPECIFIC_FEEDBACK", $feedback);
634  $this->tpl->parseCurrentBlock();
635  return true;
636  }
637  return false;
638  }
639 
643  protected function populateGenericFeedbackBlock(assQuestionGUI $question_gui, $solutionCorrect): bool
644  {
645  // fix #031263: add pass
646  $feedback = $question_gui->getGenericFeedbackOutput($this->test_session->getActiveId(), $this->test_session->getPass());
647 
648  if (strlen($feedback)) {
649  $cssClass = (
650  $solutionCorrect ?
652  );
653 
654  $this->tpl->setCurrentBlock("answer_feedback");
655  $this->tpl->setVariable("ANSWER_FEEDBACK", $feedback);
656  $this->tpl->setVariable("ILC_FB_CSS_CLASS", $cssClass);
657  $this->tpl->parseCurrentBlock();
658  return true;
659  }
660  return false;
661  }
662 
663  protected function populateScoreBlock($reachedPoints, $maxPoints)
664  {
665  $scoreInformation = sprintf(
666  $this->lng->txt("you_received_a_of_b_points"),
667  $reachedPoints,
668  $maxPoints
669  );
670 
671  $this->tpl->setCurrentBlock("received_points_information");
672  $this->tpl->setVariable("RECEIVED_POINTS_INFORMATION", $scoreInformation);
673  $this->tpl->parseCurrentBlock();
674  }
675 
676  protected function populateSolutionBlock($solutionoutput)
677  {
678  if (strlen($solutionoutput)) {
679  $this->tpl->setCurrentBlock("solution_output");
680  $this->tpl->setVariable("CORRECT_SOLUTION", $this->lng->txt("tst_best_solution_is"));
681  $this->tpl->setVariable("QUESTION_FEEDBACK", $solutionoutput);
682  $this->tpl->parseCurrentBlock();
683  }
684  }
685 
686  protected function populateSyntaxStyleBlock()
687  {
688  $this->tpl->setCurrentBlock("SyntaxStyle");
689  $this->tpl->setVariable(
690  "LOCATION_SYNTAX_STYLESHEET",
692  );
693  $this->tpl->parseCurrentBlock();
694  }
695 
696  protected function populateContentStyleBlock()
697  {
698  $this->tpl->setCurrentBlock("ContentStyle");
699  $this->tpl->setVariable(
700  "LOCATION_CONTENT_STYLESHEET",
702  );
703  $this->tpl->parseCurrentBlock();
704  }
705 
711  public function setAnonymousIdCmd(): void
712  {
713  if ($this->test_session->isAnonymousUser()) {
714  $this->test_session->setAccessCodeToSession($this->testrequest->strVal('anonymous_id'));
715  }
716 
717  $this->ctrl->redirectByClass([ilRepositoryGUI::class, ilObjTestGUI::class, ilInfoScreenGUI::class]);
718  }
719 
726  protected function startPlayerCmd()
727  {
728  $testStartLock = $this->getLockParameter();
729  $isFirstTestStartRequest = false;
730 
731  $this->process_locker->executeTestStartLockOperation(function () use ($testStartLock, &$isFirstTestStartRequest) {
732  if ($this->test_session->lookupTestStartLock() !== $testStartLock) {
733  $this->test_session->persistTestStartLock($testStartLock);
734  $isFirstTestStartRequest = true;
735  }
736  });
737 
738  if ($isFirstTestStartRequest) {
739  $this->handleUserSettings();
740  $this->ctrl->redirect($this, ilTestPlayerCommands::INIT_TEST);
741  }
742 
743  $this->ctrl->setParameterByClass('ilObjTestGUI', 'lock', $testStartLock);
744  $this->ctrl->redirectByClass([ilRepositoryGUI::class, ilObjTestGUI::class, ilInfoScreenGUI::class]);
745  }
746 
747  public function getLockParameter()
748  {
749  if ($this->testrequest->isset('lock') && strlen($this->testrequest->raw('lock'))) {
750  return $this->testrequest->raw('lock');
751  }
752 
753  return null;
754  }
755 
756  protected function resumePlayerCmd()
757  {
758  $this->handleUserSettings();
759 
760  $active_id = $this->test_session->getActiveId();
761  $this->ctrl->setParameter($this, "active_id", $active_id);
762 
763  $active_time_id = $this->object->startWorkingTime($active_id, $this->test_session->getPass());
764  ilSession::set("active_time_id", $active_time_id);
765  ilSession::set('tst_pass_finish', 0);
766 
767  if ($this->object->isRandomTest()) {
768  if (!$this->test_sequence->hasRandomQuestionsForPass($active_id, $this->test_session->getPass())) {
769  // create a new set of random questions
771  }
772  }
773 
774  $shuffle = $this->object->getShuffleQuestions();
775  if ($this->object->isRandomTest()) {
776  $shuffle = false;
777  }
778 
779  $this->object->updateTestPassResults(
780  $active_id,
781  $this->test_session->getPass(),
782  null,
783  $this->object->getId()
784  );
785 
786  // ensure existing test sequence
787  if (!$this->test_sequence->hasSequence()) {
788  $this->test_sequence->createNewSequence($this->object->getQuestionCount(), $shuffle);
789  $this->test_sequence->saveToDb();
790  }
791 
792  if ($this->object->getListOfQuestionsStart()) {
793  $this->ctrl->redirect($this, ilTestPlayerCommands::QUESTION_SUMMARY);
794  }
795 
796  $this->ctrl->setParameter($this, 'sequence', $this->test_session->getLastSequence());
797  $this->ctrl->setParameter($this, 'pmode', '');
798  $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
799  }
800 
804  protected function initTestCmd()
805  {
806  if ($this->test_session->isAnonymousUser()
807  && !$this->test_session->doesAccessCodeInSessionExists()) {
808  $access_code = $this->test_session->createNewAccessCode();
809 
810  $this->test_session->setAccessCodeToSession($access_code);
811  $this->test_session->setAnonymousId($access_code);
812  $this->test_session->saveToDb();
813 
814  $this->ctrl->redirect($this, ilTestPlayerCommands::DISPLAY_ACCESS_CODE);
815  }
816 
817  if (!$this->test_session->isAnonymousUser()) {
818  $this->test_session->unsetAccessCodeInSession();
819  }
820  $this->ctrl->redirect($this, ilTestPlayerCommands::START_TEST);
821  }
822 
823  public function displayAccessCodeCmd()
824  {
825  $this->tpl->addBlockFile($this->getContentBlockName(), "adm_content", "tpl.il_as_tst_anonymous_code_presentation.html", "components/ILIAS/Test");
826  $this->tpl->setCurrentBlock("adm_content");
827  $this->tpl->setVariable("TEXT_ANONYMOUS_CODE_CREATED", $this->lng->txt("tst_access_code_created"));
828  $this->tpl->setVariable("TEXT_ANONYMOUS_CODE", $this->test_session->getAccessCodeFromSession());
829  $this->tpl->setVariable("FORMACTION", $this->ctrl->getFormAction($this));
830  $this->tpl->setVariable("CMD_CONFIRM", ilTestPlayerCommands::ACCESS_CODE_CONFIRMED);
831  $this->tpl->setVariable("TXT_CONFIRM", $this->lng->txt("continue_work"));
832  $this->tpl->parseCurrentBlock();
833  }
834 
835  public function accessCodeConfirmedCmd()
836  {
837  $this->ctrl->redirect($this, ilTestPlayerCommands::START_TEST);
838  }
839 
843  public function handleUserSettings()
844  {
845  if ($this->object->getNrOfTries() != 1
846  && $this->object->getUsePreviousAnswers() == 1
847  ) {
848  $chb_use_previous_answers = 0;
849  if ($this->post_wrapper->has('chb_use_previous_answers')) {
850  $chb_use_previous_answers = $this->post_wrapper->retrieve(
851  'chb_use_previous_answers',
852  $this->refinery->kindlyTo()->int()
853  );
854  }
855  $this->user->writePref("tst_use_previous_answers", (string) $chb_use_previous_answers);
856  }
857  }
858 
863  public function redirectAfterAutosaveCmd(): void
864  {
865  $this->performTestPassFinishedTasks(StatusOfAttempt::FINISHED_BY_DURATION);
866 
867  $this->redirectAfterFinish();
868  }
869 
870  public function redirectAfterQuestionListCmd(): void
871  {
872  $this->performTestPassFinishedTasks(StatusOfAttempt::FINISHED_BY_DURATION);
873 
874  $this->redirectAfterFinish();
875  }
876 
877  protected function redirectAfterFinish(): void
878  {
879  $url = $this->ctrl->getLinkTarget($this, ilTestPlayerCommands::AFTER_TEST_PASS_FINISHED, '', false, false);
880 
881  $this->tpl->addBlockFile($this->getContentBlockName(), "adm_content", "tpl.il_as_tst_redirect_autosave.html", "components/ILIAS/Test");
882  $this->tpl->setVariable("TEXT_REDIRECT", $this->lng->txt("redirectAfterSave"));
883  $this->tpl->setVariable("URL", $url);
884  }
885 
886  protected function getCurrentQuestionId(): int
887  {
888  return $this->test_sequence->getQuestionForSequence($this->testrequest->int('sequence'));
889  }
890 
895  public function autosaveCmd(): void
896  {
897  if (!$this->access->checkAccess('read', '', $this->ref_id)) {
898  echo $this->lng->txt('autosave_failed') . ': ' . $this->lng->txt('msg_no_perm_read_item');
899  exit;
900  }
901  $test_can_run = $this->object->isExecutable($this->test_session, $this->test_session->getUserId());
902  if (!$test_can_run['executable']) {
903  echo $test_can_run['errormessage'];
904  exit;
905  }
906  if ($this->testrequest->getPostKeys() === []) {
907  echo '';
908  exit;
909  }
910 
911  if (!$this->canSaveResult() || $this->isParticipantsAnswerFixed($this->getCurrentQuestionId())) {
912  echo '-IGNORE-';
913  exit;
914  }
915 
916  if ($this->saveQuestionSolution(!$this->getAnswerChangedParameter(), true)) {
917  echo $this->lng->txt('autosave_success');
918  exit;
919  }
920 
921  echo $this->lng->txt('autosave_failed');
922  exit;
923  }
924 
929  public function autosaveOnTimeLimitCmd()
930  {
931  if (!$this->isParticipantsAnswerFixed($this->getCurrentQuestionId())) {
932  $this->saveQuestionSolution(false, true);
933  }
934  $this->ctrl->redirect($this, ilTestPlayerCommands::REDIRECT_ON_TIME_LIMIT);
935  }
936 
937 
938  // fau: testNav - new function detectChangesCmd()
944  protected function detectChangesCmd()
945  {
946  $question_id = $this->getCurrentQuestionId();
947  $state = $this->getQuestionInstance($question_id)->lookupForExistingSolutions(
948  $this->test_session->getActiveId(),
949  $this->test_session->getPass()
950  );
951  $result = [];
952  $result['isAnswered'] = $state['authorized'];
953  $result['isAnswerChanged'] = $state['intermediate'];
954 
955  echo json_encode($result);
956  exit;
957  }
958  // fau.
959 
960  protected function submitIntermediateSolutionCmd()
961  {
962  $this->saveQuestionSolution(false, true);
963  // fau: testNav - set the 'answer changed' parameter when an intermediate solution is submitted
964  $this->setAnswerChangedParameter(true);
965  // fau.
966  $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
967  }
968 
969  protected function markQuestionAndSaveIntermediateCmd(): void
970  {
971  $this->handleIntermediateSubmit();
972  $this->markQuestionCmd();
973  }
974 
978  protected function markQuestionCmd(): void
979  {
980  $question_id = $this->test_sequence->getQuestionForSequence(
982  );
983 
984  $this->object->setQuestionSetSolved(1, $question_id, $this->test_session->getUserId());
985 
986  $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
987  }
988 
990  {
991  // fau: testNav - handle intermediate submit when unmarking the question
992  $this->handleIntermediateSubmit();
993  // fau.
994  $this->unmarkQuestionCmd();
995  }
996 
1000  protected function unmarkQuestionCmd()
1001  {
1002  $question_id = $this->test_sequence->getQuestionForSequence(
1003  $this->getCurrentSequenceElement()
1004  );
1005 
1006  $this->object->setQuestionSetSolved(0, $question_id, $this->test_session->getUserId());
1007 
1008  $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
1009  }
1010 
1011  public function finishTestCmd()
1012  {
1013  $this->handleCheckTestPassValid();
1014  ilSession::clear('tst_next');
1015 
1016  if ($this->testrequest->strVal('finalization_confirmed') !== 'confirmed') {
1017  $this->finish_test_modal = $this->buildFinishTestModal();
1018  $this->showQuestionCmd();
1019  return;
1020  }
1021 
1022  // Non-last try finish
1023  if (ilSession::get('tst_pass_finish') === null) {
1024  ilSession::set('tst_pass_finish', 1);
1025  }
1026 
1027  $this->performTestPassFinishedTasks(StatusOfAttempt::FINISHED_BY_PARTICIPANT);
1028 
1029  if ($this->logger->isLoggingEnabled()
1030  && !$this->getObject()->getAnonymity()
1031  && ($interaction = $this->logger->getInteractionFactory()->buildParticipantInteraction(
1032  $this->ref_id,
1033  null,
1034  $this->user->getId(),
1035  $this->logger->isIPLoggingEnabled() ? $_SERVER['REMOTE_ADDR'] : '',
1037  []
1038  )) !== null) {
1039  $this->logger->logParticipantInteraction($interaction);
1040  }
1041 
1042  $this->ctrl->redirect($this, ilTestPlayerCommands::AFTER_TEST_PASS_FINISHED);
1043  }
1044 
1045  protected function performTestPassFinishedTasks(StatusOfAttempt $status_of_attempt): void
1046  {
1047  (new ilTestPassFinishTasks(
1048  $this->test_session,
1049  $this->object,
1050  $this->test_pass_result_repository
1051  ))->performFinishTasks($this->process_locker, $status_of_attempt);
1052  $this->object->updateTestResultCache($this->test_session->getActiveId(), null);
1053 
1055  $this->test_session->getActiveId(),
1056  $this->test_session->getPass()
1057  );
1058  }
1059 
1060  protected function sendNewPassFinishedNotificationEmailIfActivated(int $active_id, int $pass)
1061  {
1062  $notification_type = $this->object->getMainSettings()->getFinishingSettings()->getMailNotificationContentType();
1063 
1064  if ($notification_type === 0
1065  || !$this->object->getMainSettings()->getFinishingSettings()->getAlwaysSendMailNotification()
1066  && $pass !== $this->object->getNrOfTries() - 1) {
1067  return;
1068  }
1069 
1070  switch ($this->object->getMainSettings()->getFinishingSettings()->getMailNotificationContentType()) {
1071  case 1:
1072  $this->object->sendSimpleNotification($active_id);
1073  break;
1074  case 2:
1075  $this->object->sendAdvancedNotification($active_id);
1076  break;
1077  }
1078  }
1079 
1080  protected function afterTestPassFinishedCmd()
1081  {
1082  // show final statement
1083  if (!$this->testrequest->isset('skipfinalstatement')) {
1084  if ($this->object->getMainSettings()->getFinishingSettings()->getConcludingRemarksEnabled()) {
1085  $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_FINAL_STATMENT);
1086  }
1087  }
1088 
1089  // redirect after test
1090  $redirection_url = $this->object->getMainSettings()->getFinishingSettings()->getRedirectionUrl();
1091  if (!empty($redirection_url)
1092  && !$this->object->canShowTestResults($this->test_session)
1093  && $this->object->getMainSettings()->getFinishingSettings()->getRedirectionMode() !== ilObjTest::REDIRECT_NONE) {
1094  if ($this->object->isRedirectModeKiosk()) {
1095  if ($this->object->getKioskMode()) {
1096  ilUtil::redirect($redirection_url);
1097  }
1098  } else {
1099  ilUtil::redirect($redirection_url);
1100  }
1101  }
1102 
1103  // default redirect (pass overview when enabled, otherwise testscreen)
1104  $this->redirectBackCmd();
1105  }
1106 
1108  {
1109  $class = get_class($this);
1110  $this->ctrl->setParameterByClass($class, 'finalization_confirmed', 'confirmed');
1111  $next_url = $this->ctrl->getLinkTargetByClass($class, ilTestPlayerCommands::FINISH_TEST);
1112  $this->ctrl->clearParameterByClass($class, 'finalization_confirmed');
1113 
1114  $message = $this->lng->txt('tst_finish_confirmation_question');
1115  if (($this->object->getNrOfTries() - 1) === $this->test_session->getPass()) {
1116  $message = $this->lng->txt('tst_finish_confirmation_question_no_attempts_left');
1117  }
1118 
1119  return $this->ui_factory->modal()->interruptive(
1120  $this->lng->txt('finish_test'),
1121  $message,
1122  $next_url
1123  )->withActionButtonLabel($this->lng->txt('tst_finish_confirm_button'));
1124  }
1125 
1126  public function redirectBackCmd(): void
1127  {
1128  $testPassesSelector = new ilTestPassesSelector($this->db, $this->object);
1129  $testPassesSelector->setActiveId($this->test_session->getActiveId());
1130  $testPassesSelector->setLastFinishedPass($this->test_session->getLastFinishedPass());
1131 
1132  if (count($testPassesSelector->getReportablePasses())) {
1133  if ($this->getObjectiveOrientedContainer()->isObjectiveOrientedPresentationRequired()) {
1134  $this->ctrl->redirectByClass(['ilTestResultsGUI', 'ilTestEvalObjectiveOrientedGUI']);
1135  }
1136 
1137  $this->ctrl->redirectByClass([ilTestResultsGUI::class, ilMyTestResultsGUI::class, ilTestEvaluationGUI::class]);
1138  }
1139 
1140  $this->ctrl->redirectByClass(TestScreenGUI::class, TestScreenGUI::DEFAULT_CMD);
1141  }
1142 
1143  /*
1144  * Presents the final statement of a test
1145  */
1146  public function showFinalStatementCmd()
1147  {
1148  $this->global_screen->tool()->context()->current()->getAdditionalData()->replace(
1150  $this->object->getTitle() . ' - ' . $this->lng->txt('final_statement')
1151  );
1152 
1153  $this->content_style->gui()->addCss($this->tpl, $this->ref_id);
1154  $this->ctrl->setParameterByClass(ilTestPageGUI::class, 'page_type', 'concludingremarkspage');
1155  $this->ctrl->setParameterByClass(static::class, 'skipfinalstatement', 1);
1156  $this->tpl->setVariable(
1157  $this->getContentBlockName(),
1158  $this->ui_renderer->render([
1159  $this->ui_factory->legacy(
1160  $this->object->getFinalStatement()
1161  ),
1162  $this->ui_factory->button()->standard(
1163  $this->lng->txt('btn_next'),
1164  $this->ctrl->getLinkTargetByClass(static::class, ilTestPlayerCommands::AFTER_TEST_PASS_FINISHED)
1165  )
1166  ])
1167  );
1168  }
1169 
1170  protected function prepareTestPage($presentationMode, $sequenceElement, $question_id)
1171  {
1172  $this->navigation_history->addItem(
1173  $this->test_session->getRefId(),
1174  $this->ctrl->getLinkTarget($this, ilTestPlayerCommands::RESUME_PLAYER),
1175  'tst'
1176  );
1177 
1178  $this->initTestPageTemplate();
1179  $this->populateContentStyleBlock();
1180  $this->populateSyntaxStyleBlock();
1181 
1182  if ($this->isMaxProcessingTimeReached()) {
1183  $this->max_processing_time_reached();
1184  return;
1185  }
1186 
1187  if ($this->object->endingTimeReached()) {
1188  $this->endingTimeReached();
1189  return;
1190  }
1191 
1192  if ($this->isOptionalQuestionAnsweringConfirmationRequired($sequenceElement)) {
1193  $this->ctrl->setParameter($this, "sequence", $sequenceElement);
1195  return;
1196  }
1197 
1198  $this->tpl->setVariable("TEST_ID", (string) $this->object->getTestId());
1199  $this->tpl->setVariable("LOGIN", $this->user->getLogin());
1200 
1201  $this->tpl->setVariable("SEQ_ID", $sequenceElement);
1202  $this->tpl->setVariable("QUEST_ID", $question_id);
1203 
1204  if ($this->object->getEnableProcessingTime()) {
1205  $this->outProcessingTime($this->test_session->getActiveId(), false);
1206  }
1207 
1208  $this->tpl->setVariable("PAGETITLE", "- " . $this->object->getTitle());
1209 
1210  if ($this->object->isShowExamIdInTestPassEnabled() && !$this->object->getKioskMode()) {
1211  $this->tpl->setCurrentBlock('exam_id_footer');
1212  $this->tpl->setVariable('EXAM_ID_VAL', ilObjTest::lookupExamId(
1213  $this->test_session->getActiveId(),
1214  $this->test_session->getPass(),
1215  $this->object->getId()
1216  ));
1217  $this->tpl->setVariable('EXAM_ID_TXT', $this->lng->txt('exam_id'));
1218  $this->tpl->parseCurrentBlock();
1219  }
1220 
1221  if ($this->object->getListOfQuestions()) {
1222  $this->showSideList($sequenceElement);
1223  }
1224  }
1225 
1226  protected function isOptionalQuestionAnsweringConfirmationRequired(int $sequence_key): bool
1227  {
1228  if ($this->test_sequence->isAnsweringOptionalQuestionsConfirmed()) {
1229  return false;
1230  }
1231 
1232  $question_id = $this->test_sequence->getQuestionForSequence($sequence_key);
1233 
1234  if (!$this->test_sequence->isQuestionOptional($question_id)) {
1235  return false;
1236  }
1237 
1238  return true;
1239  }
1240 
1241  protected function isShowingPostponeStatusReguired(int $question_id): bool
1242  {
1243  return $this->test_sequence->isPostponedQuestion($question_id);
1244  }
1245 
1246  protected function showQuestionViewable(
1247  assQuestionGUI $question_gui,
1248  string $form_action,
1249  bool $is_question_worked_through,
1250  bool $instant_response
1251  ): void {
1252  $question_navigation_gui = $this->buildReadOnlyStateQuestionNavigationGUI($question_gui->getObject()->getId());
1253  $question_navigation_gui->setQuestionWorkedThrough($is_question_worked_through);
1254  $question_gui->setNavigationGUI($question_navigation_gui);
1255  $question_gui->getQuestionHeaderBlockBuilder()->setQuestionAnswered($is_question_worked_through);
1256 
1257  $solutionoutput = $question_gui->getSolutionOutput(
1258  $this->test_session->getActiveId(),
1259  $this->test_session->getPass(),
1260  false,
1261  false,
1262  true,
1263  $instant_response && $this->object->getSpecificAnswerFeedback(),
1264  false,
1265  false,
1266  true
1267  );
1268 
1269  $pageoutput = $question_gui->outQuestionPage(
1270  '',
1271  $this->isShowingPostponeStatusReguired($question_gui->getObject()->getId()),
1272  $this->test_session->getActiveId(),
1273  $solutionoutput
1274  );
1275 
1276  $this->tpl->setVariable(
1277  'LOCKSTATE_INFOBOX',
1278  $this->ui_renderer->render(
1279  $this->ui_factory->messageBox()->info($this->lng->txt('tst_player_answer_saved_and_locked'))
1280  )
1281  );
1282  $this->tpl->parseCurrentBlock();
1283  $this->tpl->setVariable('QUESTION_OUTPUT', $pageoutput);
1284  $this->tpl->setVariable('FORMACTION', $form_action);
1285  $this->tpl->setVariable('ENCTYPE', 'enctype="' . $question_gui->getFormEncodingType() . '"');
1286  $this->tpl->setVariable('FORM_TIMESTAMP', time());
1287  $this->populateQuestionEditControl($question_gui);
1288  }
1289 
1290  protected function showQuestionEditable(
1291  assQuestionGUI $question_gui,
1292  string $form_action,
1293  bool $is_question_worked_through,
1294  bool $instant_response
1295  ): void {
1296  $question_navigation_gui = $this->buildEditableStateQuestionNavigationGUI($question_gui->getObject()->getId());
1297  $question_navigation_gui->setQuestionWorkedThrough($is_question_worked_through);
1298  if ($is_question_worked_through) {
1299  $question_navigation_gui->setDiscardSolutionButtonEnabled(true);
1300  $question_gui->getQuestionHeaderBlockBuilder()->setQuestionAnswered(true);
1301  } elseif ($this->object->isPostponingEnabled()) {
1302  $question_navigation_gui->setSkipQuestionLinkTarget(
1303  $this->ctrl->getLinkTarget($this, ilTestPlayerCommands::SKIP_QUESTION)
1304  );
1305  }
1306  $question_gui->setNavigationGUI($question_navigation_gui);
1307 
1308  $user_post_solution = false;
1309  if ($this->testrequest->isset('save_error')
1310  && $this->testrequest->int('save_error') === 1
1311  && ilSession::get('previouspost') !== null) {
1312  $user_post_solution = ilSession::get('previouspost');
1313  ilSession::clear('previouspost');
1314  }
1315 
1316  $question_config = $question_gui->getObject()->getTestPresentationConfig();
1317 
1318  if ($question_gui instanceof assMultipleChoiceGUI) {
1319  $question_config->setWorkedThrough($is_question_worked_through);
1320  }
1321 
1322  if ($question_config->isPreviousPassSolutionReuseAllowed()) {
1323  $pass_index = $this->determineSolutionPassIndex($question_gui);
1324  if ($pass_index < $this->test_session->getPass()) {
1325  $question_config->setSolutionInitiallyPrefilled(true);
1326  }
1327  } else {
1328  $pass_index = $this->test_session->getPass();
1329  }
1330 
1331 
1332  $this->modal_signals = $this->populateModals();
1333  $question_navigation_gui->setShowDiscardModalSignal($this->modal_signals[self::DISCARD_MODAL]);
1334 
1335  $question_gui->outQuestionForTest(
1336  $form_action,
1337  $this->test_session->getActiveId(),
1338  $pass_index,
1339  $this->isShowingPostponeStatusReguired($question_gui->getObject()->getId()),
1340  $user_post_solution,
1341  $instant_response && $this->object->getSpecificAnswerFeedback()
1342  );
1343 
1344  $this->populateQuestionEditControl($question_gui);
1345  }
1346 
1347  protected function determineSolutionPassIndex(assQuestionGUI $question_gui): int
1348  {
1349  if ($this->object->isPreviousSolutionReuseEnabled($this->test_session->getActiveId())) {
1350  $currentSolutionAvailable = $question_gui->getObject()->authorizedOrIntermediateSolutionExists(
1351  $this->test_session->getActiveId(),
1352  $this->test_session->getPass()
1353  );
1354 
1355  if (!$currentSolutionAvailable) {
1356  $previousPass = $question_gui->getObject()->getSolutionMaxPass(
1357  $this->test_session->getActiveId()
1358  );
1359 
1360  $previousSolutionAvailable = $question_gui->getObject()->authorizedSolutionExists(
1361  $this->test_session->getActiveId(),
1362  $previousPass
1363  );
1364 
1365  if ($previousSolutionAvailable) {
1366  return $previousPass;
1367  }
1368 
1369  }
1370  }
1371 
1372  return $this->test_session->getPass();
1373  }
1374 
1375  protected function showQuestionCmd(): void
1376  {
1377  ilSession::set('tst_pass_finish', 0);
1378 
1380  "active_time_id",
1381  $this->object->startWorkingTime(
1382  $this->test_session->getActiveId(),
1383  $this->test_session->getPass()
1384  )
1385  );
1386 
1387  $this->help->setScreenIdComponent('tst');
1388  $this->help->setScreenId('assessment');
1389  $this->help->setSubScreenId('question');
1390 
1391  $sequence_element = $this->getCurrentSequenceElement();
1392 
1393  if (!$this->isValidSequenceElement($sequence_element)) {
1394  $sequence_element = $this->test_sequence->getFirstSequence();
1395  }
1396 
1397  $this->test_session->setLastSequence($sequence_element ?? 0);
1398  $this->test_session->saveToDb();
1399 
1400  $question_id = $this->test_sequence->getQuestionForSequence($sequence_element ?? 0);
1401  if ($question_id === null && $this->test_session->isObjectiveOriented()) {
1403  }
1404 
1405  if ($question_id !== null && !$this->test_sequence->isQuestionPresented($question_id)) {
1406  $this->test_sequence->setQuestionPresented($question_id);
1407  $this->test_sequence->saveToDb();
1408  }
1409 
1410  $question_worked_through = $this->questionrepository->lookupResultRecordExist(
1411  $this->test_session->getActiveId(),
1412  $question_id,
1413  $this->test_session->getPass()
1414  );
1415 
1416  $instant_response = false;
1417  if ($this->isParticipantsAnswerFixed($question_id)) {
1419  $s = $this->object->getMainSettings()->getQuestionBehaviourSettings();
1420  if ($s->getInstantFeedbackGenericEnabled()
1421  || $s->getInstantFeedbackPointsEnabled()
1422  || $s->getInstantFeedbackSolutionEnabled()
1423  || $s->getInstantFeedbackSpecificEnabled()) {
1424  $instant_response = true;
1425  }
1426  } else {
1428  if (!$this->object->isInstantFeedbackAnswerFixationEnabled()) {
1429  $instant_response = $this->getInstantResponseParameter();
1430  }
1431  }
1432 
1433  $question_gui = $this->getQuestionGuiInstance($question_id);
1434 
1435  if (!($question_gui instanceof assQuestionGUI)) {
1436  $this->handleTearsAndAngerQuestionIsNull($question_id, $sequence_element);
1437  }
1438 
1439  $question_gui->setSequenceNumber($this->test_sequence->getPositionOfSequence($sequence_element));
1440  $question_gui->setQuestionCount($this->test_sequence->getUserQuestionCount());
1441 
1442  $header_block_builder = new ilTestQuestionHeaderBlockBuilder($this->lng);
1443  $header_block_builder->setHeaderMode($this->object->getTitleOutput());
1444  $header_block_builder->setQuestionTitle($question_gui->getObject()->getTitleForHTMLOutput());
1445  $header_block_builder->setQuestionPoints($question_gui->getObject()->getPoints());
1446  $header_block_builder->setQuestionPosition($this->test_sequence->getPositionOfSequence($sequence_element));
1447  $header_block_builder->setQuestionCount($this->test_sequence->getUserQuestionCount());
1448  $header_block_builder->setQuestionPostponed($this->test_sequence->isPostponedQuestion($question_id));
1449  if ($this->test_session->isObjectiveOriented()) {
1450  $objectives_adapter = ilLOTestQuestionAdapter::getInstance($this->test_session);
1451  $objectives_adapter->buildQuestionRelatedObjectiveList($this->test_sequence, $this->question_related_objectives_list);
1452  $this->question_related_objectives_list->loadObjectivesTitles();
1453 
1454  $header_block_builder->setQuestionRelatedObjectives(
1455  $this->question_related_objectives_list->getQuestionRelatedObjectiveTitles($question_id)
1456  );
1457  }
1458  $question_gui->setQuestionHeaderBlockBuilder($header_block_builder);
1459 
1460  $this->prepareTestPage($presentation_mode, $sequence_element, $question_id);
1461 
1462  $navigation_toolbar_gui = $this->getTestNavigationToolbarGUI();
1463  $navigation_toolbar_gui->setFinishTestButtonEnabled(true);
1464 
1465  $is_next_primary = $this->handlePrimaryButton($navigation_toolbar_gui, $question_id);
1466 
1467  $this->ctrl->setParameter($this, 'sequence', $sequence_element);
1468  $this->ctrl->setParameter($this, 'pmode', $presentation_mode);
1469  $form_action = $this->ctrl->getFormAction($this, ilTestPlayerCommands::SUBMIT_INTERMEDIATE_SOLUTION);
1470 
1471  switch ($presentation_mode) {
1473  $navigation_toolbar_gui->setDisabledStateEnabled(false);
1474  $this->showQuestionEditable(
1475  $question_gui,
1476  $form_action,
1477  $question_worked_through,
1478  $instant_response
1479  );
1480 
1481  if ($this->ctrl->getCmd() !== self::FINISH_TEST_CMD
1482  && $this->logger->isLoggingEnabled()
1483  && !$this->getObject()->getAnonymity()) {
1484  $this->logger->logParticipantInteraction(
1485  $this->logger->getInteractionFactory()->buildParticipantInteraction(
1486  $this->object->getRefId(),
1487  $question_id,
1488  $this->user->getId(),
1489  $this->logger->isIPLoggingEnabled() ? $_SERVER['REMOTE_ADDR'] : '',
1490  TestParticipantInteractionTypes::QUESTION_SHOWN,
1491  []
1492  )
1493  );
1494  }
1495  break;
1496 
1498  if ($this->test_sequence->isQuestionOptional($question_gui->getObject()->getId())) {
1500  }
1501 
1502  $this->showQuestionViewable(
1503  $question_gui,
1504  $form_action,
1505  $question_worked_through,
1506  $instant_response
1507  );
1508  break;
1509 
1510  default:
1511  throw new ilTestException('no presentation mode given');
1512  }
1513 
1514  $navigation_toolbar_gui->build();
1515  $this->populateTestNavigationToolbar($navigation_toolbar_gui);
1516  $this->populateQuestionNavigation($sequence_element, $is_next_primary);
1517 
1518  if ($instant_response) {
1520  $question_gui,
1521  true
1522  );
1523  }
1524 
1525  if ($this->isForcedFeedbackNavUrlRegistered()) {
1526  $this->populateInstantResponseModal($question_gui, $this->getRegisteredForcedFeedbackNavUrl());
1528  }
1529  }
1530 
1531  protected function editSolutionCmd()
1532  {
1533  $this->ctrl->setParameter($this, 'pmode', ilTestPlayerAbstractGUI::PRESENTATION_MODE_EDIT);
1534  $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
1535  }
1536 
1537  protected function submitSolutionCmd()
1538  {
1539  if ($this->saveQuestionSolution(true, false)) {
1540  $question_id = $this->test_sequence->getQuestionForSequence(
1541  $this->getCurrentSequenceElement()
1542  );
1543 
1544  $this->removeIntermediateSolution();
1545 
1546  if ($this->object->isForceInstantFeedbackEnabled()) {
1547  $this->ctrl->setParameter($this, 'instresp', 1);
1548 
1549  $this->test_sequence->setQuestionChecked($question_id);
1550  $this->test_sequence->saveToDb();
1551  }
1552 
1553  if ($this->getNextCommandParameter()) {
1554  if ($this->getNextSequenceParameter()) {
1555  $this->ctrl->setParameter($this, 'sequence', $this->getNextSequenceParameter());
1556  $this->ctrl->setParameter($this, 'pmode', '');
1557  }
1558 
1559  $this->ctrl->redirect($this, $this->getNextCommandParameter());
1560  }
1561 
1562  $this->ctrl->setParameter($this, 'pmode', ilTestPlayerAbstractGUI::PRESENTATION_MODE_VIEW);
1563  } else {
1564  $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
1565  }
1566 
1567  // fau: testNav - remember to prevent the navigation confirmation
1569  // fau.
1570 
1571  // fau: testNav - handle navigation after saving
1572  if ($this->getNavigationUrlParameter()) {
1574  } else {
1575  $this->ctrl->saveParameter($this, 'sequence');
1576  }
1577  // fau.
1578  $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
1579  }
1580 
1581  // fau: testNav - new function to revert probably auto-saved changes and show the last submitted question state
1582  protected function revertChangesCmd()
1583  {
1584  $this->removeIntermediateSolution();
1585  $this->setAnswerChangedParameter(false);
1586  $this->ctrl->saveParameter($this, 'sequence');
1587  $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
1588  }
1589  // fau.
1590 
1591  protected function discardSolutionCmd()
1592  {
1593  $current_sequence_element = $this->getCurrentSequenceElement();
1594 
1595  $current_question_obj = $this->getQuestionInstance(
1596  $this->test_sequence->getQuestionForSequence($current_sequence_element)
1597  );
1598  $current_question_obj->setTestId($this->object->getId());
1599 
1600  $current_question_obj->resetUsersAnswer(
1601  $this->test_session->getActiveId(),
1602  $this->test_session->getPass()
1603  );
1604 
1605  if ($this->logger->isLoggingEnabled()
1606  && !$this->getObject()->getAnonymity()) {
1607  $this->logger->logParticipantInteraction(
1608  $this->logger->getInteractionFactory()->buildParticipantInteraction(
1609  $this->object->getRefId(),
1610  $this->test_sequence->getQuestionForSequence($current_sequence_element),
1611  $this->user->getId(),
1612  $this->logger->isIPLoggingEnabled() ? $_SERVER['REMOTE_ADDR'] : '',
1613  TestParticipantInteractionTypes::ANSWER_DELETED,
1614  []
1615  )
1616  );
1617  }
1618 
1619  $this->ctrl->saveParameter($this, 'sequence');
1620 
1621  $this->ctrl->setParameter($this, 'pmode', ilTestPlayerAbstractGUI::PRESENTATION_MODE_VIEW);
1622 
1623  $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
1624  }
1625 
1626  protected function skipQuestionCmd()
1627  {
1628  $current_sequence_element = $this->getCurrentSequenceElement();
1629  $next_sequence_element = $this->test_sequence->getNextSequence($current_sequence_element);
1630 
1631  if (!$this->isValidSequenceElement($next_sequence_element)) {
1632  $next_sequence_element = $this->test_sequence->getFirstSequence();
1633  }
1634 
1635  if ($this->object->isPostponingEnabled()) {
1636  $this->test_sequence->postponeSequence($current_sequence_element);
1637  $this->test_sequence->saveToDb();
1638  }
1639 
1640  if ($this->logger->isLoggingEnabled()
1641  && !$this->getObject()->getAnonymity()) {
1642  $this->logger->logParticipantInteraction(
1643  $this->logger->getInteractionFactory()->buildParticipantInteraction(
1644  $this->object->getRefId(),
1645  $this->test_sequence->getQuestionForSequence($current_sequence_element),
1646  $this->user->getId(),
1647  $this->logger->isIPLoggingEnabled() ? $_SERVER['REMOTE_ADDR'] : '',
1648  TestParticipantInteractionTypes::QUESTION_SKIPPED,
1649  []
1650  )
1651  );
1652  }
1653 
1654  $this->ctrl->setParameter($this, 'sequence', $next_sequence_element);
1655  $this->ctrl->setParameter($this, 'pmode', '');
1656 
1657  $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
1658  }
1659 
1660  protected function startTestCmd()
1661  {
1662  ilSession::set('tst_pass_finish', 0);
1663 
1664  // ensure existing test session
1665  $this->test_session->setUserId($this->user->getId());
1666  $access_code = ilSession::get('tst_access_code');
1667  if ($access_code != null && isset($access_code[$this->object->getTestId()])) {
1668  $this->test_session->setAnonymousId($access_code[$this->object->getTestId()]);
1669  }
1670  if ($this->getObjectiveOrientedContainer()->isObjectiveOrientedPresentationRequired()) {
1671  $this->test_session->setObjectiveOrientedContainerId($this->getObjectiveOrientedContainer()->getObjId());
1672  }
1673  $this->test_session->saveToDb();
1674 
1675  $active_id = $this->test_session->getActiveId();
1676  $this->ctrl->setParameter($this, "active_id", $active_id);
1677 
1678  $shuffle = $this->object->getShuffleQuestions();
1679  if ($this->object->isRandomTest()) {
1681 
1682  $this->object->loadQuestions();
1683  $shuffle = false; // shuffle is already done during the creation of the random questions
1684  }
1685 
1686  $this->object->updateTestPassResults(
1687  $active_id,
1688  $this->test_session->getPass(),
1689  null,
1690  $this->object->getId()
1691  );
1692 
1693  // ensure existing test sequence
1694  if (!$this->test_sequence->hasSequence()) {
1695  $this->test_sequence->createNewSequence($this->object->getQuestionCount(), $shuffle);
1696  $this->test_sequence->saveToDb();
1697  }
1698 
1699  $this->test_sequence->loadFromDb();
1700  $this->test_sequence->loadQuestions();
1701 
1702  if ($this->test_session->isObjectiveOriented()) {
1703  $objectivesAdapter = ilLOTestQuestionAdapter::getInstance($this->test_session);
1704 
1705  $objectivesAdapter->notifyTestStart($this->test_session, $this->object->getId());
1706  $objectivesAdapter->prepareTestPass($this->test_session, $this->test_sequence);
1707 
1708  $objectivesAdapter->buildQuestionRelatedObjectiveList(
1709  $this->test_sequence,
1710  $this->question_related_objectives_list
1711  );
1712 
1713  if ($this->test_sequence->hasOptionalQuestions()) {
1715 
1716  $this->test_sequence->reorderOptionalQuestionsToSequenceEnd();
1717  $this->test_sequence->saveToDb();
1718  }
1719  }
1720 
1721  $active_time_id = $this->object->startWorkingTime(
1722  $this->test_session->getActiveId(),
1723  $this->test_session->getPass()
1724  );
1725  ilSession::set("active_time_id", $active_time_id);
1726 
1728 
1729  $sequence_element = $this->test_sequence->getFirstSequence();
1730 
1731  $this->ctrl->setParameter($this, 'sequence', $sequence_element);
1732  $this->ctrl->setParameter($this, 'pmode', '');
1733 
1734  if ($this->logger->isLoggingEnabled()
1735  && !$this->getObject()->getAnonymity()) {
1736  $this->logger->logParticipantInteraction(
1737  $this->logger->getInteractionFactory()->buildParticipantInteraction(
1738  $this->object->getRefId(),
1739  null,
1740  $this->user->getId(),
1741  $this->logger->isIPLoggingEnabled() ? $_SERVER['REMOTE_ADDR'] : '',
1742  TestParticipantInteractionTypes::TEST_RUN_STARTED,
1743  []
1744  )
1745  );
1746  }
1747 
1748  if ($this->object->getListOfQuestionsStart()) {
1749  $this->ctrl->setParameterByClass(static::class, 'first', '1');
1750  $this->ctrl->redirect($this, ilTestPlayerCommands::QUESTION_SUMMARY);
1751  }
1752 
1753  $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
1754  }
1755 
1759  public function isTestAccessible(): bool
1760  {
1761  return !$this->isNrOfTriesReached()
1762  and !$this->isMaxProcessingTimeReached()
1763  and $this->object->startingTimeReached()
1764  and !$this->object->endingTimeReached();
1765  }
1766 
1770  public function isNrOfTriesReached(): bool
1771  {
1772  return $this->object->hasNrOfTriesRestriction() && $this->object->isNrOfTriesReached($this->test_session->getPass());
1773  }
1774 
1780  public function endingTimeReached()
1781  {
1782  $this->tpl->setOnScreenMessage('info', sprintf($this->lng->txt("detail_ending_time_reached"), ilDatePresentation::formatDate(new ilDateTime($this->object->getEndingTime(), IL_CAL_UNIX))));
1783  $this->test_session->increasePass();
1784  $this->test_session->setLastSequence(0);
1785  $this->test_session->saveToDb();
1786 
1787  $this->redirectBackCmd();
1788  }
1789 
1798  {
1799  $this->suspendTestCmd();
1800  }
1801 
1807  public function confirmSubmitAnswers()
1808  {
1809  $this->tpl->addBlockFile($this->getContentBlockName(), "adm_content", "tpl.il_as_tst_submit_answers_confirm.html", "components/ILIAS/Test");
1810  $this->tpl->setCurrentBlock("adm_content");
1811  if ($this->object->isTestFinished($this->test_session->getActiveId())) {
1812  $this->tpl->setCurrentBlock("not_submit_allowed");
1813  $this->tpl->setVariable("TEXT_ALREADY_SUBMITTED", $this->lng->txt("tst_already_submitted"));
1814  $this->tpl->setVariable("BTN_OK", $this->lng->txt("tst_show_answer_sheet"));
1815  } else {
1816  $this->tpl->setCurrentBlock("submit_allowed");
1817  $this->tpl->setVariable("TEXT_CONFIRM_SUBMIT_RESULTS", $this->lng->txt("tst_confirm_submit_answers"));
1818  $this->tpl->setVariable("BTN_OK", $this->lng->txt("tst_submit_results"));
1819  }
1820  $this->tpl->setVariable("BTN_BACK", $this->lng->txt("back"));
1821  $this->tpl->setVariable("FORMACTION", $this->ctrl->getFormAction($this, "finalSubmission"));
1822  $this->tpl->parseCurrentBlock();
1823  }
1824 
1825  private function outProcessingTime(int $active_id, bool $verbose): void
1826  {
1827  $starting_time = $this->object->getStartingTimeOfUser($active_id);
1828  $working_time = new WorkingTime(
1829  $this->lng,
1830  $this->ui_factory,
1831  $this->ui_renderer,
1832  $starting_time,
1833  $this->object->getProcessingTimeInSeconds($active_id)
1834  );
1835 
1836  $this->tpl->setCurrentBlock('enableprocessingtime');
1837  $this->tpl->setVariable('USER_WORKING_TIME_MESSAGE_BOX', $working_time->getMessageBox($verbose));
1838  $this->tpl->parseCurrentBlock();
1839 
1840  $working_time_js_template = $working_time->prepareWorkingTimeJsTemplate(
1841  $this->getObject(),
1842  getdate($starting_time),
1843  $this->ctrl->getLinkTarget($this, 'checkWorkingTime', '', true),
1844  $this->ctrl->getFormAction($this, ilTestPlayerCommands::REDIRECT_AFTER_QUESTION_LIST)
1845  );
1846 
1847  $this->tpl->addOnLoadCode($working_time_js_template->get());
1848  }
1849 
1857  public function checkWorkingTimeCmd(): void
1858  {
1859  $active_id = $this->test_session->getActiveId();
1860  echo (string) $this->object->getProcessingTimeInSeconds($active_id);
1861  exit;
1862  }
1863 
1864  protected function showSideList($current_sequence_element): void
1865  {
1866  $question_summary_data = $this->service->getQuestionSummaryData($this->test_sequence);
1867  $questions = [];
1868  $active = 0;
1869 
1870  foreach ($question_summary_data as $idx => $row) {
1871  $title = htmlspecialchars($row['title'], ENT_QUOTES, null, false);
1872  $description = '';
1873  if ($row['description'] !== '') {
1874  $description = htmlspecialchars($row['description'], ENT_QUOTES, null, false);
1875  }
1876 
1877  if (!$row['disabled']) {
1878  $this->ctrl->setParameter($this, 'pmode', '');
1879  $this->ctrl->setParameter($this, 'sequence', $row['sequence']);
1880  $action = $this->ctrl->getLinkTarget($this, ilTestPlayerCommands::SHOW_QUESTION);
1881  $this->ctrl->setParameter($this, 'pmode', ilTestPlayerAbstractGUI::PRESENTATION_MODE_VIEW);
1882  $this->ctrl->setParameter($this, 'sequence', $this->getCurrentSequenceElement($current_sequence_element));
1883  }
1884 
1886 
1887  if (
1888  ($row['worked_through'] || $row['isAnswered'])
1889  && $row['has_authorized_answer']
1890  ) {
1892  }
1893 
1894  $questions[] = $this->ui_factory->listing()->workflow()
1895  ->step($title, $description, $action)
1896  ->withStatus($status);
1897  $active = $row['sequence'] == $current_sequence_element ? $idx : $active;
1898  }
1899 
1900  $question_listing = $this->ui_factory->listing()->workflow()->linear(
1901  $this->lng->txt('mainbar_button_label_questionlist'),
1902  $questions
1903  )->withActive($active);
1904 
1905 
1906  $this->global_screen->tool()->context()->current()->addAdditionalData(
1908  $question_listing
1909  );
1910  }
1911 
1915  public function outQuestionSummaryCmd()
1916  {
1917  $this->help->setScreenIdComponent('tst');
1918  $this->help->setScreenId('assessment');
1919  $this->help->setSubScreenId('question_summary');
1920 
1921  $is_first_page = $this->testrequest->strVal('first') === '1';
1922 
1923  $this->tpl->addBlockFile(
1924  $this->getContentBlockName(),
1925  'adm_content',
1926  'tpl.il_as_tst_question_summary.html',
1927  'components/ILIAS/Test'
1928  );
1929 
1930  $this->global_screen->tool()->context()->current()->getAdditionalData()->replace(
1932  $this->getObject()->getTitle() . ' - ' . $this->lng->txt('question_summary')
1933  );
1934 
1935  $active_id = $this->test_session->getActiveId();
1936  $question_summary_data = $this->service->getQuestionSummaryData($this->test_sequence);
1937 
1938  $this->ctrl->setParameter($this, 'sequence', $this->testrequest->raw('sequence'));
1939 
1940  $table = new QuestionsOfAttemptTable(
1941  $this->lng,
1942  $this->ctrl,
1943  $this->ui_factory,
1944  new DataFactory(),
1945  $this->http,
1946  $this,
1947  $this->object,
1948  $question_summary_data
1949  );
1950  $this->tpl->setVariable('TABLE_LIST_OF_QUESTIONS', $this->ui_renderer->render($table->buildComponents($is_first_page)));
1951 
1952  if ($this->object->getEnableProcessingTime()) {
1953  $this->outProcessingTime($active_id, true);
1954  }
1955 
1956  if ($this->object->isShowExamIdInTestPassEnabled()) {
1957  $this->tpl->setCurrentBlock('exam_id_footer');
1958  $this->tpl->setVariable('EXAM_ID_VAL', ilObjTest::lookupExamId(
1959  $this->test_session->getActiveId(),
1960  $this->test_session->getPass(),
1961  $this->object->getId()
1962  ));
1963  $this->tpl->setVariable('EXAM_ID_TXT', $this->lng->txt('exam_id'));
1964  $this->tpl->parseCurrentBlock();
1965  }
1966  }
1967 
1968  public function backFromFinishingCmd()
1969  {
1970  $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
1971  }
1972 
1976  public function outCorrectSolution(): void
1977  {
1978  $this->tpl->addBlockFile("ADM_CONTENT", "adm_content", "tpl.il_as_tst_correct_solution.html", "components/ILIAS/Test");
1979 
1980  $this->tpl->setCurrentBlock("ContentStyle");
1981  $this->tpl->setVariable("LOCATION_CONTENT_STYLESHEET", ilObjStyleSheet::getContentStylePath(0));
1982  $this->tpl->parseCurrentBlock();
1983 
1984  $this->tpl->setCurrentBlock("SyntaxStyle");
1985  $this->tpl->setVariable("LOCATION_SYNTAX_STYLESHEET", ilObjStyleSheet::getSyntaxStylePath());
1986  $this->tpl->parseCurrentBlock();
1987 
1988  $this->tpl->addCss(ilUtil::getStyleSheetLocation("output", "test_print.css"), "print");
1989  if ($this->object->getShowSolutionAnswersOnly()) {
1990  $this->tpl->addCss(ilUtil::getStyleSheetLocation("output", "test_print_hide_content.css"), "print");
1991  }
1992 
1993  $this->tpl->setCurrentBlock("adm_content");
1994  $solution = $this->getCorrectSolutionOutput($this->testrequest->raw("evaluation"), $this->testrequest->raw("active_id"), $this->testrequest->raw("pass"));
1995  $this->tpl->setVariable("OUTPUT_SOLUTION", $solution);
1996  $this->tpl->setVariable("TEXT_BACK", $this->lng->txt("back"));
1997  $this->ctrl->saveParameter($this, "pass");
1998  $this->ctrl->saveParameter($this, "active_id");
1999  $this->tpl->setVariable("URL_BACK", $this->ctrl->getLinkTarget($this, "outUserResultsOverview"));
2000  $this->tpl->parseCurrentBlock();
2001  }
2002 
2012  public function showListOfAnswers($active_id, $pass = null, $top_data = "", $bottom_data = "")
2013  {
2014  $this->tpl->addBlockFile($this->getContentBlockName(), "adm_content", "tpl.il_as_tst_finish_list_of_answers.html", "components/ILIAS/Test");
2015 
2016  $result_array = $this->object->getTestResult(
2017  $active_id,
2018  $pass,
2019  false,
2020  !$this->getObjectiveOrientedContainer()->isObjectiveOrientedPresentationRequired()
2021  );
2022 
2023  $counter = 1;
2024  // output of questions with solutions
2025  foreach ($result_array as $question_data) {
2026  $question = $question_data["qid"];
2027  if (is_numeric($question)) {
2028  $this->tpl->setCurrentBlock("printview_question");
2029  $question_gui = $this->object->createQuestionGUI("", $question);
2030  $template = new ilTemplate("tpl.il_as_qpl_question_printview.html", true, true, "components/ILIAS/TestQuestionPool");
2031  $template->setVariable("COUNTER_QUESTION", $counter . ". ");
2032  $template->setVariable("QUESTION_TITLE", $question_gui->getObject()->getTitleForHTMLOutput());
2033 
2034  $show_question_only = ($this->object->getShowSolutionAnswersOnly()) ? true : false;
2035  $result_output = $question_gui->getSolutionOutput(
2036  $active_id,
2037  $pass,
2038  false,
2039  false,
2040  $show_question_only,
2041  $this->object->getShowSolutionFeedback()
2042  );
2043  $template->setVariable("SOLUTION_OUTPUT", $result_output);
2044  $this->tpl->setVariable("QUESTION_OUTPUT", $template->get());
2045  $this->tpl->parseCurrentBlock();
2046  $counter++;
2047  }
2048  }
2049 
2050  $this->tpl->addCss(ilUtil::getStyleSheetLocation("output", "test_print.css"), "print");
2051  if ($this->object->getShowSolutionAnswersOnly()) {
2052  $this->tpl->addCss(ilUtil::getStyleSheetLocation("output", "test_print_hide_content.css"), "print");
2053  }
2054  if (strlen($top_data)) {
2055  $this->tpl->setCurrentBlock("top_data");
2056  $this->tpl->setVariable("TOP_DATA", $top_data);
2057  $this->tpl->parseCurrentBlock();
2058  }
2059 
2060  if (strlen($bottom_data)) {
2061  $this->tpl->setCurrentBlock("bottom_data");
2062  $this->tpl->setVariable("FORMACTION", $this->ctrl->getFormAction($this));
2063  $this->tpl->setVariable("BOTTOM_DATA", $bottom_data);
2064  $this->tpl->parseCurrentBlock();
2065  }
2066 
2067  $this->tpl->setCurrentBlock("adm_content");
2068  $this->tpl->setVariable("TXT_ANSWER_SHEET", $this->lng->txt("tst_list_of_answers"));
2069  $user_data = $this->getAdditionalUsrDataHtmlAndPopulateWindowTitle($this->test_session, $active_id, true);
2070  $signature = $this->getResultsSignature();
2071  $this->tpl->setVariable("USER_DETAILS", $user_data);
2072  $this->tpl->setVariable("SIGNATURE", $signature);
2073  $this->tpl->setVariable("TITLE", $this->object->getTitle());
2074  $this->tpl->setVariable("TXT_TEST_PROLOG", $this->lng->txt("tst_your_answers"));
2075  $invited_user = &$this->object->getInvitedUsers($this->user->getId());
2076  $pagetitle = $this->object->getTitle() . ' - ' . $this->lng->txt('clientip') .
2077  ': ' . $_SERVER['REMOTE_ADDR'] . ' - ' .
2078  $this->lng->txt('matriculation') . ': ' .
2079  $invited_user[$this->user->getId()]['matriculation'];
2080  $this->tpl->setVariable('PAGETITLE', $pagetitle);
2081  $this->tpl->parseCurrentBlock();
2082  }
2083 
2089  public function getContentBlockName(): string
2090  {
2091  return "ADM_CONTENT";
2092 
2093  if ($this->object->getKioskMode()) {
2094  $this->tpl->setBodyClass("kiosk");
2095  $this->tpl->hideFooter();
2096  return "CONTENT";
2097  } else {
2098  return "ADM_CONTENT";
2099  }
2100  }
2101 
2102  public function outUserResultsOverviewCmd()
2103  {
2104  $this->ctrl->redirectByClass(
2105  [ilRepositoryGUI::class, ilObjTestGUI::class, ilTestEvaluationGUI::class],
2106  "outUserResultsOverview"
2107  );
2108  }
2109 
2113  protected function showRequestedHintListCmd()
2114  {
2115  // fau: testNav - handle intermediate submit for viewing requested hints
2116  $this->handleIntermediateSubmit();
2117  // fau.
2118 
2119  $this->ctrl->setParameter($this, 'pmode', self::PRESENTATION_MODE_EDIT);
2120 
2121  $this->ctrl->redirectByClass('ilAssQuestionHintRequestGUI', ilAssQuestionHintRequestGUI::CMD_SHOW_LIST);
2122  }
2123 
2127  protected function confirmHintRequestCmd()
2128  {
2129  // fau: testNav - handle intermediate submit for confirming hint requests
2130  $this->handleIntermediateSubmit();
2131  // fau.
2132 
2133  $this->ctrl->setParameter($this, 'pmode', self::PRESENTATION_MODE_EDIT);
2134 
2135  $this->ctrl->redirectByClass('ilAssQuestionHintRequestGUI', ilAssQuestionHintRequestGUI::CMD_CONFIRM_REQUEST);
2136  }
2137 
2138  protected function isFirstQuestionInSequence($sequence_element): bool
2139  {
2140  return $sequence_element == $this->test_sequence->getFirstSequence();
2141  }
2142 
2143  protected function isLastQuestionInSequence($sequence_element): bool
2144  {
2145  return $sequence_element == $this->test_sequence->getLastSequence();
2146  }
2147 
2148  protected function handleQuestionActionCmd()
2149  {
2150  $question_id = $this->test_sequence->getQuestionForSequence(
2151  $this->getCurrentSequenceElement()
2152  );
2153 
2154  if (!$this->isParticipantsAnswerFixed($question_id)) {
2155  $this->updateWorkingTime();
2156  $this->saveQuestionSolution(false);
2157  // fau: testNav - add changed status of the question
2158  $this->setAnswerChangedParameter(true);
2159  // fau.
2160  }
2161 
2162  $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
2163  }
2164 
2165  protected function showInstantResponseCmd()
2166  {
2167  $question_id = $this->test_sequence->getQuestionForSequence(
2168  $this->getCurrentSequenceElement()
2169  );
2170 
2171  if (!$this->isParticipantsAnswerFixed($question_id)) {
2172  if ($this->saveQuestionSolution(true)) {
2173  $this->removeIntermediateSolution();
2174  $this->setAnswerChangedParameter(false);
2175  } else {
2176  $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
2177  }
2178  $this->test_sequence->setQuestionChecked($question_id);
2179  $this->test_sequence->saveToDb();
2180  } elseif ($this->object->isForceInstantFeedbackEnabled()) {
2181  $this->test_sequence->setQuestionChecked($question_id);
2182  $this->test_sequence->saveToDb();
2183  }
2184 
2185  $this->ctrl->setParameter($this, 'instresp', 1);
2186 
2187  // fau: testNav - handle navigation after feedback
2188  if ($this->getNavigationUrlParameter()) {
2191  }
2192  // fau.
2193  $this->ctrl->redirectByClass(static::class, ilTestPlayerCommands::SHOW_QUESTION);
2194  }
2195 
2196  protected function nextQuestionCmd()
2197  {
2198  $this->handleCheckTestPassValid();
2199  $last_sequence_element = $this->getCurrentSequenceElement();
2200  $next_sequence_element = $this->test_sequence->getNextSequence($last_sequence_element);
2201 
2202  $question_id = $this->test_sequence->getQuestionForSequence($last_sequence_element);
2203  $is_worked_through = $this->questionrepository->lookupResultRecordExist(
2204  $this->test_session->getActiveId(),
2205  $question_id,
2206  $this->test_session->getPass()
2207  );
2208 
2209  if (!$is_worked_through) {
2210  if ($this->logger->isLoggingEnabled()
2211  && !$this->getObject()->getAnonymity()) {
2212  $this->logger->logParticipantInteraction(
2213  $this->logger->getInteractionFactory()->buildParticipantInteraction(
2214  $this->object->getRefId(),
2215  $question_id,
2216  $this->user->getId(),
2217  $this->logger->isIPLoggingEnabled() ? $_SERVER['REMOTE_ADDR'] : '',
2218  TestParticipantInteractionTypes::QUESTION_SKIPPED,
2219  []
2220  )
2221  );
2222  }
2223  if ($this->object->isPostponingEnabled()) {
2224  $this->handleQuestionPostponing($question_id);
2225  }
2226  }
2227 
2228  if (!$this->isValidSequenceElement($next_sequence_element)) {
2229  $next_sequence_element = $this->test_sequence->getFirstSequence();
2230  }
2231 
2232  $this->ctrl->setParameter($this, 'sequence', $next_sequence_element);
2233  $this->ctrl->setParameter($this, 'pmode', '');
2234 
2235  $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
2236  }
2237 
2238  protected function previousQuestionCmd()
2239  {
2240  $this->handleCheckTestPassValid();
2241 
2242  $sequence_element = $this->test_sequence->getPreviousSequence(
2243  $this->getCurrentSequenceElement()
2244  );
2245 
2246  if (!$this->isValidSequenceElement($sequence_element)) {
2247  $sequence_element = $this->test_sequence->getLastSequence();
2248  }
2249 
2250  $this->ctrl->setParameter($this, 'sequence', $sequence_element);
2251  $this->ctrl->setParameter($this, 'pmode', '');
2252 
2253  $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
2254  }
2255 
2256  protected function prepareSummaryPage()
2257  {
2258  $this->tpl->addBlockFile(
2259  $this->getContentBlockName(),
2260  'adm_content',
2261  'tpl.il_as_tst_question_summary.html',
2262  'components/ILIAS/Test'
2263  );
2264  }
2265 
2266  protected function initTestPageTemplate()
2267  {
2268  $onload_js = <<<JS
2269  let key_event = (event) => {
2270  if( event.key === 13 && event.target.tagName.toLowerCase() === "a" ) {
2271  return;
2272  }
2273  if (event.key === 13 &&
2274  event.target.tagName.toLowerCase() !== "textarea" &&
2275  (event.target.tagName.toLowerCase() !== "input" || event.target.type.toLowerCase() !== "submit")) {
2276  event.preventDefault();
2277  }
2278  };
2279 
2280  let form = document.getElementById('taForm');
2281  form.onkeyup = key_event;
2282  form.onkeydown = key_event;
2283  form.onkeypress = key_event;
2284 JS;
2285  $this->tpl->addOnLoadCode($onload_js);
2286  $this->tpl->addBlockFile(
2287  $this->getContentBlockName(),
2288  'adm_content',
2289  'tpl.il_as_tst_output.html',
2290  'components/ILIAS/Test'
2291  );
2292  }
2293 
2295  {
2302  if ($this->test_session->isPasswordChecked() === true) {
2303  return;
2304  }
2305 
2306  if ($this->ctrl->getNextClass() === 'iltestpasswordprotectiongui') {
2307  return;
2308  }
2309 
2310  if (!$this->password_checker->isPasswordProtectionPageRedirectRequired()) {
2311  $this->test_session->setPasswordChecked(true);
2312  return;
2313  }
2314 
2315  $this->ctrl->setParameterByClass(self::class, 'lock', $this->getLockParameter());
2316 
2317  $next_command = $this->ctrl->getCmdClass() . '::' . ilTestPlayerCommands::START_TEST;
2318  $this->ctrl->setParameterByClass(ilTestPasswordProtectionGUI::class, 'nextCommand', $next_command);
2319  $this->ctrl->redirectByClass(ilTestPasswordProtectionGUI::class, 'showPasswordForm');
2320  }
2321 
2322  protected function isParticipantsAnswerFixed($question_id): bool
2323  {
2324  if ($this->object->isInstantFeedbackAnswerFixationEnabled()) {
2325  return $this->test_sequence->isQuestionChecked($question_id);
2326  }
2327 
2328  if ($this->object->isFollowupQuestionAnswerFixationEnabled()) {
2329  return $this->isForcedFeedbackNavUrlRegistered() || $this->test_sequence->isNextQuestionPresented($question_id);
2330  }
2331 
2332  return false;
2333  }
2334 
2338  protected function getIntroductionPageButtonLabel(): string
2339  {
2340  return $this->lng->txt("save_introduction");
2341  }
2342 
2343  protected function initAssessmentSettings()
2344  {
2345  $this->ass_settings = new ilSetting('assessment');
2346  }
2347 
2351  protected function handleSkillTriggering(ilTestSession $test_session): void
2352  {
2353  $skill_evaluation = new ilTestSkillEvaluation(
2354  $this->db,
2355  $this->logger,
2356  $this->object->getTestId(),
2357  $this->object->getRefId(),
2358  $this->skills_service->profile(),
2359  $this->skills_service->personal()
2360  );
2361 
2362  $skill_evaluation->setUserId($test_session->getUserId());
2363  $skill_evaluation->setActiveId($test_session->getActiveId());
2364  $skill_evaluation->setPass($test_session->getPass());
2365 
2366  $skill_evaluation->setNumRequiredBookingsForSkillTriggering(
2367  $this->object->getGlobalSettings()->getSkillTriggeringNumberOfAnswers()
2368  );
2369 
2370  $question_list = $this->buildTestPassQuestionList();
2371  $question_list->load();
2372  $skill_evaluation->init($question_list);
2373  $skill_evaluation->evaluate(
2374  $this->object->getTestResult(
2375  $test_session->getActiveId(),
2376  $test_session->getPass(),
2377  true
2378  )
2379  );
2380 
2381  $skill_evaluation->handleSkillTriggering();
2382  }
2383 
2385  {
2386  $confirmation = new ilTestAnswerOptionalQuestionsConfirmationGUI($this->lng);
2387 
2388  $confirmation->setFormAction($this->ctrl->getFormAction($this));
2389  $confirmation->setCancelCmd('cancelAnswerOptionalQuestions');
2390  $confirmation->setConfirmCmd('confirmAnswerOptionalQuestions');
2391 
2392  $confirmation->build($this->object->isFixedTest());
2393 
2394  $this->populateHelperGuiContent($confirmation);
2395  }
2396 
2398  {
2399  $this->test_sequence->setAnsweringOptionalQuestionsConfirmed(true);
2400  $this->test_sequence->saveToDb();
2401 
2402  $this->ctrl->setParameter($this, 'activecommand', 'gotoquestion');
2403  $this->ctrl->redirect($this, 'redirectQuestion');
2404  }
2405 
2407  {
2408  if ($this->object->getListOfQuestions()) {
2409  $this->ctrl->setParameter($this, 'activecommand', 'summary');
2410  } else {
2411  $this->ctrl->setParameter($this, 'activecommand', 'previous');
2412  }
2413 
2414  $this->ctrl->redirect($this, 'redirectQuestion');
2415  }
2416 
2420  protected function populateHelperGuiContent($helperGui)
2421  {
2422  $this->tpl->setVariable($this->getContentBlockName(), $this->ctrl->getHTML($helperGui));
2423  }
2424 
2426  {
2427  $navigation_toolbar = new ilTestNavigationToolbarGUI($this->ctrl, $this);
2428  $navigation_toolbar->setSuspendTestButtonEnabled($this->object->getShowCancel());
2429  $navigation_toolbar->setUserPassOverviewEnabled($this->object->getUsrPassOverviewEnabled());
2430  $navigation_toolbar->setFinishTestCommand($this->getFinishTestCommand());
2431  return $navigation_toolbar;
2432  }
2433 
2435  {
2436  $navigationGUI = new ilTestQuestionNavigationGUI(
2437  $this->lng,
2438  $this->ui_factory,
2439  $this->ui_renderer
2440  );
2441 
2442  if (!$this->isParticipantsAnswerFixed($question_id)) {
2443  $navigationGUI->setEditSolutionCommand(ilTestPlayerCommands::EDIT_SOLUTION);
2444  }
2445 
2446  if ($this->object->getShowMarker()) {
2447  $solved_array = ilObjTest::_getSolvedQuestions($this->test_session->getActiveId(), $question_id);
2448  $solved = 0;
2449 
2450  if (count($solved_array) > 0) {
2451  $solved = array_pop($solved_array);
2452  $solved = $solved["solved"];
2453  }
2454  // fau: testNav - change question mark command to link target
2455  if ($solved == 1) {
2456  $navigationGUI->setQuestionMarkLinkTarget($this->ctrl->getLinkTarget($this, ilTestPlayerCommands::UNMARK_QUESTION));
2457  $navigationGUI->setQuestionMarked(true);
2458  } else {
2459  $navigationGUI->setQuestionMarkLinkTarget($this->ctrl->getLinkTarget($this, ilTestPlayerCommands::MARK_QUESTION));
2460  $navigationGUI->setQuestionMarked(false);
2461  }
2462  }
2463  // fau.
2464 
2465  return $navigationGUI;
2466  }
2467 
2469  {
2470  $navigation_gui = new ilTestQuestionNavigationGUI(
2471  $this->lng,
2472  $this->ui_factory,
2473  $this->ui_renderer
2474  );
2475 
2476  // fau: testNav - add a 'revert changes' link for editable question
2477  $navigation_gui->setRevertChangesLinkTarget($this->ctrl->getLinkTarget($this, ilTestPlayerCommands::REVERT_CHANGES));
2478 
2479  if ($this->object->getSpecificAnswerFeedback()
2480  || $this->object->getGenericAnswerFeedback()
2481  || $this->object->getAnswerFeedbackPoints()
2482  || $this->object->getInstantFeedbackSolution()) {
2483  $navigation_gui->setAnswerFreezingEnabled($this->object->isInstantFeedbackAnswerFixationEnabled());
2484 
2485  if ($this->object->isForceInstantFeedbackEnabled()) {
2486  $navigation_gui->setForceInstantResponseEnabled(true);
2487  $navigation_gui->setInstantFeedbackCommand(ilTestPlayerCommands::SUBMIT_SOLUTION);
2488  } else {
2489  $navigation_gui->setInstantFeedbackCommand(ilTestPlayerCommands::SHOW_INSTANT_RESPONSE);
2490  }
2491  }
2492 
2493  // hints
2494  if ($this->object->isOfferingQuestionHintsEnabled()) {
2495  $activeId = $this->test_session->getActiveId();
2496  $pass = $this->test_session->getPass();
2497 
2498  $questionHintTracking = new ilAssQuestionHintTracking($question_id, $activeId, $pass);
2499 
2500  if ($questionHintTracking->requestsPossible()) {
2501  $navigation_gui->setRequestHintCommand(ilTestPlayerCommands::CONFIRM_HINT_REQUEST);
2502  }
2503 
2504  if ($questionHintTracking->requestsExist()) {
2505  $navigation_gui->setShowHintsCommand(ilTestPlayerCommands::SHOW_REQUESTED_HINTS_LIST);
2506  }
2507  }
2508 
2509  if ($this->object->getShowMarker()) {
2510  $solved_array = ilObjTest::_getSolvedQuestions($this->test_session->getActiveId(), $question_id);
2511  $solved = 0;
2512 
2513  if (count($solved_array) > 0) {
2514  $solved = array_pop($solved_array);
2515  $solved = $solved['solved'];
2516  }
2517 
2518  if ($solved === 1) {
2519  $navigation_gui->setQuestionMarkLinkTarget($this->ctrl->getLinkTarget($this, ilTestPlayerCommands::UNMARK_QUESTION_SAVE));
2520  $navigation_gui->setQuestionMarked(true);
2521  } else {
2522  $navigation_gui->setQuestionMarkLinkTarget($this->ctrl->getLinkTarget($this, ilTestPlayerCommands::MARK_QUESTION_SAVE));
2523  $navigation_gui->setQuestionMarked(false);
2524  }
2525  }
2526  return $navigation_gui;
2527  }
2528 
2529  protected function getFinishTestCommand(): string
2530  {
2531  if (!$this->object->getListOfQuestionsEnd()) {
2533  }
2534 
2536  }
2537 
2538  protected function populateInstantResponseModal(assQuestionGUI $question_gui, $nav_url): void
2539  {
2540  $question_gui->setNavigationGUI(null);
2541  $question_gui->getQuestionHeaderBlockBuilder()->setQuestionAnswered(true);
2542 
2543  $answer_feedback_enabled = $this->object->getSpecificAnswerFeedback();
2544 
2545  $solutionoutput = $question_gui->getSolutionOutput(
2546  $this->test_session->getActiveId(), #active_id
2547  $this->test_session->getPass(), #pass
2548  false, #graphical_output
2549  false, #result_output
2550  true, #show_question_only
2551  $answer_feedback_enabled, #show_feedback
2552  false, #show_correct_solution
2553  false, #show_manual_scoring
2554  true #show_question_text
2555  );
2556 
2557  $pageoutput = $question_gui->outQuestionPage(
2558  "",
2559  $this->isShowingPostponeStatusReguired($question_gui->getObject()->getId()),
2560  $this->test_session->getActiveId(),
2561  $solutionoutput
2562  );
2563 
2564  $tpl = new ilTemplate('tpl.tst_player_response_modal.html', true, true, 'components/ILIAS/Test');
2565 
2566  // populate the instant response blocks in the
2567  $saved_tpl = $this->tpl;
2568  $this->tpl = $tpl;
2569  $this->populateInstantResponseBlocks($question_gui, true);
2570  $this->tpl = $saved_tpl;
2571 
2572  $tpl->setVariable('QUESTION_OUTPUT', $pageoutput);
2573  $this->tpl->setVariable('INSTANT_RESPONSE_MODAL', $this->getQuestionFeedbackModalHtml($tpl, $nav_url));
2574  }
2575 
2576  private function getQuestionFeedbackModalHtml(ilTemplate $tpl, string $nav_url): string
2577  {
2578  $modal = $this->ui_factory->modal()->roundtrip(
2579  $this->lng->txt('tst_instant_feedback'),
2580  $this->ui_factory->legacy($tpl->get()),
2581  []
2582  )->withActionButtons([
2583  $this->ui_factory->button()->standard($this->lng->txt('proceed'), $nav_url)
2584  ]);
2585 
2586  return $this->ui_renderer->render([
2587  $modal->withOnLoad($modal->getShowSignal())
2588  ]);
2589  }
2590  // fau;
2591 
2595  protected function populateInstantResponseBlocks(assQuestionGUI $question_gui, $authorizedSolution)
2596  {
2597  $response_available = false;
2598  $jump_to_response = false;
2599 
2600  // This controls if the solution should be shown.
2601  // It gets the parameter "Scoring and Results" -> "Instant Feedback" -> "Show Solutions"
2602  if ($this->object->getInstantFeedbackSolution()) {
2603  $show_question_inline_score = $this->determineInlineScoreDisplay();
2604 
2605  // Notation of the params prior to getting rid of this crap in favor of a class
2606  $solutionoutput = $question_gui->getSolutionOutput(
2607  $this->test_session->getActiveId(), #active_id
2608  $this->test_session->getPass(), #pass
2609  false, #graphical_output
2610  $show_question_inline_score, #result_output
2611  true, #show_question_only
2612  false, #show_feedback
2613  true, #show_correct_solution
2614  false, #show_manual_scoring
2615  false #show_question_text
2616  );
2617  $solutionoutput = str_replace('<h1 class="ilc_page_title_PageTitle"></h1>', '', $solutionoutput);
2618  $this->populateSolutionBlock($solutionoutput);
2619  $response_available = true;
2620  $jump_to_response = true;
2621  }
2622 
2623  $reachedPoints = $question_gui->getObject()->getAdjustedReachedPoints(
2624  $this->test_session->getActiveId(),
2625  ilObjTest::_getPass($this->test_session->getActiveId()),
2626  $authorizedSolution
2627  );
2628 
2629  $maxPoints = $question_gui->getObject()->getMaximumPoints();
2630 
2631  $solutionCorrect = ($reachedPoints == $maxPoints);
2632 
2633  // This controls if the score should be shown.
2634  // It gets the parameter "Scoring and Results" -> "Instant Feedback" -> "Show Results (Only Points)"
2635  if ($this->object->getAnswerFeedbackPoints()) {
2636  $this->populateScoreBlock($reachedPoints, $maxPoints);
2637  $response_available = true;
2638  $jump_to_response = true;
2639  }
2640 
2641  // This controls if the generic feedback should be shown.
2642  // It gets the parameter "Scoring and Results" -> "Instant Feedback" -> "Show Solutions"
2643  if ($this->object->getGenericAnswerFeedback()) {
2644  if ($this->populateGenericFeedbackBlock($question_gui, $solutionCorrect)) {
2645  $response_available = true;
2646  $jump_to_response = true;
2647  }
2648  }
2649 
2650  // This controls if the specific feedback should be shown.
2651  // It gets the parameter "Scoring and Results" -> "Instant Feedback" -> "Show Answer-Specific Feedback"
2652  if ($this->object->getSpecificAnswerFeedback()) {
2653  if ($question_gui->hasInlineFeedback()) {
2654  // Don't jump to the feedback below the question if some feedback is shown within the question
2655  $jump_to_response = false;
2656  } elseif ($this->populateSpecificFeedbackBlock($question_gui)) {
2657  $response_available = true;
2658  $jump_to_response = true;
2659  }
2660  }
2661 
2662  $this->populateFeedbackBlockHeader($jump_to_response);
2663  if (!$response_available) {
2664  if ($question_gui->hasInlineFeedback()) {
2665  $this->populateFeedbackBlockMessage($this->lng->txt('tst_feedback_is_given_inline'));
2666  } else {
2667  $this->populateFeedbackBlockMessage($this->lng->txt('tst_feedback_not_available_for_answer'));
2668  }
2669  }
2670  }
2671 
2672  protected function populateFeedbackBlockHeader($withFocusAnchor)
2673  {
2674  if ($withFocusAnchor) {
2675  $this->tpl->setCurrentBlock('inst_resp_id');
2676  $this->tpl->setVariable('INSTANT_RESPONSE_FOCUS_ID', 'focus');
2677  $this->tpl->parseCurrentBlock();
2678  }
2679 
2680  $this->tpl->setCurrentBlock('instant_response_header');
2681  $this->tpl->setVariable('INSTANT_RESPONSE_HEADER', $this->lng->txt('tst_feedback'));
2682  $this->tpl->parseCurrentBlock();
2683  }
2684 
2685  protected function populateFeedbackBlockMessage(string $a_message)
2686  {
2687  $this->tpl->setCurrentBlock('instant_response_message');
2688  $this->tpl->setVariable('INSTANT_RESPONSE_MESSAGE', $a_message);
2689  $this->tpl->parseCurrentBlock();
2690  }
2691 
2692 
2693  protected function getCurrentSequenceElement(): int
2694  {
2695  if ($this->getSequenceElementParameter()) {
2696  return $this->getSequenceElementParameter();
2697  }
2698 
2699  return $this->test_session->getLastSequence();
2700  }
2701 
2702  protected function getSequenceElementParameter(): ?int
2703  {
2704  if ($this->testrequest->isset('sequence')) {
2705  return $this->testrequest->int('sequence');
2706  }
2707 
2708  return null;
2709  }
2710 
2711  protected function getPresentationModeParameter()
2712  {
2713  if ($this->testrequest->isset('pmode')) {
2714  return $this->testrequest->raw('pmode');
2715  }
2716 
2717  return null;
2718  }
2719 
2720  protected function getInstantResponseParameter(): bool
2721  {
2722  return $this->testrequest->isInstanceResponseRequested();
2723  }
2724 
2725  protected function getNextCommandParameter()
2726  {
2727  $nextcmd = '';
2728  if ($this->testrequest->isset('nextcmd')) {
2729  $nextcmd = $this->testrequest->strVal('nextcmd');
2730  }
2731 
2732  return $nextcmd !== '' ? $nextcmd : null;
2733  }
2734 
2735  protected function getNextSequenceParameter(): int
2736  {
2737  return $this->testrequest->int('nextseq');
2738  }
2739 
2740  protected function getNavigationUrlParameter(): string
2741  {
2742  $navigation_url = $this->testrequest->strVal('test_player_navigation_url');
2743  if ($navigation_url !== '') {
2744  $navigation_url_parts = parse_url($navigation_url);
2745  $ilias_url_parts = parse_url(ilUtil::_getHttpPath());
2746 
2747  if (!isset($navigation_url_parts['host']) || ($ilias_url_parts['host'] === $navigation_url_parts['host'])) {
2748  return $navigation_url;
2749  }
2750  }
2751  return '';
2752  }
2753 
2754  protected function getAnswerChangedParameter(): bool
2755  {
2756  return !empty($this->testrequest->raw('test_answer_changed'));
2757  }
2758 
2759  protected function setAnswerChangedParameter(bool $changed = true)
2760  {
2761  $this->ctrl->setParameter($this, 'test_answer_changed', $changed ? '1' : '0');
2762  }
2763 
2764  protected function handleIntermediateSubmit()
2765  {
2766  if ($this->getAnswerChangedParameter()) {
2767  $this->saveQuestionSolution(false);
2768  } else {
2769  $this->removeIntermediateSolution();
2770  }
2772  }
2773 
2774  protected function saveNavigationPreventConfirmation(): void
2775  {
2776  if ($this->testrequest->retrieveBoolFromPost('save_on_navigation_prevent_confirmation')) {
2777  ilSession::set('save_on_navigation_prevent_confirmation', true);
2778  }
2779 
2780  if ($this->testrequest->retrieveBoolFromPost(self::FOLLOWUP_QST_LOCKS_PREVENT_CONFIRMATION_PARAM)) {
2781  ilSession::set(self::FOLLOWUP_QST_LOCKS_PREVENT_CONFIRMATION_PARAM, true);
2782  }
2783  }
2784 
2785  protected function getQuestionGuiInstance(int $question_id, bool $from_cache = true): object
2786  {
2787  $tpl = $this->tpl;
2788 
2789  if (!$from_cache || !isset($this->cached_question_guis[$question_id])) {
2790  $question_gui = $this->object->createQuestionGUI("", $question_id);
2791  $question_gui->setTargetGui($this);
2792  $question_gui->setPresentationContext(assQuestionGUI::PRESENTATION_CONTEXT_TEST);
2793  $question = $question_gui->getObject();
2794  $question->setShuffler($this->shuffler->getAnswerShuffleFor(
2795  $question_id,
2796  $this->test_session->getActiveId(),
2797  $this->test_session->getPass()
2798  ));
2799  $question_gui->setObject($question);
2800  $question_gui->populateJavascriptFilesRequiredForWorkForm($tpl);
2801 
2802  // hey: prevPassSolutions - determine solution pass index and configure gui accordingly
2803  $this->initTestQuestionConfig($question_gui->getObject());
2804  // hey.
2805 
2806  $this->cached_question_guis[$question_id] = $question_gui;
2807  }
2808 
2809  return $this->cached_question_guis[$question_id];
2810  }
2811 
2812  protected function getQuestionInstance(int $question_id, bool $from_cache = true): assQuestion
2813  {
2814  if ($from_cache && isset($this->cached_question_objects[$question_id])) {
2815  return $this->cached_question_objects[$question_id];
2816  }
2817  $question = assQuestion::instantiateQuestion($question_id);
2818  $ass_settings = new ilSetting('assessment');
2819 
2820  $process_locker_factory = new ilAssQuestionProcessLockerFactory($ass_settings, $this->db);
2821  $process_locker_factory->setQuestionId($question->getId());
2822  $process_locker_factory->setUserId($this->user->getId());
2823  $question->setProcessLocker($process_locker_factory->getLocker());
2824 
2825  $this->initTestQuestionConfig($question);
2826 
2827  $this->cached_question_objects[$question_id] = $question;
2828  return $question;
2829  }
2830 
2831  protected function initTestQuestionConfig(assQuestion $question_obj)
2832  {
2833  $question_obj->getTestPresentationConfig()->setPreviousPassSolutionReuseAllowed(
2834  $this->object->isPreviousSolutionReuseEnabled($this->test_session->getActiveId())
2835  );
2836  }
2837 
2838  protected function handleTearsAndAngerQuestionIsNull(int $question_id, $sequence_element): void
2839  {
2840  $this->logger->error(
2841  "INV SEQ:"
2842  . "active={$this->test_session->getActiveId()} "
2843  . "qId=$question_id seq=$sequence_element "
2844  . serialize($this->test_sequence)
2845  );
2846 
2847  $this->ctrl->setParameter($this, 'sequence', $this->test_sequence->getFirstSequence());
2848  $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
2849  }
2850 
2851  protected function populateMessageContent(string $content_html): void
2852  {
2853  if ($this->object->getKioskMode()) {
2854  $this->tpl->addBlockfile($this->getContentBlockName(), 'content', "tpl.il_as_tst_kiosk_mode_content.html", "components/ILIAS/Test");
2855  $this->tpl->setContent($content_html);
2856  return;
2857  }
2858  $this->tpl->setVariable($this->getContentBlockName(), $content_html);
2859  }
2860 
2864  protected function populateModals(): array
2865  {
2866  $signals = [self::DISCARD_MODAL => $this->populateDiscardSolutionModal()];
2867 
2868  if ($this->object->isFollowupQuestionAnswerFixationEnabled()) {
2869  $signals[self::LOCKS_CHANGED_MODAL] = $this->populateNextLocksChangedModal();
2870  $signals[self::LOCKS_UNCHANGED_MODAL] = $this->populateNextLocksUnchangedModal();
2871  }
2872 
2873  return $signals;
2874  }
2875 
2876  protected function populateDiscardSolutionModal(): Signal
2877  {
2878  $modal = $this->ui_factory->modal()->interruptive(
2879  $this->lng->txt('discard_answer'),
2880  $this->lng->txt('discard_answer_confirmation'),
2881  $this->ctrl->getLinkTarget($this, ilTestPlayerCommands::DISCARD_SOLUTION)
2882  )->withActionButtonLabel($this->lng->txt('discard_answer'));
2883 
2884  $this->tpl->setCurrentBlock('discard_solution_modal');
2885  $this->tpl->setVariable('DISCARD_SOLUTION_MODAL', $this->ui_renderer->render($modal));
2886  $this->tpl->parseCurrentBlock();
2887  return $modal->getShowSignal();
2888  }
2889 
2891  {
2892  $modal = $this->ui_factory->modal()->interruptive(
2893  $this->lng->txt('tst_nav_next_locks_empty_answer_header'),
2894  $this->lng->txt('tst_nav_next_locks_empty_answer_confirm'),
2895  'javascript:il.TestPlayerQuestionEditControl.confirmNextLocksUnchanged()'
2896  )->withActionButtonLabel($this->lng->txt('tst_proceed'));
2897 
2898  $this->tpl->setCurrentBlock('next_locks_unchanged_modal');
2899  $this->tpl->setVariable('NEXT_LOCKS_UNCHANGED_MODAL', $this->ui_renderer->render($modal));
2900  $this->tpl->parseCurrentBlock();
2901  return $modal->getShowSignal();
2902  }
2903 
2905  {
2906  $empty_signal = new \ILIAS\UI\Implementation\Component\Signal('');
2908  return $empty_signal;
2909  }
2910 
2911  $modal_message = $this->ui_factory->messageBox()->confirmation(
2912  $this->lng->txt('tst_nav_next_locks_current_answer_confirm')
2913  );
2914  $modal_checkbox = $this->ui_factory->input()->field()->checkbox(
2915  $this->lng->txt('tst_dont_show_msg_again_in_current_session')
2916  )->withDedicatedName(self::FOLLOWUP_QST_LOCKS_PREVENT_CONFIRMATION_PARAM);
2917 
2918  $modal = $this->ui_factory->modal()->roundtrip(
2919  $this->lng->txt('tst_nav_next_locks_current_answer_header'),
2920  $modal_message,
2921  [ $modal_checkbox ],
2922  'javascript:il.TestPlayerQuestionEditControl.confirmNextLocksChanged()'
2923  )->withSubmitLabel($this->lng->txt('tst_proceed'));
2924 
2925  $this->tpl->setCurrentBlock('next_locks_changed_modal');
2926  $this->tpl->setVariable('NEXT_LOCKS_CHANGED_MODAL', $this->ui_renderer->render($modal));
2927  $this->tpl->parseCurrentBlock();
2928  return $modal->getShowSignal();
2929  }
2930 
2931  public const FOLLOWUP_QST_LOCKS_PREVENT_CONFIRMATION_PARAM = 'followup_qst_locks_prevent_confirmation';
2932 
2934  {
2935  ilSession::set(self::FOLLOWUP_QST_LOCKS_PREVENT_CONFIRMATION_PARAM, true);
2936  }
2937 
2939  {
2940  if (ilSession::get(self::FOLLOWUP_QST_LOCKS_PREVENT_CONFIRMATION_PARAM) == null) {
2941  return false;
2942  }
2943 
2944  return ilSession::get(self::FOLLOWUP_QST_LOCKS_PREVENT_CONFIRMATION_PARAM);
2945  }
2946 
2947  protected function populateQuestionEditControl(assQuestionGUI $question_gui): void
2948  {
2949  $config = [];
2950  $state = $question_gui->getObject()->lookupForExistingSolutions($this->test_session->getActiveId(), $this->test_session->getPass());
2951  $config['isAnswered'] = $state['authorized'];
2952  $config['isAnswerChanged'] = $state['intermediate'] || $this->getAnswerChangedParameter();
2953  $config['isAnswerFixed'] = $this->isParticipantsAnswerFixed($question_gui->getObject()->getId());
2954  $config['saveOnTimeReachedUrl'] = str_replace('&amp;', '&', $this->ctrl->getFormAction($this, ilTestPlayerCommands::AUTO_SAVE_ON_TIME_LIMIT));
2955 
2956  $config['autosaveUrl'] = '';
2957  $config['autosaveInterval'] = 0;
2958  if ($question_gui->getObject() instanceof QuestionAutosaveable && $this->object->getAutosave()) {
2959  $config['autosaveUrl'] = $this->ctrl->getLinkTarget($this, ilTestPlayerCommands::AUTO_SAVE, '', true);
2960  $config['autosaveInterval'] = $this->object->getMainSettings()->getQuestionBehaviourSettings()->getAutosaveInterval();
2961  }
2962 
2963  $question_config = $question_gui->getObject()->getTestPresentationConfig();
2964 
2965  $config['withFormChangeDetection'] = $question_config->isFormChangeDetectionEnabled();
2966 
2967  // Flash and Java questions: changes are directly sent to ilias and have to be polled from there
2968  $config['withBackgroundChangeDetection'] = $question_config->isBackgroundChangeDetectionEnabled();
2969  $config['backgroundDetectorUrl'] = $this->ctrl->getLinkTarget($this, ilTestPlayerCommands::DETECT_CHANGES, '', true);
2970 
2971  // Forced feedback will change the navigation saving command
2972  $config['forcedInstantFeedback'] = $this->object->isForceInstantFeedbackEnabled();
2973  $config['questionLocked'] = $this->isParticipantsAnswerFixed($question_gui->getObject()->getId());
2974  $config['nextQuestionLocks'] = $this->object->isFollowupQuestionAnswerFixationEnabled();
2975  $config['autosaveFailureMessage'] = $this->lng->txt('autosave_failed');
2976 
2977  // Add the modal signals and parameter name for the follow-up question locks confirmation
2978  $config['modalSignals'] = array_map(fn(Signal $signal) => $signal->getId(), $this->modal_signals);
2979  $config['preventConfirmationParam'] = self::FOLLOWUP_QST_LOCKS_PREVENT_CONFIRMATION_PARAM;
2980 
2981  $this->tpl->addJavascript('assets/js/ilTestPlayerQuestionEditControl.js');
2982  $this->tpl->addOnLoadCode('il.TestPlayerQuestionEditControl.init(' . json_encode($config) . ')');
2983  }
2984  // fau.
2985 
2986  protected function getQuestionsDefaultPresentationMode(): string
2987  {
2988  return self::PRESENTATION_MODE_EDIT;
2989  }
2990 
2991  protected function registerForcedFeedbackNavUrl(string $forced_feedback_nav_url): void
2992  {
2993  if (ilSession::get('forced_feedback_navigation_url') == null) {
2994  ilSession::set('forced_feedback_navigation_url', []);
2995  }
2996  $forced_feeback_navigation_url = ilSession::get('forced_feedback_navigation_url');
2997  $forced_feeback_navigation_url[$this->test_session->getActiveId()] = $forced_feedback_nav_url;
2998  ilSession::set('forced_feedback_navigation_url', $forced_feeback_navigation_url);
2999  }
3000 
3001  protected function getRegisteredForcedFeedbackNavUrl(): ?string
3002  {
3003  if (ilSession::get('forced_feedback_navigation_url') === null) {
3004  return null;
3005  }
3006  $forced_feedback_navigation_url = ilSession::get('forced_feedback_navigation_url');
3007  if (!isset($forced_feedback_navigation_url[$this->test_session->getActiveId()])) {
3008  return null;
3009  }
3010 
3011  return $forced_feedback_navigation_url[$this->test_session->getActiveId()];
3012  }
3013 
3014  protected function isForcedFeedbackNavUrlRegistered(): bool
3015  {
3016  return $this->getRegisteredForcedFeedbackNavUrl() !== null;
3017  }
3018 
3019  protected function unregisterForcedFeedbackNavUrl(): void
3020  {
3021  $forced_feedback_navigation_url = ilSession::get('forced_feedback_navigation_url');
3022  if (isset($forced_feedback_navigation_url[$this->test_session->getActiveId()])) {
3023  unset($forced_feedback_navigation_url[$this->test_session->getActiveId()]);
3024  ilSession::set('forced_feedback_navigation_url', $forced_feedback_navigation_url);
3025  }
3026  }
3027 
3028  protected function handleFileUploadCmd(): void
3029  {
3030  $this->updateWorkingTime();
3031  $this->saveQuestionSolution(false);
3032  $this->ctrl->redirect($this, ilTestPlayerCommands::SUBMIT_SOLUTION);
3033  }
3034 
3035  protected function updateLearningProgressOnTestStart(): void
3036  {
3037  ilLPStatusWrapper::_updateStatus($this->object->getId(), $this->user->getId());
3038  }
3039 
3040  private function isValidSequenceElement($sequence_element): bool
3041  {
3042  if ($sequence_element === false) {
3043  return false;
3044  }
3045 
3046  if ($sequence_element < 1) {
3047  return false;
3048  }
3049 
3050  if (!$this->test_sequence->getPositionOfSequence($sequence_element)) {
3051  return false;
3052  }
3053 
3054  return true;
3055  }
3056 
3057  protected function submitSolutionAndNextCmd(): void
3058  {
3059  if ($this->object->isForceInstantFeedbackEnabled()) {
3060  $this->submitSolutionCmd();
3061  return;
3062  }
3063 
3064  if ($this->saveQuestionSolution(true, false)) {
3065  $this->test_sequence->getQuestionForSequence(
3066  $this->getCurrentSequenceElement()
3067  );
3068 
3069  $this->removeIntermediateSolution();
3070 
3071  $next_sequence_element = $this->test_sequence->getNextSequence($this->getCurrentSequenceElement());
3072 
3073  if (!$this->isValidSequenceElement($next_sequence_element)) {
3074  $next_sequence_element = $this->test_sequence->getFirstSequence();
3075  }
3076 
3077  $this->test_session->setLastSequence($next_sequence_element ?? 0);
3078  $this->test_session->saveToDb();
3079 
3080  $this->ctrl->setParameter($this, 'sequence', $next_sequence_element);
3081  $this->ctrl->setParameter($this, 'pmode', '');
3082  }
3083 
3084  $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
3085  }
3086 
3087  protected function handleQuestionPostponing(
3088  int $question_id
3089  ): void {
3090  $this->test_sequence->postponeQuestion($question_id);
3091  $this->test_sequence->saveToDb();
3092  }
3093 
3094  protected function handleCheckTestPassValid(bool $with_redirect = false): void
3095  {
3096  $testObj = new ilObjTest($this->ref_id, true);
3097 
3098  $participants = $testObj->getActiveParticipantList();
3099  $participant = $participants->getParticipantByActiveId($this->testrequest->getActiveId());
3100  if ($participant && $participant->hasUnfinishedPasses()) {
3101  return;
3102  }
3103  $this->tpl->setOnScreenMessage('failure', $this->lng->txt('tst_current_run_no_longer_valid'), true);
3104  if ($with_redirect) {
3105  $this->ctrl->redirectByClass([
3106  ilRepositoryGUI::class,
3107  ilObjTestGUI::class,
3108  TestScreenGUI::class
3109  ]);
3110  }
3111  }
3112 
3114  {
3115  if ($this->test_session->getActiveId() > 0) {
3116  if ($this->test_sequence->hasRandomQuestionsForPass($this->test_session->getActiveId(), $this->test_session->getPass()) > 0) {
3117  $this->logger->info(
3118  __METHOD__ . ' Random Questions allready exists for user ' .
3119  $this->user->getId() . ' in test ' . $this->object->getTestId()
3120  );
3121 
3122  return true;
3123  }
3124  } else {
3125  $this->logger->info(__METHOD__ . ' ' . sprintf(
3126  $this->lng->txt("error_random_question_generation"),
3127  $this->user->getId(),
3128  $this->object->getTestId()
3129  ));
3130 
3131  return true;
3132  };
3133 
3134  return false;
3135  }
3136 
3137  protected function generateRandomTestPassForActiveUser(): void
3138  {
3139  $questionSetConfig = new ilTestRandomQuestionSetConfig(
3140  $this->tree,
3141  $this->db,
3142  $this->lng,
3143  $this->logger,
3144  $this->component_repository,
3145  $this->object,
3146  $this->questionrepository
3147  );
3148  $questionSetConfig->loadFromDb();
3149 
3150  $sourcePoolDefinitionFactory = new ilTestRandomQuestionSetSourcePoolDefinitionFactory($this->db, $this->object);
3151 
3152  $sourcePoolDefinitionList = new ilTestRandomQuestionSetSourcePoolDefinitionList($this->db, $this->object, $sourcePoolDefinitionFactory);
3153  $sourcePoolDefinitionList->loadDefinitions();
3154 
3155  $this->process_locker->executeRandomPassBuildOperation(function () use ($questionSetConfig, $sourcePoolDefinitionList) {
3157  $stagingPoolQuestionList = new ilTestRandomQuestionSetStagingPoolQuestionList($this->db, $this->component_repository);
3158 
3159  $questionSetBuilder = ilTestRandomQuestionSetBuilder::getInstance(
3160  $this->db,
3161  $this->lng,
3162  $this->logger,
3163  $this->object,
3164  $questionSetConfig,
3165  $sourcePoolDefinitionList,
3166  $stagingPoolQuestionList
3167  );
3168 
3169  $questionSetBuilder->performBuild($this->test_session);
3170  }
3171  }, $sourcePoolDefinitionList->hasTaxonomyFilters());
3172  }
3173 
3174  protected function adoptUserSolutionsFromPreviousPass(): void
3175  {
3176  $ass_settings = new ilSetting('assessment');
3177 
3178  $userSolutionAdopter = new ilAssQuestionUserSolutionAdopter($this->db, $ass_settings);
3179  $userSolutionAdopter->setUserId($this->user->getId());
3180  $userSolutionAdopter->setActiveId($this->test_session->getActiveId());
3181  $userSolutionAdopter->setTargetPass($this->test_sequence->getPass());
3182  $userSolutionAdopter->setQuestionIds($this->test_sequence->getOptionalQuestions());
3183 
3184  $userSolutionAdopter->perform();
3185  }
3186 
3188  {
3189  $this->tpl->setOnScreenMessage('failure', sprintf($this->lng->txt('tst_objective_oriented_test_pass_without_questions'), $this->object->getTitle()), true);
3190  $this->ctrl->redirectByClass(ilObjTestGUI::class);
3191  }
3192 
3193  protected function handlePrimaryButton(ilTestNavigationToolbarGUI $navigation_toolbar_gui, int $current_question_id): bool
3194  {
3195  $is_next_primary = true;
3196 
3197  if ($this->object->isForceInstantFeedbackEnabled()) {
3198  $is_next_primary = false;
3199  }
3200 
3201  $questions_missing_result = $this->questionrepository->getQuestionsMissingResultRecord(
3202  $this->test_session->getActiveId(),
3203  $this->test_session->getPass(),
3204  $this->test_sequence->getOrderedSequenceQuestions()
3205  );
3206 
3207  if ($questions_missing_result === []) {
3208  $navigation_toolbar_gui->setFinishTestButtonPrimary(true);
3209  return false;
3210  }
3211 
3212  if (count($questions_missing_result) === 1
3213  && $current_question_id === current($questions_missing_result)) {
3214  $navigation_toolbar_gui->setFinishTestButtonPrimary(true);
3215  return false;
3216  }
3217 
3218  return $is_next_primary;
3219  }
3220 
3221  protected function getTestPlayerTitle(): string
3222  {
3223  $title_content = $this->ui_factory->listing()->property();
3224 
3225  if ($this->object->getShowKioskModeParticipant()) {
3226  $pax_name_label = $this->lng->txt("conf_user_name");
3227  // this is a placeholder solution with inline html tags to differentiate the different elements
3228  // should be removed when a title component with grouping and visual weighting is available
3229  // see: https://github.com/ILIAS-eLearning/ILIAS/pull/7311
3230  $pax_name_value = "<span class='il-test-kiosk-head__participant-name'>"
3231  . $this->user->getFullname() . "</span>";
3232  $title_content = $title_content->withProperty($pax_name_label, $pax_name_value, false);
3233  }
3234 
3235  if ($this->object->isShowExamIdInTestPassEnabled()) {
3236  $exam_id_label = $this->lng->txt("exam_id_label");
3237  $exam_id_value = ilObjTest::buildExamId(
3238  $this->test_session->getActiveId(),
3239  $this->test_session->getPass(),
3240  $this->object->getId()
3241  );
3242  $title_content = $title_content->withProperty($exam_id_label, $exam_id_value);
3243  }
3244 
3245  if ($this->object->getShowKioskModeTitle()) {
3246  $test_title_label = $this->lng->txt("test");
3247  $test_title_value = $this->object->getTitle();
3248  $title_content = $title_content->withProperty($test_title_label, $test_title_value, false);
3249  }
3250 
3251  return $this->ui_renderer->render($title_content);
3252  }
3253 }
outQuestionSummaryCmd()
Output of a summary of all test questions for test participants.
showSideList($current_sequence_element)
getQuestionInstance(int $question_id, bool $from_cache=true)
static get(string $a_var)
redirectAfterAutosaveCmd()
Redirect the user after an automatic save when the time limit is reached.
static getStyleSheetLocation(string $mode="output", string $a_css_name="")
get full style sheet file name (path inclusive) of current user
startPlayerCmd()
Start a test for the first time.
prepareWorkingTimeJsTemplate(ilObjTest $object, array $date, string $check_url, string $redirect_url)
Definition: WorkingTime.php:42
getOnLoadCodeForNavigationButtons(string $target, string $cmd)
static formatDate(ilDateTime $date, bool $a_skip_day=false, bool $a_include_wd=false, bool $include_seconds=false, ilObjUser $user=null,)
Class ilTestPassFinishTasks.
sendNewPassFinishedNotificationEmailIfActivated(int $active_id, int $pass)
getAdditionalUsrDataHtmlAndPopulateWindowTitle($testSession, $active_id, $overwrite_anonymity=false)
Returns the user data for a test results output.
static _getPass($active_id)
Retrieves the actual pass of a given user for a given test.
showQuestionViewable(assQuestionGUI $question_gui, string $form_action, bool $is_question_worked_through, bool $instant_response)
populateSpecificFeedbackBlock(assQuestionGUI $question_gui)
get(string $part=ilGlobalTemplateInterface::DEFAULT_BLOCK)
populateInstantResponseModal(assQuestionGUI $question_gui, $nav_url)
setAnonymousIdCmd()
Sets a session variable with the test access code for an anonymous test user.
autosaveCmd()
Automatically save a user answer while working on the test (called repeatedly by asynchronous posts i...
populateTestNavigationToolbar(ilTestNavigationToolbarGUI $toolbar_gui)
Class ilTaggingGUI.
confirmSubmitAnswers()
confirm submit results if confirm then results are submitted and the screen will be redirected to the...
updateWorkingTime()
updates working time and stores state saveresult to see if question has to be stored or not ...
saveTagsCmd()
Save tags for tagging gui.
Class ilPageObjectGUI.
ilGlobalTemplateInterface ilTemplate $tpl
sk 2023-08-01: We need this union type, even if it is wrong! To change this
ilTestQuestionRelatedObjectivesList $question_related_objectives_list
static getInstance(ilDBInterface $db, ilLanguage $lng, TestLogger $logger, ilObjTest $testOBJ, ilTestRandomQuestionSetConfig $questionSetConfig, ilTestRandomQuestionSetSourcePoolDefinitionList $sourcePoolDefinitionList, ilTestRandomQuestionSetStagingPoolQuestionList $stagingPoolQuestionList)
max_processing_time_reached()
Outputs a message when the maximum processing time is reached.
getResultsSignature()
Returns HTML code for a signature field.
$url
Definition: shib_logout.php:63
setVariable(string $variable, $value='')
Sets the given variable to the given value.
persistWorkingState(int $active_id, $pass, bool $authorized=true)
persists the working state for current testactive and testpass
handleUserSettings()
Handles some form parameters on starting and resuming a test.
const IL_CAL_UNIX
getContentBlockName()
Returns the name of the current content block (depends on the kiosk mode setting) ...
confirmHintRequestCmd()
Go to hint request confirmation.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
handleCheckTestPassValid(bool $with_redirect=false)
checkWorkingTimeCmd()
This is asynchronously called by tpl.workingtime.js to check for changes in the user&#39;s processing tim...
Base Exception for all Exceptions relating to Modules/Test.
Test sequence handler.
getId()
Get the ID of this signal.
showRequestedHintListCmd()
Go to requested hint list.
ilTestSequenceFactory $test_sequence_factory
checkTestSessionUser(ilTestSession $test_session)
showListOfAnswers($active_id, $pass=null, $top_data="", $bottom_data="")
Creates an output of the list of answers for a test participant during the test (only the actual pass...
static instantiateQuestion(int $question_id)
populateQuestionNavigation($sequence_element, $primary_next)
static getInstance(ilTestSession $a_test_session)
markQuestionCmd()
Set a question solved.
static http()
Fetches the global http state from ILIAS.
getQuestionFeedbackModalHtml(ilTemplate $tpl, string $nav_url)
populateGenericFeedbackBlock(assQuestionGUI $question_gui, $solutionCorrect)
outQuestionForTest(string $formaction, int $active_id, ?int $pass, bool $is_question_postponed=false, array|bool $user_post_solutions=false, bool $show_specific_inline_feedback=false)
readonly ilDBInterface $db
answerToParticipantInteraction(AdditionalInformationGenerator $additional_info, int $test_ref_id, int $active_id, int $pass, string $source_ip, TestParticipantInteractionTypes $interaction_type)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
$_SERVER['HTTP_HOST']
Definition: raiseError.php:10
outCorrectSolution()
Creates an output of the solution of an answer compared to the correct solution.
handleSkillTriggering(ilTestSession $test_session)
endingTimeReached()
handle endingTimeReached
const REDIRECT_NONE
prepareTestPage($presentationMode, $sequenceElement, $question_id)
static _getSolvedQuestions($active_id, $question_fi=null)
get solved questions
setUserId(int $user_id)
ensureExistingTestSession(ilTestSession $test_session)
performTestPassFinishedTasks(StatusOfAttempt $status_of_attempt)
outProcessingTime(int $active_id, bool $verbose)
getCorrectSolutionOutput($question_id, $active_id, $pass, ilTestQuestionRelatedObjectivesList $objectives_list=null)
Returns an output of the solution to an answer compared to the correct solution.
detectChangesCmd()
Detect changes sent in the background to the server This is called by ajax from ilTestPlayerQuestionE...
static getContentStylePath(int $a_style_id, bool $add_random=true, bool $add_token=true)
get content style path static (to avoid full reading)
setFinishTestButtonPrimary($finishTestButtonPrimary)
static redirect(string $a_script)
unmarkQuestionCmd()
Set a question unsolved.
populateQuestionEditControl(assQuestionGUI $question_gui)
isOptionalQuestionAnsweringConfirmationRequired(int $sequence_key)
handlePrimaryButton(ilTestNavigationToolbarGUI $navigation_toolbar_gui, int $current_question_id)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
populateInstantResponseBlocks(assQuestionGUI $question_gui, $authorizedSolution)
form( $class_path, string $cmd, string $submit_caption="")
setAnonymousId(string $anonymous_id)
getSpecificFeedbackOutput(array $userSolution)
Returns the answer specific feedback for the question.
isTestAccessible()
test accessible returns true if the user can perform the test
static _getHttpPath()
outQuestionPage($a_temp_var, $a_postponed=false, $active_id="", $html="", $inlineFeedbackEnabled=false)
static initjQuery(ilGlobalTemplateInterface $a_tpl=null)
inits and adds the jQuery JS-File to the global or a passed template
$id
plugin.php for ilComponentBuildPluginInfoObjectiveTest::testAddPlugins
Definition: plugin.php:24
__construct(Container $dic, ilPlugin $plugin)
populateMessageContent(string $content_html)
static buildExamId($active_id, $pass, $test_obj_id=null)
registerForcedFeedbackNavUrl(string $forced_feedback_nav_url)
handleTearsAndAngerQuestionIsNull(int $question_id, $sequence_element)
Service GUI class for tests.
save(assQuestion $question_obj, bool $authorized)
showQuestionEditable(assQuestionGUI $question_gui, string $form_action, bool $is_question_worked_through, bool $instant_response)
initTestCmd()
Start a test for the first time after a redirect.
determineSolutionPassIndex(assQuestionGUI $question_gui)
static clear(string $a_var)
saveQuestionSolution(bool $authorized=true, bool $force=false)
getSolutionOutput(int $active_id, ?int $pass=null, bool $graphical_output=false, bool $result_output=false, bool $show_question_only=true, bool $show_feedback=false, bool $show_correct_solution=false, bool $show_manual_scoring=false, bool $show_question_text=true, bool $show_inline_feedback=true)
static set(string $a_var, $a_val)
Set a value.
initTestQuestionConfig(assQuestion $question_obj)
getQuestionGuiInstance(int $question_id, bool $from_cache=true)
populateScoreBlock($reachedPoints, $maxPoints)
static _updateStatus(int $a_obj_id, int $a_usr_id, ?object $a_obj=null, bool $a_percentage=false, bool $a_force_raise=false)
setNavigationGUI(?ilTestQuestionNavigationGUI $navigationGUI)
autosaveOnTimeLimitCmd()
Automatically save a user answer when the limited duration of a test run is reached (called by synchr...
static lookupExamId($active_id, $pass)
getGenericFeedbackOutput(int $active_id, ?int $pass)