1<?php
2
3/* Copyright (c) 1998-2013 ILIAS open source, Extended GPL, see docs/LICENSE */
4
5/**
6 * Class ilTestCorrectionsGUI
7 *
8 * @author    Björn Heyser <info@bjoernheyser.de>
9 * @version    $Id$
10 *
11 * @package    Modules/Test
12 */
13class ilTestCorrectionsGUI
14{
15    /**
16     * @var \ILIAS\DI\Container
17     */
18    protected $DIC;
19
20    /**
21     * @var ilObjTest
22     */
23    protected $testOBJ;
24
25    /**
26     * @var ilTestAccess
27     */
28    protected $testAccess;
29
30    /**
31     * ilTestCorrectionsGUI constructor.
32     * @param \ILIAS\DI\Container $DIC
33     * @param ilObjTest $testOBJ
34     */
35    public function __construct(\ILIAS\DI\Container $DIC, ilObjTest $testOBJ)
36    {
37        $this->DIC = $DIC;
38        $this->testOBJ = $testOBJ;
39
40        $this->testAccess = new ilTestAccess($testOBJ->getRefId(), $testOBJ->getTestId());
41    }
42
43    public function executeCommand()
44    {
45        if (!$this->testAccess->checkCorrectionsAccess()) {
46            ilObjTestGUI::accessViolationRedirect();
47        }
48
49        if (isset($_GET['eqid']) && (int) $_GET["eqid"] && isset($_GET['eqpl']) && (int) $_GET["eqpl"]) {
50            $this->DIC->ctrl()->setParameter($this, 'qid', (int) $_GET["eqid"]);
51            $this->DIC->ctrl()->redirect($this, 'showQuestion');
52        }
53
54        if (isset($_GET['removeQid']) && (int) $_GET['removeQid']) {
55            $this->DIC->ctrl()->setParameter($this, 'qid', (int) $_GET['removeQid']);
56            $this->DIC->ctrl()->redirect($this, 'confirmQuestionRemoval');
57        }
58
59        if ((int) $_GET['qid'] && !$this->checkQuestion((int) $_GET['qid'])) {
60            ilObjTestGUI::accessViolationRedirect();
61        }
62
63        $this->DIC->ctrl()->saveParameter($this, 'qid');
64
65        switch ($this->DIC->ctrl()->getNextClass($this)) {
66            default:
67
68                $command = $this->DIC->ctrl()->getCmd('showQuestionList');
69                $this->{$command}();
70        }
71    }
72
73    protected function showQuestionList()
74    {
75        $this->DIC->tabs()->activateTab(ilTestTabsManager::TAB_ID_CORRECTION);
76
77        $ui = $this->DIC->ui();
78
79        if ($this->testOBJ->isFixedTest()) {
80            $table_gui = new ilTestQuestionsTableGUI(
81                $this,
82                'showQuestionList',
83                $this->testOBJ->getRefId()
84            );
85
86            $table_gui->setQuestionTitleLinksEnabled(true);
87            $table_gui->setQuestionRemoveRowButtonEnabled(true);
88            $table_gui->init();
89
90            $table_gui->setData($this->getQuestions());
91
92            $rendered_gui_component = $table_gui->getHTML();
93        } else {
94            $lng = $this->DIC->language();
95            $txt = $lng->txt('tst_corrections_incompatible_question_set_type');
96
97            $infoBox = $ui->factory()->messageBox()->info($txt);
98
99            $rendered_gui_component = $ui->renderer()->render($infoBox);
100        }
101
102        $ui->mainTemplate()->setContent($rendered_gui_component);
103    }
104
105    protected function showQuestion(ilPropertyFormGUI $form = null)
106    {
107        $questionGUI = $this->getQuestion((int) $_GET['qid']);
108
109        $this->setCorrectionTabsContext($questionGUI, 'question');
110
111        if ($form === null) {
112            $form = $this->buildQuestionCorrectionForm($questionGUI);
113        }
114
115        $this->populatePageTitleAndDescription($questionGUI);
116        $this->DIC->ui()->mainTemplate()->setContent($form->getHTML());
117    }
118
119    protected function saveQuestion()
120    {
121        $questionGUI = $this->getQuestion((int) $_GET['qid']);
122
123        $form = $this->buildQuestionCorrectionForm($questionGUI);
124
125        $form->setValuesByPost();
126
127        if (!$form->checkInput()) {
128            $questionGUI->prepareReprintableCorrectionsForm($form);
129
130            $this->showQuestion($form);
131            return;
132        }
133
134        $questionGUI->saveCorrectionsFormProperties($form);
135        $questionGUI->object->setPoints($questionGUI->object->getMaximumPoints());
136        $questionGUI->object->saveToDb();
137
138        $scoring = new ilTestScoring($this->testOBJ);
139        $scoring->setPreserveManualScores(false);
140        $scoring->setQuestionId($questionGUI->object->getId());
141        $scoring->recalculateSolutions();
142
143        $this->DIC->ctrl()->redirect($this, 'showQuestion');
144    }
145
146    protected function buildQuestionCorrectionForm(assQuestionGUI $questionGUI)
147    {
148        $form = new ilPropertyFormGUI();
149        $form->setFormAction($this->DIC->ctrl()->getFormAction($this));
150        $form->setId('tst_question_correction');
151
152        $form->setTitle($this->DIC->language()->txt('tst_corrections_qst_form'));
153
154        $hiddenQid = new ilHiddenInputGUI('qid');
155        $hiddenQid->setValue($questionGUI->object->getId());
156        $form->addItem($hiddenQid);
157
158        $questionGUI->populateCorrectionsFormProperties($form);
159
160        $scoring = new ilTestScoring($this->testOBJ);
161        $scoring->setQuestionId($questionGUI->object->getId());
162
163        if ($scoring->getNumManualScorings()) {
164            $form->addCommandButton('confirmManualScoringReset', $this->DIC->language()->txt('save'));
165        } else {
166            $form->addCommandButton('saveQuestion', $this->DIC->language()->txt('save'));
167        }
168
169        return $form;
170    }
171
172    protected function addHiddenItemsFromArray(ilConfirmationGUI $gui, $array, $curPath = array())
173    {
174        foreach ($array as $name => $value) {
175            if ($name == 'cmd' && !count($curPath)) {
176                continue;
177            }
178
179            if (count($curPath)) {
180                $name = "[{$name}]";
181            }
182
183            if (is_array($value)) {
184                $nextPath = array_merge($curPath, array($name));
185                $this->addHiddenItemsFromArray($gui, $value, $nextPath);
186            } else {
187                $postVar = implode('', $curPath) . $name;
188                $gui->addHiddenItem($postVar, $value);
189            }
190        }
191    }
192
193    protected function confirmManualScoringReset()
194    {
195        $questionGUI = $this->getQuestion((int) $_GET['qid']);
196
197        $this->setCorrectionTabsContext($questionGUI, 'question');
198
199        $scoring = new ilTestScoring($this->testOBJ);
200        $scoring->setQuestionId($questionGUI->object->getId());
201
202        $confirmation = sprintf(
203            $this->DIC->language()->txt('tst_corrections_manscore_reset_warning'),
204            $scoring->getNumManualScorings(),
205            $questionGUI->object->getTitle(),
206            $questionGUI->object->getId()
207        );
208
209        $gui = new ilConfirmationGUI();
210        $gui->setHeaderText($confirmation);
211        $gui->setFormAction($this->DIC->ctrl()->getFormAction($this));
212        $gui->setCancel($this->DIC->language()->txt('cancel'), 'showQuestion');
213        $gui->setConfirm($this->DIC->language()->txt('confirm'), 'saveQuestion');
214
215        $this->addHiddenItemsFromArray($gui, $_POST);
216
217        $this->DIC->ui()->mainTemplate()->setContent($gui->getHTML());
218    }
219
220    protected function showSolution()
221    {
222        $questionGUI = $this->getQuestion((int) $_GET['qid']);
223
224        $this->setCorrectionTabsContext($questionGUI, 'solution');
225
226        $pageGUI = new ilAssQuestionPageGUI($questionGUI->object->getId());
227        $pageGUI->setRenderPageContainer(false);
228        $pageGUI->setEditPreview(true);
229        $pageGUI->setEnabledTabs(false);
230
231        $solutionHTML = $questionGUI->getSolutionOutput(
232            0,
233            null,
234            false,
235            false,
236            true,
237            false,
238            true,
239            false,
240            true
241        );
242
243        $pageGUI->setQuestionHTML(array($questionGUI->object->getId() => $solutionHTML));
244        $pageGUI->setPresentationTitle($questionGUI->object->getTitle());
245
246        $tpl = new ilTemplate('tpl.tst_corrections_solution_presentation.html', true, true, 'Modules/Test');
247        $tpl->setVariable('SOLUTION_PRESENTATION', $pageGUI->preview());
248
249        $this->populatePageTitleAndDescription($questionGUI);
250
251        $this->DIC->ui()->mainTemplate()->setContent($tpl->get());
252        $this->DIC->ui()->mainTemplate()->addCss('Modules/Test/templates/default/ta.css');
253
254        $this->DIC->ui()->mainTemplate()->setCurrentBlock("ContentStyle");
255        $stylesheet = ilObjStyleSheet::getContentStylePath(0);
256        $this->DIC->ui()->mainTemplate()->setVariable("LOCATION_CONTENT_STYLESHEET", $stylesheet);
257        $this->DIC->ui()->mainTemplate()->parseCurrentBlock();
258
259        $this->DIC->ui()->mainTemplate()->setCurrentBlock("SyntaxStyle");
260        $stylesheet = ilObjStyleSheet::getSyntaxStylePath();
261        $this->DIC->ui()->mainTemplate()->setVariable("LOCATION_SYNTAX_STYLESHEET", $stylesheet);
262        $this->DIC->ui()->mainTemplate()->parseCurrentBlock();
263    }
264
265    protected function showAnswerStatistic()
266    {
267        $questionGUI = $this->getQuestion((int) $_GET['qid']);
268        $solutions = $this->getSolutions($questionGUI->object);
269
270        $this->setCorrectionTabsContext($questionGUI, 'answers');
271
272        $tablesHtml = '';
273
274        foreach ($questionGUI->getSubQuestionsIndex() as $subQuestionIndex) {
275            $table = $questionGUI->getAnswerFrequencyTableGUI(
276                $this,
277                'showAnswerStatistic',
278                $solutions,
279                $subQuestionIndex
280            );
281
282            $tablesHtml .= $table->getHTML() . $table->getAdditionalHtml();
283        }
284
285        $this->populatePageTitleAndDescription($questionGUI);
286        $this->DIC->ui()->mainTemplate()->setContent($tablesHtml);
287        $this->DIC->ui()->mainTemplate()->addCss('Modules/Test/templates/default/ta.css');
288    }
289
290    protected function addAnswerAsynch()
291    {
292        $response = new stdClass();
293
294        $form = new ilAddAnswerModalFormGUI();
295        $form->build();
296        $form->setValuesByPost();
297
298        if (!$form->checkInput()) {
299            $uid = md5($form->getInput('answer'));
300
301            $form->setId($uid);
302            $form->setFormAction($this->DIC->ctrl()->getFormAction($this, 'addAnswerAsynch'));
303
304            $alert = $this->DIC->ui()->factory()->messageBox()->failure(
305                $this->DIC->language()->txt('form_input_not_valid')
306            );
307
308            $bodyTpl = new ilTemplate('tpl.tst_corr_addanswermodal.html', true, true, 'Modules/TestQuestionPool');
309            $bodyTpl->setVariable('MESSAGE', $this->DIC->ui()->renderer()->render($alert));
310            $bodyTpl->setVariable('FORM', $form->getHTML());
311            $bodyTpl->setVariable('BODY_UID', $uid);
312
313            $response->result = false;
314            $response->html = $bodyTpl->get();
315
316            echo json_encode($response);
317            exit;
318        }
319
320        $qid = (int) $form->getInput('qid');
321
322        if (!$this->checkQuestion($qid)) {
323            $response->html = '';
324            $response->result = false;
325
326            echo json_encode($response);
327            exit;
328        }
329
330        $questionGUI = $this->getQuestion($qid);
331
332        $qIndex = (int) $form->getInput('qindex');
333        $points = (float) $form->getInput('points');
334        $answerOption = $form->getInput('answer');
335
336        if ($questionGUI->object->isAddableAnswerOptionValue($qIndex, $answerOption)) {
337            $questionGUI->object->addAnswerOptionValue($qIndex, $answerOption, $points);
338            $questionGUI->object->saveToDb();
339        }
340
341        $scoring = new ilTestScoring($this->testOBJ);
342        $scoring->setPreserveManualScores(true);
343        $scoring->recalculateSolutions();
344
345        $response->result = true;
346
347        echo json_encode($response);
348        exit;
349    }
350
351    protected function confirmQuestionRemoval()
352    {
353        $this->DIC->tabs()->activateTab(ilTestTabsManager::TAB_ID_CORRECTION);
354
355        $questionGUI = $this->getQuestion((int) $_GET['qid']);
356
357        $confirmation = sprintf(
358            $this->DIC->language()->txt('tst_corrections_qst_remove_confirmation'),
359            $questionGUI->object->getTitle(),
360            $questionGUI->object->getId()
361        );
362
363        $buttons = array(
364            $this->DIC->ui()->factory()->button()->standard(
365                $this->DIC->language()->txt('confirm'),
366                $this->DIC->ctrl()->getLinkTarget($this, 'performQuestionRemoval')
367            ),
368            $this->DIC->ui()->factory()->button()->standard(
369                $this->DIC->language()->txt('cancel'),
370                $this->DIC->ctrl()->getLinkTarget($this, 'showQuestionList')
371            )
372        );
373
374        $this->DIC->ui()->mainTemplate()->setContent($this->DIC->ui()->renderer()->render(
375            $this->DIC->ui()->factory()->messageBox()->confirmation($confirmation)->withButtons($buttons)
376        ));
377    }
378
379    protected function performQuestionRemoval()
380    {
381        global $DIC; /* @var ILIAS\DI\Container $DIC */
382
383        $questionGUI = $this->getQuestion((int) $_GET['qid']);
384        $scoring = new ilTestScoring($this->testOBJ);
385
386        $participantData = new ilTestParticipantData($DIC->database(), $DIC->language());
387        $participantData->load($this->testOBJ->getTestId());
388
389        // remove question solutions
390        $questionGUI->object->removeAllExistingSolutions();
391
392        // remove test question results
393        $scoring->removeAllQuestionResults($questionGUI->object->getId());
394
395        // remove question from test and reindex remaining questions
396        $this->testOBJ->removeQuestion($questionGUI->object->getId());
397        $reindexedSequencePositionMap = $this->testOBJ->reindexFixedQuestionOrdering();
398        $this->testOBJ->loadQuestions();
399
400        // remove questions from all sequences
401        $this->testOBJ->removeQuestionFromSequences(
402            $questionGUI->object->getId(),
403            $participantData->getActiveIds(),
404            $reindexedSequencePositionMap
405        );
406
407        // update pass and test results
408        $scoring->updatePassAndTestResults($participantData->getActiveIds());
409
410        // trigger learning progress
411        ilLPStatusWrapper::_refreshStatus($this->testOBJ->getId(), $participantData->getUserIds());
412
413        // finally delete the question itself
414        $questionGUI->object->delete($questionGUI->object->getId());
415
416        // check for empty test and set test offline
417        if (!count($this->testOBJ->getTestQuestions())) {
418            $this->testOBJ->setOnline(false);
419            $this->testOBJ->saveToDb(true);
420        }
421
422        $this->DIC->ctrl()->setParameter($this, 'qid', '');
423        $this->DIC->ctrl()->redirect($this, 'showQuestionList');
424    }
425
426    protected function setCorrectionTabsContext(assQuestionGUI $questionGUI, $activeTabId)
427    {
428        $this->DIC->tabs()->clearTargets();
429        $this->DIC->tabs()->clearSubTabs();
430
431        $this->DIC->tabs()->setBackTarget(
432            $this->DIC->language()->txt('back'),
433            $this->DIC->ctrl()->getLinkTarget($this, 'showQuestionList')
434        );
435
436        $this->DIC->tabs()->addTab(
437            'question',
438            $this->DIC->language()->txt('tst_corrections_tab_question'),
439            $this->DIC->ctrl()->getLinkTarget($this, 'showQuestion')
440        );
441
442        $this->DIC->tabs()->addTab(
443            'solution',
444            $this->DIC->language()->txt('tst_corrections_tab_solution'),
445            $this->DIC->ctrl()->getLinkTarget($this, 'showSolution')
446        );
447
448        if ($questionGUI->isAnswerFreuqencyStatisticSupported()) {
449            $this->DIC->tabs()->addTab(
450                'answers',
451                $this->DIC->language()->txt('tst_corrections_tab_statistics'),
452                $this->DIC->ctrl()->getLinkTarget($this, 'showAnswerStatistic')
453            );
454        }
455
456        $this->DIC->tabs()->activateTab($activeTabId);
457    }
458
459    /**
460     * @param assQuestionGUI $questionGUI
461     */
462    protected function populatePageTitleAndDescription(assQuestionGUI $questionGUI)
463    {
464        $this->DIC->ui()->mainTemplate()->setTitle($questionGUI->object->getTitle());
465        $this->DIC->ui()->mainTemplate()->setDescription($questionGUI->outQuestionType());
466    }
467
468    /**
469     * @param int $qId
470     * @return bool
471     */
472    protected function checkQuestion($qId)
473    {
474        if (!$this->testOBJ->isTestQuestion($qId)) {
475            return false;
476        }
477
478        $questionGUI = $this->getQuestion($qId);
479
480        if (!$this->supportsAdjustment($questionGUI)) {
481            return false;
482        }
483
484        if (!$this->allowedInAdjustment($questionGUI)) {
485            return false;
486        }
487
488        return true;
489    }
490
491    /**
492     * @param int $qId
493     * @return assQuestionGUI
494     */
495    protected function getQuestion($qId)
496    {
497        $question = assQuestion::instantiateQuestionGUI($qId);
498        $question->object->setObjId($this->testOBJ->getId());
499
500        return $question;
501    }
502
503    protected function getSolutions(assQuestion $question)
504    {
505        $solutionRows = array();
506
507        foreach ($this->testOBJ->getParticipants() as $activeId => $participantData) {
508            $passesSelector = new ilTestPassesSelector($this->DIC->database(), $this->testOBJ);
509            $passesSelector->setActiveId($activeId);
510            $passesSelector->loadLastFinishedPass();
511
512            foreach ($passesSelector->getClosedPasses() as $pass) {
513                foreach ($question->getSolutionValues($activeId, $pass) as $row) {
514                    $solutionRows[] = $row;
515                }
516            }
517        }
518
519        return $solutionRows;
520    }
521
522    /**
523     * @return array
524     */
525    protected function getQuestions() : array
526    {
527        $questions = array();
528
529        foreach ($this->testOBJ->getTestQuestions() as $questionData) {
530            $questionGUI = $this->getQuestion($questionData['question_id']);
531
532            if (!$this->supportsAdjustment($questionGUI)) {
533                continue;
534            }
535
536            if (!$this->allowedInAdjustment($questionGUI)) {
537                continue;
538            }
539
540            $questions[] = $questionData;
541        }
542
543        return $questions;
544    }
545
546    /**
547     * Returns if the given question object support scoring adjustment.
548     *
549     * @param $question_object assQuestionGUI
550     *
551     * @return bool True, if relevant interfaces are implemented to support scoring adjustment.
552     */
553    protected function supportsAdjustment(\assQuestionGUI $question_object)
554    {
555        return ($question_object instanceof ilGuiQuestionScoringAdjustable
556                || $question_object instanceof ilGuiAnswerScoringAdjustable)
557            && ($question_object->object instanceof ilObjQuestionScoringAdjustable
558                || $question_object->object instanceof ilObjAnswerScoringAdjustable);
559    }
560
561    /**
562     * Returns if the question type is allowed for adjustments in the global test administration.
563     *
564     * @param assQuestionGUI $question_object
565     * @return bool
566     */
567    protected function allowedInAdjustment(\assQuestionGUI $question_object)
568    {
569        $setting = new ilSetting('assessment');
570        $types = explode(',', $setting->get('assessment_scoring_adjustment'));
571        require_once './Modules/TestQuestionPool/classes/class.ilObjQuestionPool.php';
572        $type_def = array();
573        foreach ($types as $type) {
574            $type_def[$type] = ilObjQuestionPool::getQuestionTypeByTypeId($type);
575        }
576
577        $type = $question_object->getQuestionType();
578        if (in_array($type, $type_def)) {
579            return true;
580        }
581        return false;
582    }
583}
584