1<?php
2/* Copyright (c) 1998-2013 ILIAS open source, Extended GPL, see docs/LICENSE */
3
4require_once './Modules/Test/classes/class.ilTestPlayerAbstractGUI.php';
5
6/**
7 * Output class for assessment test execution
8 *
9 * The ilTestOutputGUI class creates the output for the ilObjTestGUI class when learners execute a test. This saves
10 * some heap space because the ilObjTestGUI class will be much smaller then
11 *
12 * @author		Helmut Schottmüller <helmut.schottmueller@mac.com>
13 * @author		Björn Heyser <bheyser@databay.de>
14 * @author		Maximilian Becker <mbecker@databay.de>
15 *
16 * @version		$Id$
17 *
18 * @inGroup		ModulesTest
19 */
20abstract class ilTestOutputGUI extends ilTestPlayerAbstractGUI
21{
22    /**
23     * @var ilTestQuestionRelatedObjectivesList
24     */
25    protected $questionRelatedObjectivesList;
26
27    /**
28     * Execute Command
29     */
30    public function executeCommand()
31    {
32        global $DIC;
33        $ilDB = $DIC['ilDB'];
34        $ilPluginAdmin = $DIC['ilPluginAdmin'];
35        $lng = $DIC['lng'];
36        $ilTabs = $DIC['ilTabs'];
37
38        $this->checkReadAccess();
39
40        $ilTabs->clearTargets();
41
42        $cmd = $this->ctrl->getCmd();
43        $next_class = $this->ctrl->getNextClass($this);
44
45        $this->ctrl->saveParameter($this, "sequence");
46        $this->ctrl->saveParameter($this, "pmode");
47        $this->ctrl->saveParameter($this, "active_id");
48
49        $this->initAssessmentSettings();
50
51        $DIC->globalScreen()->tool()->context()->current()->addAdditionalData(
52            ilTestPlayerLayoutProvider::TEST_PLAYER_KIOSK_MODE_ENABLED,
53            $this->object->getKioskMode()
54        );
55
56        $testSessionFactory = new ilTestSessionFactory($this->object);
57        $this->testSession = $testSessionFactory->getSession($_GET['active_id']);
58
59        $this->ensureExistingTestSession($this->testSession);
60        $this->checkTestSessionUser($this->testSession);
61
62        $this->initProcessLocker($this->testSession->getActiveId());
63
64        $testSequenceFactory = new ilTestSequenceFactory($ilDB, $lng, $ilPluginAdmin, $this->object);
65        $this->testSequence = $testSequenceFactory->getSequenceByTestSession($this->testSession);
66        $this->testSequence->loadFromDb();
67        $this->testSequence->loadQuestions();
68
69        require_once 'Modules/Test/classes/class.ilTestQuestionRelatedObjectivesList.php';
70        $this->questionRelatedObjectivesList = new ilTestQuestionRelatedObjectivesList();
71
72        include_once 'Services/jQuery/classes/class.iljQueryUtil.php';
73        iljQueryUtil::initjQuery();
74        include_once "./Services/YUI/classes/class.ilYuiUtil.php";
75        ilYuiUtil::initConnectionWithAnimation();
76
77        $this->handlePasswordProtectionRedirect();
78
79        $cmd = $this->getCommand($cmd);
80
81        switch ($next_class) {
82            case 'ilassquestionpagegui':
83
84                $this->checkTestExecutable();
85
86                $questionId = $this->testSequence->getQuestionForSequence($this->getCurrentSequenceElement());
87
88                require_once "./Modules/TestQuestionPool/classes/class.ilAssQuestionPageGUI.php";
89                $page_gui = new ilAssQuestionPageGUI($questionId);
90                $ret = $this->ctrl->forwardCommand($page_gui);
91                break;
92
93            case 'iltestsubmissionreviewgui':
94
95                $this->checkTestExecutable();
96
97                require_once './Modules/Test/classes/class.ilTestSubmissionReviewGUI.php';
98                $gui = new ilTestSubmissionReviewGUI($this, $this->object, $this->testSession);
99                $gui->setObjectiveOrientedContainer($this->getObjectiveOrientedContainer());
100                $ret = $this->ctrl->forwardCommand($gui);
101                break;
102
103            case 'ilassquestionhintrequestgui':
104
105                $this->checkTestExecutable();
106
107                $questionGUI = $this->object->createQuestionGUI(
108                    "",
109                    $this->testSequence->getQuestionForSequence($this->getCurrentSequenceElement())
110                );
111
112                require_once 'Modules/TestQuestionPool/classes/class.ilAssQuestionHintTracking.php';
113                $questionHintTracking = new ilAssQuestionHintTracking(
114                    $questionGUI->object->getId(),
115                    $this->testSession->getActiveId(),
116                    $this->testSession->getPass()
117                );
118
119                require_once 'Modules/TestQuestionPool/classes/class.ilAssQuestionHintRequestGUI.php';
120                $gui = new ilAssQuestionHintRequestGUI($this, ilTestPlayerCommands::SHOW_QUESTION, $questionGUI, $questionHintTracking);
121
122// fau: testNav - save the 'answer changed' status for viewing hint requests
123                $this->setAnswerChangedParameter($this->getAnswerChangedParameter());
124// fau.
125                $ret = $this->ctrl->forwardCommand($gui);
126
127                break;
128
129            case 'iltestsignaturegui':
130
131                $this->checkTestExecutable();
132
133                require_once './Modules/Test/classes/class.ilTestSignatureGUI.php';
134                $gui = new ilTestSignatureGUI($this);
135                $ret = $this->ctrl->forwardCommand($gui);
136                break;
137
138            case 'iltestpasswordprotectiongui':
139
140                $this->checkTestExecutable();
141
142                require_once 'Modules/Test/classes/class.ilTestPasswordProtectionGUI.php';
143                $gui = new ilTestPasswordProtectionGUI($this->ctrl, $this->tpl, $this->lng, $this, $this->passwordChecker);
144                $ret = $this->ctrl->forwardCommand($gui);
145                break;
146
147            default:
148
149                if (ilTestPlayerCommands::isTestExecutionCommand($cmd)) {
150                    $this->checkTestExecutable();
151                }
152
153                $cmd .= 'Cmd';
154                $ret = &$this->$cmd();
155                break;
156        }
157        return $ret;
158    }
159
160    protected function startTestCmd()
161    {
162        global $DIC;
163        $ilUser = $DIC['ilUser'];
164
165        $_SESSION['tst_pass_finish'] = 0;
166
167        // ensure existing test session
168        $this->testSession->setUserId($ilUser->getId());
169        $this->testSession->setAnonymousId($_SESSION["tst_access_code"][$this->object->getTestId()]);
170        $this->testSession->setObjectiveOrientedContainerId($this->getObjectiveOrientedContainer()->getObjId());
171        $this->testSession->saveToDb();
172
173        $active_id = $this->testSession->getActiveId();
174        $this->ctrl->setParameter($this, "active_id", $active_id);
175
176        $shuffle = $this->object->getShuffleQuestions();
177        if ($this->object->isRandomTest()) {
178            $this->generateRandomTestPassForActiveUser();
179
180            $this->object->loadQuestions();
181            $shuffle = false; // shuffle is already done during the creation of the random questions
182        }
183
184        assQuestion::_updateTestPassResults(
185            $active_id,
186            $this->testSession->getPass(),
187            $this->object->areObligationsEnabled(),
188            null,
189            $this->object->id
190        );
191
192        // ensure existing test sequence
193        if (!$this->testSequence->hasSequence()) {
194            $this->testSequence->createNewSequence($this->object->getQuestionCount(), $shuffle);
195            $this->testSequence->saveToDb();
196        }
197
198        $this->testSequence->loadFromDb();
199        $this->testSequence->loadQuestions();
200
201        if ($this->testSession->isObjectiveOriented()) {
202            require_once 'Modules/Course/classes/Objectives/class.ilLOTestQuestionAdapter.php';
203            $objectivesAdapter = ilLOTestQuestionAdapter::getInstance($this->testSession);
204
205            $objectivesAdapter->notifyTestStart($this->testSession, $this->object->getId());
206            $objectivesAdapter->prepareTestPass($this->testSession, $this->testSequence);
207
208            $objectivesAdapter->buildQuestionRelatedObjectiveList(
209                $this->testSequence,
210                $this->questionRelatedObjectivesList
211            );
212
213            if ($this->testSequence->hasOptionalQuestions()) {
214                $this->adoptUserSolutionsFromPreviousPass();
215
216                $this->testSequence->reorderOptionalQuestionsToSequenceEnd();
217                $this->testSequence->saveToDb();
218            }
219        }
220
221        $active_time_id = $this->object->startWorkingTime(
222            $this->testSession->getActiveId(),
223            $this->testSession->getPass()
224        );
225        $_SESSION["active_time_id"] = $active_time_id;
226
227        $this->updateLearningProgressOnTestStart();
228
229        $sequenceElement = $this->testSequence->getFirstSequence();
230
231        $this->ctrl->setParameter($this, 'sequence', $sequenceElement);
232        $this->ctrl->setParameter($this, 'pmode', '');
233
234        if ($this->object->getListOfQuestionsStart()) {
235            $this->ctrl->redirect($this, ilTestPlayerCommands::QUESTION_SUMMARY);
236        }
237
238        $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
239    }
240
241    protected function updateLearningProgressOnTestStart()
242    {
243        global $DIC;
244        $ilUser = $DIC['ilUser'];
245
246        require_once('./Modules/Test/classes/class.ilObjTestAccess.php');
247        require_once('./Services/Tracking/classes/class.ilLPStatusWrapper.php');
248        ilLPStatusWrapper::_updateStatus($this->object->getId(), $ilUser->getId());
249    }
250
251    private function isValidSequenceElement($sequenceElement)
252    {
253        if ($sequenceElement === false) {
254            return false;
255        }
256
257        if ($sequenceElement < 1) {
258            return false;
259        }
260
261        if (!$this->testSequence->getPositionOfSequence($sequenceElement)) {
262            return false;
263        }
264
265        return true;
266    }
267
268    protected function showQuestionCmd()
269    {
270        $_SESSION['tst_pass_finish'] = 0;
271
272        $_SESSION["active_time_id"] = $this->object->startWorkingTime(
273            $this->testSession->getActiveId(),
274            $this->testSession->getPass()
275        );
276
277        $sequenceElement = $this->getCurrentSequenceElement();
278
279        if (!$this->isValidSequenceElement($sequenceElement)) {
280            $sequenceElement = $this->testSequence->getFirstSequence();
281        }
282
283        $this->testSession->setLastSequence($sequenceElement);
284        $this->testSession->saveToDb();
285
286
287        $questionId = $this->testSequence->getQuestionForSequence($sequenceElement);
288
289        if (!(int) $questionId && $this->testSession->isObjectiveOriented()) {
290            $this->handleTearsAndAngerNoObjectiveOrientedQuestion();
291        }
292
293        if (!$this->testSequence->isQuestionPresented($questionId)) {
294            $this->testSequence->setQuestionPresented($questionId);
295            $this->testSequence->saveToDb();
296        }
297
298        $isQuestionWorkedThrough = assQuestion::_isWorkedThrough(
299            $this->testSession->getActiveId(),
300            $questionId,
301            $this->testSession->getPass()
302        );
303
304        // fau: testNav - always use edit mode, except for fixed answer
305        if ($this->isParticipantsAnswerFixed($questionId)) {
306            $presentationMode = ilTestPlayerAbstractGUI::PRESENTATION_MODE_VIEW;
307            $instantResponse = true;
308        } else {
309            $presentationMode = ilTestPlayerAbstractGUI::PRESENTATION_MODE_EDIT;
310            $instantResponse = $this->getInstantResponseParameter();
311        }
312        // fau.
313
314        $questionGui = $this->getQuestionGuiInstance($questionId);
315
316        if (!($questionGui instanceof assQuestionGUI)) {
317            $this->handleTearsAndAngerQuestionIsNull($questionId, $sequenceElement);
318        }
319
320        $questionGui->setSequenceNumber($this->testSequence->getPositionOfSequence($sequenceElement));
321        $questionGui->setQuestionCount($this->testSequence->getUserQuestionCount());
322
323        require_once 'Modules/Test/classes/class.ilTestQuestionHeaderBlockBuilder.php';
324        $headerBlockBuilder = new ilTestQuestionHeaderBlockBuilder($this->lng);
325        $headerBlockBuilder->setHeaderMode($this->object->getTitleOutput());
326        $headerBlockBuilder->setQuestionTitle($questionGui->object->getTitle());
327        $headerBlockBuilder->setQuestionPoints($questionGui->object->getPoints());
328        $headerBlockBuilder->setQuestionPosition($this->testSequence->getPositionOfSequence($sequenceElement));
329        $headerBlockBuilder->setQuestionCount($this->testSequence->getUserQuestionCount());
330        $headerBlockBuilder->setQuestionPostponed($this->testSequence->isPostponedQuestion($questionId));
331        $headerBlockBuilder->setQuestionObligatory(
332            $this->object->areObligationsEnabled() && ilObjTest::isQuestionObligatory($questionGui->object->getId())
333        );
334        if ($this->testSession->isObjectiveOriented()) {
335            require_once 'Modules/Course/classes/Objectives/class.ilLOTestQuestionAdapter.php';
336            $objectivesAdapter = ilLOTestQuestionAdapter::getInstance($this->testSession);
337            $objectivesAdapter->buildQuestionRelatedObjectiveList($this->testSequence, $this->questionRelatedObjectivesList);
338            $this->questionRelatedObjectivesList->loadObjectivesTitles();
339
340            $objectivesString = $this->questionRelatedObjectivesList->getQuestionRelatedObjectiveTitles($questionId);
341            $headerBlockBuilder->setQuestionRelatedObjectives($objectivesString);
342        }
343        $questionGui->setQuestionHeaderBlockBuilder($headerBlockBuilder);
344
345        $this->prepareTestPage($presentationMode, $sequenceElement, $questionId);
346
347        $navigationToolbarGUI = $this->getTestNavigationToolbarGUI();
348        $navigationToolbarGUI->setFinishTestButtonEnabled(true);
349
350        $isNextPrimary = $this->handlePrimaryButton($navigationToolbarGUI, $questionId);
351
352        $this->ctrl->setParameter($this, 'sequence', $sequenceElement);
353        $this->ctrl->setParameter($this, 'pmode', $presentationMode);
354        $formAction = $this->ctrl->getFormAction($this, ilTestPlayerCommands::SUBMIT_INTERMEDIATE_SOLUTION);
355
356        switch ($presentationMode) {
357            case ilTestPlayerAbstractGUI::PRESENTATION_MODE_EDIT:
358
359// fau: testNav - enable navigation toolbar in edit mode
360                $navigationToolbarGUI->setDisabledStateEnabled(false);
361// fau.
362                $this->showQuestionEditable($questionGui, $formAction, $isQuestionWorkedThrough, $instantResponse);
363
364                break;
365
366            case ilTestPlayerAbstractGUI::PRESENTATION_MODE_VIEW:
367
368                if ($this->testSequence->isQuestionOptional($questionGui->object->getId())) {
369                    $this->populateQuestionOptionalMessage();
370                }
371
372                $this->showQuestionViewable($questionGui, $formAction, $isQuestionWorkedThrough, $instantResponse);
373
374                break;
375
376            default:
377
378                require_once 'Modules/Test/exceptions/class.ilTestException.php';
379                throw new ilTestException('no presentation mode given');
380        }
381
382        $navigationToolbarGUI->build();
383        $this->populateTestNavigationToolbar($navigationToolbarGUI);
384
385        // fau: testNav - enable the question navigation in edit mode
386        $this->populateQuestionNavigation($sequenceElement, false, $isNextPrimary);
387        // fau.
388
389        if ($instantResponse) {
390            // fau: testNav - always use authorized solution for instant feedback
391            $this->populateInstantResponseBlocks(
392                $questionGui,
393                true
394            );
395            // fau.
396        }
397
398        // fau: testNav - add feedback modal
399        if ($this->isForcedFeedbackNavUrlRegistered()) {
400            $this->populateInstantResponseModal($questionGui, $this->getRegisteredForcedFeedbackNavUrl());
401            $this->unregisterForcedFeedbackNavUrl();
402        }
403        // fau.
404    }
405
406    protected function editSolutionCmd()
407    {
408        $this->ctrl->setParameter($this, 'pmode', ilTestPlayerAbstractGUI::PRESENTATION_MODE_EDIT);
409        $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
410    }
411
412    protected function submitSolutionAndNextCmd()
413    {
414        if ($this->object->isForceInstantFeedbackEnabled()) {
415            return $this->submitSolutionCmd();
416        }
417
418        if ($this->saveQuestionSolution(true, false)) {
419            $questionId = $this->testSequence->getQuestionForSequence(
420                $this->getCurrentSequenceElement()
421            );
422
423            $this->removeIntermediateSolution();
424
425            $nextSequenceElement = $this->testSequence->getNextSequence($this->getCurrentSequenceElement());
426
427            if (!$this->isValidSequenceElement($nextSequenceElement)) {
428                $nextSequenceElement = $this->testSequence->getFirstSequence();
429            }
430
431            $this->testSession->setLastSequence($nextSequenceElement);
432            $this->testSession->saveToDb();
433
434            $this->ctrl->setParameter($this, 'sequence', $nextSequenceElement);
435            $this->ctrl->setParameter($this, 'pmode', '');
436        }
437
438        $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
439    }
440
441    protected function submitSolutionCmd()
442    {
443        if ($this->saveQuestionSolution(true, false)) {
444            $questionId = $this->testSequence->getQuestionForSequence(
445                $this->getCurrentSequenceElement()
446            );
447
448            $this->removeIntermediateSolution();
449
450            if ($this->object->isForceInstantFeedbackEnabled()) {
451                $this->ctrl->setParameter($this, 'instresp', 1);
452
453                $this->testSequence->setQuestionChecked($questionId);
454                $this->testSequence->saveToDb();
455            }
456
457            if ($this->getNextCommandParameter()) {
458                if ($this->getNextSequenceParameter()) {
459                    $this->ctrl->setParameter($this, 'sequence', $this->getNextSequenceParameter());
460                    $this->ctrl->setParameter($this, 'pmode', '');
461                }
462
463                $this->ctrl->redirect($this, $this->getNextCommandParameter());
464            }
465
466            $this->ctrl->setParameter($this, 'pmode', ilTestPlayerAbstractGUI::PRESENTATION_MODE_VIEW);
467        } else {
468            $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
469        }
470
471        // fau: testNav - remember to prevent the navigation confirmation
472        $this->saveNavigationPreventConfirmation();
473        // fau.
474
475        // fau: testNav - handle navigation after saving
476        if ($this->getNavigationUrlParameter()) {
477            ilUtil::redirect($this->getNavigationUrlParameter());
478        } else {
479            $this->ctrl->saveParameter($this, 'sequence');
480        }
481        // fau.
482        $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
483    }
484
485    protected function discardSolutionCmd()
486    {
487        $currentSequenceElement = $this->getCurrentSequenceElement();
488
489        $currentQuestionOBJ = $this->getQuestionInstance(
490            $this->testSequence->getQuestionForSequence($currentSequenceElement)
491        );
492
493        $currentQuestionOBJ->resetUsersAnswer(
494            $this->testSession->getActiveId(),
495            $this->testSession->getPass()
496        );
497
498        $this->ctrl->saveParameter($this, 'sequence');
499
500        $this->ctrl->setParameter($this, 'pmode', ilTestPlayerAbstractGUI::PRESENTATION_MODE_VIEW);
501
502        $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
503    }
504
505    protected function skipQuestionCmd()
506    {
507        $curSequenceElement = $this->getCurrentSequenceElement();
508        $nextSequenceElement = $this->testSequence->getNextSequence($curSequenceElement);
509
510        if (!$this->isValidSequenceElement($nextSequenceElement)) {
511            $nextSequenceElement = $this->testSequence->getFirstSequence();
512        }
513
514        if ($this->object->isPostponingEnabled()) {
515            $this->testSequence->postponeSequence($curSequenceElement);
516            $this->testSequence->saveToDb();
517        }
518
519        $this->ctrl->setParameter($this, 'sequence', $nextSequenceElement);
520        $this->ctrl->setParameter($this, 'pmode', '');
521
522        $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
523    }
524
525    protected function handleQuestionPostponing($sequenceElement)
526    {
527        $questionId = $this->testSequence->getQuestionForSequence($sequenceElement);
528
529        $isQuestionWorkedThrough = assQuestion::_isWorkedThrough(
530            $this->testSession->getActiveId(),
531            $questionId,
532            $this->testSession->getPass()
533        );
534
535        if (!$isQuestionWorkedThrough) {
536            $this->testSequence->postponeQuestion($questionId);
537            $this->testSequence->saveToDb();
538        }
539    }
540
541    protected function nextQuestionCmd()
542    {
543        $lastSequenceElement = $this->getCurrentSequenceElement();
544        $nextSequenceElement = $this->testSequence->getNextSequence($lastSequenceElement);
545
546        if ($this->object->isPostponingEnabled()) {
547            $this->handleQuestionPostponing($lastSequenceElement);
548        }
549
550        if (!$this->isValidSequenceElement($nextSequenceElement)) {
551            $nextSequenceElement = $this->testSequence->getFirstSequence();
552        }
553
554        $this->ctrl->setParameter($this, 'sequence', $nextSequenceElement);
555        $this->ctrl->setParameter($this, 'pmode', '');
556
557        $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
558    }
559
560    protected function previousQuestionCmd()
561    {
562        $sequenceElement = $this->testSequence->getPreviousSequence(
563            $this->getCurrentSequenceElement()
564        );
565
566        if (!$this->isValidSequenceElement($sequenceElement)) {
567            $sequenceElement = $this->testSequence->getLastSequence();
568        }
569
570        $this->ctrl->setParameter($this, 'sequence', $sequenceElement);
571        $this->ctrl->setParameter($this, 'pmode', '');
572
573        $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
574    }
575
576    protected function isFirstQuestionInSequence($sequenceElement)
577    {
578        return $sequenceElement == $this->testSequence->getFirstSequence();
579    }
580
581    protected function isLastQuestionInSequence($sequenceElement)
582    {
583        return $sequenceElement == $this->testSequence->getLastSequence();
584    }
585
586    /**
587     * Returns TRUE if the answers of the current user could be saved
588     *
589     * @return boolean TRUE if the answers could be saved, FALSE otherwise
590     */
591    protected function canSaveResult()
592    {
593        return !$this->object->endingTimeReached() && !$this->isMaxProcessingTimeReached() && !$this->isNrOfTriesReached();
594    }
595
596    /**
597     * @return integer
598     */
599    protected function getCurrentQuestionId()
600    {
601        return $this->testSequence->getQuestionForSequence($_GET["sequence"]);
602    }
603
604    /**
605     * saves the user input of a question
606     */
607    public function saveQuestionSolution($authorized = true, $force = false)
608    {
609        $this->updateWorkingTime();
610        $this->saveResult = false;
611        if (!$force) {
612            $formtimestamp = $_POST["formtimestamp"];
613            if (strlen($formtimestamp) == 0) {
614                $formtimestamp = $_GET["formtimestamp"];
615            }
616            if ($formtimestamp != $_SESSION["formtimestamp"]) {
617                $_SESSION["formtimestamp"] = $formtimestamp;
618            } else {
619                return false;
620            }
621        }
622        // save question solution
623        if ($this->canSaveResult() || $force) {
624            // but only if the ending time is not reached
625            $q_id = $this->testSequence->getQuestionForSequence($_GET["sequence"]);
626
627            if ($this->isParticipantsAnswerFixed($q_id)) {
628                // should only be reached by firebugging the disabled form in ui
629                throw new ilTestException('not allowed request');
630            }
631
632            if (is_numeric($q_id) && (int) $q_id) {
633                $questionOBJ = $this->getQuestionInstance($q_id);
634                $pass = null;
635                $active_id = $this->testSession->getActiveId();
636                if ($this->object->isRandomTest()) {
637                    $pass = ilObjTest::_getPass($active_id);
638                }
639                $this->saveResult = $questionOBJ->persistWorkingState(
640                    $active_id,
641                    $pass,
642                    $this->object->areObligationsEnabled(),
643                    $authorized
644                );
645
646                if ($authorized && $this->testSession->isObjectiveOriented()) {
647                    require_once 'Modules/Course/classes/Objectives/class.ilLOTestQuestionAdapter.php';
648                    $objectivesAdapter = ilLOTestQuestionAdapter::getInstance($this->testSession);
649                    $objectivesAdapter->updateQuestionResult($this->testSession, $questionOBJ);
650                }
651
652                if ($authorized && $this->object->isSkillServiceToBeConsidered()) {
653                    $this->handleSkillTriggering($this->testSession);
654                }
655            }
656        }
657
658        if ($this->saveResult == false || (!$questionOBJ->validateSolutionSubmit() && $questionOBJ->savePartial()) ) {
659            $this->ctrl->setParameter($this, "save_error", "1");
660            $_SESSION["previouspost"] = $_POST;
661        }
662
663        return $this->saveResult;
664    }
665
666    protected function showInstantResponseCmd()
667    {
668        $questionId = $this->testSequence->getQuestionForSequence(
669            $this->getCurrentSequenceElement()
670        );
671
672        if ($this->getAnswerChangedParameter() && !$this->isParticipantsAnswerFixed($questionId)) {
673            if ($this->saveQuestionSolution(true)) {
674                $this->removeIntermediateSolution();
675                $this->setAnswerChangedParameter(false);
676            } else {
677                $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
678            }
679            $this->testSequence->setQuestionChecked($questionId);
680            $this->testSequence->saveToDb();
681        } else if ($this->object->isForceInstantFeedbackEnabled()) {
682            $this->testSequence->setQuestionChecked($questionId);
683            $this->testSequence->saveToDb();
684        }
685
686        $this->ctrl->setParameter($this, 'instresp', 1);
687
688        // fau: testNav - handle navigation after feedback
689        if ($this->getNavigationUrlParameter()) {
690            $this->saveNavigationPreventConfirmation();
691            $this->registerForcedFeedbackNavUrl($this->getNavigationUrlParameter());
692        }
693        // fau.
694        $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
695    }
696
697    protected function handleQuestionActionCmd()
698    {
699        $questionId = $this->testSequence->getQuestionForSequence(
700            $this->getCurrentSequenceElement()
701        );
702
703        if (!$this->isParticipantsAnswerFixed($questionId)) {
704            $this->updateWorkingTime();
705            $this->saveQuestionSolution(false);
706            // fau: testNav - add changed status of the question
707            $this->setAnswerChangedParameter(true);
708            // fau.
709        }
710
711        $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
712    }
713
714    protected function performTearsAndAngerBrokenConfessionChecks()
715    {
716        if ($this->testSession->getActiveId() > 0) {
717            if ($this->testSequence->hasRandomQuestionsForPass($this->testSession->getActiveId(), $this->testSession->getPass()) > 0) {
718                // Something went wrong. Maybe the user pressed the start button twice
719                // Questions already exist so there is no need to create new questions
720
721                global $DIC;
722                $ilLog = $DIC['ilLog'];
723                $ilUser = $DIC['ilUser'];
724
725                $ilLog->write(
726                    __METHOD__ . ' Random Questions allready exists for user ' .
727                    $ilUser->getId() . ' in test ' . $this->object->getTestId()
728                );
729
730                return true;
731            }
732        } else {
733            // This may not happen! If it happens, raise a fatal error...
734
735            global $DIC;
736            $ilLog = $DIC['ilLog'];
737            $ilUser = $DIC['ilUser'];
738
739            $ilLog->write(__METHOD__ . ' ' . sprintf(
740                $this->lng->txt("error_random_question_generation"),
741                $ilUser->getId(),
742                $this->object->getTestId()
743            ));
744
745            return true;
746        };
747
748        return false;
749    }
750
751    protected function generateRandomTestPassForActiveUser()
752    {
753        global $DIC;
754        $tree = $DIC['tree'];
755        $ilDB = $DIC['ilDB'];
756        $ilPluginAdmin = $DIC['ilPluginAdmin'];
757
758        require_once 'Modules/Test/classes/class.ilTestRandomQuestionSetConfig.php';
759        $questionSetConfig = new ilTestRandomQuestionSetConfig($tree, $ilDB, $ilPluginAdmin, $this->object);
760        $questionSetConfig->loadFromDb();
761
762        require_once 'Modules/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinitionFactory.php';
763        $sourcePoolDefinitionFactory = new ilTestRandomQuestionSetSourcePoolDefinitionFactory($ilDB, $this->object);
764
765        require_once 'Modules/Test/classes/class.ilTestRandomQuestionSetSourcePoolDefinitionList.php';
766        $sourcePoolDefinitionList = new ilTestRandomQuestionSetSourcePoolDefinitionList($ilDB, $this->object, $sourcePoolDefinitionFactory);
767        $sourcePoolDefinitionList->loadDefinitions();
768
769        $this->processLocker->executeRandomPassBuildOperation(function () use ($ilDB, $ilPluginAdmin, $questionSetConfig, $sourcePoolDefinitionList) {
770            if (!$this->performTearsAndAngerBrokenConfessionChecks()) {
771                require_once 'Modules/Test/classes/class.ilTestRandomQuestionSetStagingPoolQuestionList.php';
772                $stagingPoolQuestionList = new ilTestRandomQuestionSetStagingPoolQuestionList($ilDB, $ilPluginAdmin);
773
774                require_once 'Modules/Test/classes/class.ilTestRandomQuestionSetBuilder.php';
775                $questionSetBuilder = ilTestRandomQuestionSetBuilder::getInstance($ilDB, $this->object, $questionSetConfig, $sourcePoolDefinitionList, $stagingPoolQuestionList);
776
777                $questionSetBuilder->performBuild($this->testSession);
778            }
779        }, $sourcePoolDefinitionList->hasTaxonomyFilters());
780    }
781
782    /**
783     * Resume a test at the last position
784     */
785    protected function resumePlayerCmd()
786    {
787        $this->handleUserSettings();
788
789        $active_id = $this->testSession->getActiveId();
790        $this->ctrl->setParameter($this, "active_id", $active_id);
791
792        $active_time_id = $this->object->startWorkingTime($active_id, $this->testSession->getPass());
793        $_SESSION["active_time_id"] = $active_time_id;
794        $_SESSION['tst_pass_finish'] = 0;
795
796        if ($this->object->isRandomTest()) {
797            if (!$this->testSequence->hasRandomQuestionsForPass($active_id, $this->testSession->getPass())) {
798                // create a new set of random questions
799                $this->generateRandomTestPassForActiveUser();
800            }
801        }
802
803        $shuffle = $this->object->getShuffleQuestions();
804        if ($this->object->isRandomTest()) {
805            $shuffle = false;
806        }
807
808        assQuestion::_updateTestPassResults(
809            $active_id,
810            $this->testSession->getPass(),
811            $this->object->areObligationsEnabled(),
812            null,
813            $this->object->id
814        );
815
816        // ensure existing test sequence
817        if (!$this->testSequence->hasSequence()) {
818            $this->testSequence->createNewSequence($this->object->getQuestionCount(), $shuffle);
819            $this->testSequence->saveToDb();
820        }
821
822        if ($this->object->getListOfQuestionsStart()) {
823            $this->ctrl->redirect($this, ilTestPlayerCommands::QUESTION_SUMMARY);
824        }
825
826        $this->ctrl->setParameter($this, 'sequence', $this->testSession->getLastSequence());
827        $this->ctrl->setParameter($this, 'pmode', '');
828        $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
829    }
830
831    protected function isShowingPostponeStatusReguired($questionId)
832    {
833        return $this->testSequence->isPostponedQuestion($questionId);
834    }
835
836    protected function adoptUserSolutionsFromPreviousPass()
837    {
838        global $DIC;
839        $ilDB = $DIC['ilDB'];
840        $ilUser = $DIC['ilUser'];
841
842        $assSettings = new ilSetting('assessment');
843
844        include_once("./Modules/Test/classes/class.ilObjAssessmentFolder.php");
845        $isAssessmentLogEnabled = ilObjAssessmentFolder::_enabledAssessmentLogging();
846
847        require_once 'Modules/TestQuestionPool/classes/class.ilAssQuestionUserSolutionAdopter.php';
848        $userSolutionAdopter = new ilAssQuestionUserSolutionAdopter($ilDB, $assSettings, $isAssessmentLogEnabled);
849
850        $userSolutionAdopter->setUserId($ilUser->getId());
851        $userSolutionAdopter->setActiveId($this->testSession->getActiveId());
852        $userSolutionAdopter->setTargetPass($this->testSequence->getPass());
853        $userSolutionAdopter->setQuestionIds($this->testSequence->getOptionalQuestions());
854
855        $userSolutionAdopter->perform();
856    }
857
858    abstract protected function populateQuestionOptionalMessage();
859
860    protected function isOptionalQuestionAnsweringConfirmationRequired($sequenceKey)
861    {
862        if ($this->testSequence->isAnsweringOptionalQuestionsConfirmed()) {
863            return false;
864        }
865
866        $questionId = $this->testSequence->getQuestionForSequence($sequenceKey);
867
868        if (!$this->testSequence->isQuestionOptional($questionId)) {
869            return false;
870        }
871
872        return true;
873    }
874
875    protected function isQuestionSummaryFinishTestButtonRequired()
876    {
877        return true;
878    }
879
880    protected function handleTearsAndAngerNoObjectiveOrientedQuestion()
881    {
882        ilUtil::sendFailure(sprintf($this->lng->txt('tst_objective_oriented_test_pass_without_questions'), $this->object->getTitle()), true);
883
884        $this->backToInfoScreenCmd();
885    }
886
887    /**
888     * @param ilTestNavigationToolbarGUI $navigationToolbarGUI
889     */
890    protected function handlePrimaryButton(ilTestNavigationToolbarGUI $navigationToolbarGUI, $currentQuestionId)
891    {
892        $isNextPrimary = true;
893
894        if ($this->object->isForceInstantFeedbackEnabled()) {
895            $isNextPrimary = false;
896        }
897
898        $questionsMissingResult = assQuestion::getQuestionsMissingResultRecord(
899            $this->testSession->getActiveId(),
900            $this->testSession->getPass(),
901            $this->testSequence->getOrderedSequenceQuestions()
902        );
903
904        if (!count($questionsMissingResult)) {
905            $navigationToolbarGUI->setFinishTestButtonPrimary(true);
906            $isNextPrimary = false;
907        } elseif (count($questionsMissingResult) == 1) {
908            $lastOpenQuestion = current($questionsMissingResult);
909
910            if ($currentQuestionId == $lastOpenQuestion) {
911                $navigationToolbarGUI->setFinishTestButtonPrimary(true);
912                $isNextPrimary = false;
913            }
914        }
915
916        return $isNextPrimary;
917    }
918}
919