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