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