1<?php
2/* Copyright (c) 1998-2013 ILIAS open source, Extended GPL, see docs/LICENSE */
3
4require_once './Modules/TestQuestionPool/classes/class.assQuestionGUI.php';
5require_once './Modules/TestQuestionPool/interfaces/interface.ilGuiQuestionScoringAdjustable.php';
6require_once './Modules/TestQuestionPool/interfaces/interface.ilGuiAnswerScoringAdjustable.php';
7require_once './Modules/Test/classes/inc.AssessmentConstants.php';
8
9/**
10 * Ordering question GUI representation
11 *
12 * The assOrderingQuestionGUI class encapsulates the GUI representation for ordering questions.
13 *
14 * @author	Helmut Schottmüller <helmut.schottmueller@mac.com>
15 * @author	Björn Heyser <bheyser@databay.de>
16 * @author	Maximilian Becker <mbecker@databay.de>
17 *
18 * @version	$Id$
19 *
20 * @ingroup ModulesTestQuestionPool
21 * @ilCtrl_Calls assOrderingQuestionGUI: ilFormPropertyDispatchGUI
22 */
23class assOrderingQuestionGUI extends assQuestionGUI implements ilGuiQuestionScoringAdjustable, ilGuiAnswerScoringAdjustable
24{
25    /**
26     * @var assOrderingQuestion
27     */
28    public $object;
29
30    public $old_ordering_depth = array();
31    public $leveled_ordering = array();
32
33    /**
34     * @var bool
35     */
36    private $clearAnswersOnWritingPostDataEnabled;
37
38    /**
39     * assOrderingQuestionGUI constructor
40     *
41     * The constructor takes possible arguments an creates an instance of the assOrderingQuestionGUI object.
42     *
43     * @param integer $id The database id of a ordering question object
44     *
45     * @return assOrderingQuestionGUI
46     */
47    public function __construct($id = -1)
48    {
49        parent::__construct();
50        include_once "./Modules/TestQuestionPool/classes/class.assOrderingQuestion.php";
51        $this->object = new assOrderingQuestion();
52        if ($id >= 0) {
53            $this->object->loadFromDb($id);
54        }
55        $this->clearAnswersOnWritingPostDataEnabled = false;
56    }
57
58    /**
59     * @param boolean $clearAnswersOnWritingPostDataEnabled
60     */
61    public function setClearAnswersOnWritingPostDataEnabled($clearAnswersOnWritingPostDataEnabled)
62    {
63        $this->clearAnswersOnWritingPostDataEnabled = $clearAnswersOnWritingPostDataEnabled;
64    }
65
66    /**
67     * @return boolean
68     */
69    public function isClearAnswersOnWritingPostDataEnabled()
70    {
71        return $this->clearAnswersOnWritingPostDataEnabled;
72    }
73
74    public function changeToPictures()
75    {
76        if ($this->object->getOrderingType() != OQ_NESTED_PICTURES && $this->object->getOrderingType() != OQ_PICTURES) {
77            $this->setClearAnswersOnWritingPostDataEnabled(true);
78        }
79
80        $form = $this->buildEditForm();
81        $form->setValuesByPost();
82        $this->persistAuthoringForm($form);
83
84        $this->object->setOrderingType(OQ_PICTURES);
85        $this->object->saveToDb();
86
87        $form->ensureReprintableFormStructure($this->object);
88        $this->renderEditForm($form);
89    }
90
91    public function changeToText()
92    {
93        if ($this->object->getOrderingType() != OQ_NESTED_TERMS && $this->object->getOrderingType() != OQ_TERMS) {
94            $this->setClearAnswersOnWritingPostDataEnabled(true);
95        }
96
97        $form = $this->buildEditForm();
98        $form->setValuesByPost();
99        $this->persistAuthoringForm($form);
100
101        $this->object->setOrderingType(OQ_TERMS);
102        $this->object->saveToDb();
103
104        $form->ensureReprintableFormStructure($this->object);
105        $this->renderEditForm($form);
106    }
107
108    public function orderNestedTerms()
109    {
110        $this->writePostData(true);
111        $this->object->setOrderingType(OQ_NESTED_TERMS);
112        $this->object->saveToDb();
113
114        $this->renderEditForm($this->buildEditForm());
115    }
116
117    public function orderNestedPictures()
118    {
119        $this->writePostData(true);
120        $this->object->setOrderingType(OQ_NESTED_PICTURES);
121        $this->object->saveToDb();
122
123        $this->renderEditForm($this->buildEditForm());
124    }
125
126    public function removeElementImage()
127    {
128        $orderingInput = $this->object->buildOrderingImagesInputGui();
129        $this->object->initOrderingElementAuthoringProperties($orderingInput);
130
131        $form = $this->buildEditForm();
132        $form->replaceFormItemByPostVar($orderingInput);
133        $form->setValuesByPost();
134
135        $replacementElemList = ilAssOrderingElementList::buildInstance(
136            $this->object->getId(),
137            array()
138        );
139
140        $storedElementList = $this->object->getOrderingElementList();
141
142        foreach ($orderingInput->getElementList($this->object->getId()) as $submittedElement) {
143            if ($submittedElement->isImageRemovalRequest()) {
144                if ($this->object->isImageFileStored($submittedElement->getContent())) {
145                    $this->object->dropImageFile($submittedElement->getContent());
146                }
147
148                $submittedElement->setContent(null);
149            }
150
151            if ($storedElementList->elementExistByRandomIdentifier($submittedElement->getRandomIdentifier())) {
152                $storedElement = $storedElementList->getElementByRandomIdentifier(
153                    $submittedElement->getRandomIdentifier()
154                );
155
156                $submittedElement->setSolutionIdentifier($storedElement->getSolutionIdentifier());
157                $submittedElement->setIndentation($storedElement->getIndentation());
158            }
159
160            $replacementElemList->addElement($submittedElement);
161        }
162
163        $replacementElemList->saveToDb();
164
165        $orderingInput->setElementList($replacementElemList);
166        $this->renderEditForm($form);
167    }
168
169    public function uploadElementImage()
170    {
171        $orderingInput = $this->object->buildOrderingImagesInputGui();
172        $this->object->initOrderingElementAuthoringProperties($orderingInput);
173
174        $form = $this->buildEditForm();
175        $form->replaceFormItemByPostVar($orderingInput);
176        $form->setValuesByPost();
177
178        if (!$orderingInput->checkInput()) {
179            ilUtil::sendFailure($this->lng->txt('form_input_not_valid'));
180        }
181
182        $this->writeAnswerSpecificPostData($form);
183        $this->object->getOrderingElementList()->saveToDb();
184        $orderingInput->setElementList($this->object->getOrderingElementList());
185
186        $this->renderEditForm($form);
187    }
188
189    public function writeQuestionSpecificPostData(ilPropertyFormGUI $form)
190    {
191        $this->object->setThumbGeometry($_POST["thumb_geometry"]);
192       // $this->object->setElementHeight($_POST["element_height"]);
193        //$this->object->setOrderingType( $_POST["ordering_type"] );
194        $this->object->setPoints($_POST["points"]);
195    }
196
197    public function writeAnswerSpecificPostData(ilPropertyFormGUI $form)
198    {
199        if (!is_array($_POST[assOrderingQuestion::ORDERING_ELEMENT_FORM_FIELD_POSTVAR])) {
200            throw new ilTestQuestionPoolException('form submit request missing the form submit!?');
201        }
202
203        #$submittedElementList = $this->object->fetchSolutionListFromFormSubmissionData($_POST);
204        $submittedElementList = $this->object->fetchSolutionListFromSubmittedForm($form);
205
206        $replacementElementList = new ilAssOrderingElementList();
207        $replacementElementList->setQuestionId($this->object->getId());
208
209        $currentElementList = $this->object->getOrderingElementList();
210
211        foreach ($submittedElementList as $submittedElement) {
212            if ($this->object->hasOrderingTypeUploadSupport()) {
213                if ($submittedElement->isImageUploadAvailable()) {
214                    $suffix = strtolower(array_pop(explode(".", $submittedElement->getUploadImageName())));
215                    if (in_array($suffix, array("jpg", "jpeg", "png", "gif"))) {
216                        $submittedElement->setUploadImageName($this->object->buildHashedImageFilename(
217                            $submittedElement->getUploadImageName(),
218                            true
219                        ));
220
221                        $wasImageFileStored = $this->object->storeImageFile(
222                            $submittedElement->getUploadImageFile(),
223                            $submittedElement->getUploadImageName()
224                        );
225
226                        if ($wasImageFileStored) {
227                            if ($this->object->isImageFileStored($submittedElement->getContent())) {
228                                $this->object->dropImageFile($submittedElement->getContent());
229                            }
230
231                            $submittedElement->setContent($submittedElement->getUploadImageName());
232                        }
233                    }
234                }
235            }
236
237            if ($currentElementList->elementExistByRandomIdentifier($submittedElement->getRandomIdentifier())) {
238                $storedElement = $currentElementList->getElementByRandomIdentifier(
239                    $submittedElement->getRandomIdentifier()
240                );
241
242                $submittedElement->setSolutionIdentifier($storedElement->getSolutionIdentifier());
243
244                if ($this->isAdjustmentEditContext() || $this->object->isOrderingTypeNested()) {
245                    $submittedElement->setContent($storedElement->getContent());
246                }
247
248                if (!$this->object->isOrderingTypeNested()) {
249                    $submittedElement->setIndentation($storedElement->getIndentation());
250                }
251
252                if ($this->object->isImageReplaced($submittedElement, $storedElement)) {
253                    $this->object->dropImageFile($storedElement->getContent());
254                }
255            }
256
257            $replacementElementList->addElement($submittedElement);
258        }
259
260        if ($this->object->isImageOrderingType()) {
261            $this->object->handleThumbnailCreation($replacementElementList);
262        }
263
264        if ($this->isClearAnswersOnWritingPostDataEnabled()) {
265            $replacementElementList->clearElementContents();
266        }
267
268        if ($this->object->hasOrderingTypeUploadSupport()) {
269            $obsoleteElementList = $currentElementList->getDifferenceElementList($replacementElementList);
270
271            foreach ($obsoleteElementList as $obsoleteElement) {
272                $this->object->dropImageFile($obsoleteElement->getContent());
273            }
274        }
275
276        $this->object->setOrderingElementList($replacementElementList);
277    }
278
279    public function populateAnswerSpecificFormPart(ilPropertyFormGUI $form)
280    {
281        $header = new ilFormSectionHeaderGUI();
282        $header->setTitle($this->lng->txt('oq_header_ordering_elements'));
283        $form->addItem($header);
284
285        if ($this->isAdjustmentEditContext()) {
286            $orderingElementInput = $this->object->buildNestedOrderingElementInputGui();
287        } else {
288            $orderingElementInput = $this->object->buildOrderingElementInputGui();
289        }
290
291        $orderingElementInput->setStylingDisabled($this->isRenderPurposePrintPdf());
292        $this->object->initOrderingElementAuthoringProperties($orderingElementInput);
293
294        $orderingElementInput->setElementList($this->object->getOrderingElementList());
295
296        $form->addItem($orderingElementInput);
297
298        return $form;
299    }
300
301    public function populateQuestionSpecificFormPart(\ilPropertyFormGUI $form)
302    {
303        if ($this->object->isImageOrderingType()) {
304            $geometry = new ilNumberInputGUI($this->lng->txt("thumb_geometry"), "thumb_geometry");
305            $geometry->setValue($this->object->getThumbGeometry());
306            $geometry->setRequired(true);
307            $geometry->setMaxLength(6);
308            $geometry->setMinValue(20);
309            $geometry->setSize(6);
310            $geometry->setInfo($this->lng->txt("thumb_geometry_info"));
311            $form->addItem($geometry);
312        }
313
314        // points
315        $points = new ilNumberInputGUI($this->lng->txt("points"), "points");
316        $points->allowDecimals(true);
317        $points->setValue($this->object->getPoints());
318        $points->setRequired(true);
319        $points->setSize(3);
320        $points->setMinValue(0);
321        $points->setMinvalueShouldBeGreater(true);
322        $form->addItem($points);
323
324        return $form;
325    }
326
327    /**
328     * {@inheritdoc}
329     */
330    protected function writePostData($forceSaving = false)
331    {
332        $savingAllowed = true; // assume saving allowed first
333
334        if (!$forceSaving) {
335            // this case seems to be a regular save call, so we consider
336            // the validation result for the decision of saving as well
337
338            // inits {this->editForm} and performs validation
339            $form = $this->buildEditForm();
340            $form->setValuesByPost(); // manipulation and distribution of values
341
342            if (!$form->checkInput()) { // manipulations regular style input propeties
343                $form->prepareValuesReprintable($this->object);
344                $this->renderEditForm($form);
345
346                // consequence of vaidation
347                $savingAllowed = false;
348            }
349        } elseif (!$this->isSaveCommand()) {
350            // this case handles form workflow actions like the mode/view switching requests,
351            // so saving must not be skipped, even for inputs invalid by business rules
352
353            $form = $this->buildEditForm();
354            $form->setValuesByPost(); // manipulation and distribution of values
355            $form->checkInput(); // manipulations regular style input propeties
356        }
357
358        if ($savingAllowed) {
359            $this->persistAuthoringForm($form);
360
361            return 0; // return 0 = all fine, was saved either forced or validated
362        }
363
364        return 1; // return 1 = something went wrong, no saving happened
365    }
366
367    /**
368     * Creates an output of the edit form for the question
369     */
370    public function editQuestion($checkonly = false)
371    {
372        $this->renderEditForm($this->buildEditForm());
373    }
374
375    /**
376     * @return ilAssOrderingQuestionAuthoringFormGUI
377     */
378    protected function buildEditForm()
379    {
380        require_once 'Modules/TestQuestionPool/classes/forms/class.ilAssOrderingQuestionAuthoringFormGUI.php';
381        $form = new ilAssOrderingQuestionAuthoringFormGUI();
382        $this->editForm = $form;
383
384        $form->setFormAction($this->ctrl->getFormAction($this));
385        $form->setTitle($this->outQuestionType());
386        $form->setMultipart(($this->object->getOrderingType() == OQ_PICTURES) ? true : false);
387        $form->setTableWidth("100%");
388        $form->setId("ordering");
389        // title, author, description, question, working time (assessment mode)
390        $this->addBasicQuestionFormProperties($form);
391        $this->populateQuestionSpecificFormPart($form);
392        $this->populateAnswerSpecificFormPart($form);
393
394        $this->populateTaxonomyFormSection($form);
395
396        $form->addSpecificOrderingQuestionCommandButtons($this->object);
397        $form->addGenericAssessmentQuestionCommandButtons($this->object);
398
399        return $form;
400    }
401
402    /**
403     * Question type specific support of intermediate solution output
404     * The function getSolutionOutput respects getUseIntermediateSolution()
405     * @return bool
406     */
407    public function supportsIntermediateSolutionOutput()
408    {
409        return true;
410    }
411
412    /**
413     * Get the question solution output
414     *
415     * @param integer $active_id             The active user id
416     * @param integer $pass                  The test pass
417     * @param boolean $graphicalOutput       Show visual feedback for right/wrong answers
418     * @param boolean $result_output         Show the reached points for parts of the question
419     * @param boolean $show_question_only    Show the question without the ILIAS content around
420     * @param boolean $show_feedback         Show the question feedback
421     * @param boolean $show_correct_solution Show the correct solution instead of the user solution
422     * @param boolean $show_manual_scoring   Show specific information for the manual scoring output
423     * @param bool    $show_question_text
424     *
425     * @return string The solution output of the question as HTML code
426     */
427    public function getSolutionOutput(
428        $active_id,
429        $pass = null,
430        $graphicalOutput = false,
431        $result_output = false,
432        $show_question_only = true,
433        $show_feedback = false,
434        $forceCorrectSolution = false,
435        $show_manual_scoring = false,
436        $show_question_text = true
437    ) {
438        $solutionOrderingList = $this->object->getOrderingElementListForSolutionOutput(
439            $forceCorrectSolution,
440            $active_id,
441            $pass,
442            $this->getUseIntermediateSolution()
443        );
444
445        $answers_gui = $this->object->buildNestedOrderingElementInputGui();
446
447        if ($forceCorrectSolution) {
448            $answers_gui->setContext(ilAssNestedOrderingElementsInputGUI::CONTEXT_CORRECT_SOLUTION_PRESENTATION);
449        } else {
450            $answers_gui->setContext(ilAssNestedOrderingElementsInputGUI::CONTEXT_USER_SOLUTION_PRESENTATION);
451        }
452
453        $answers_gui->setInteractionEnabled(false);
454
455        $answers_gui->setElementList($solutionOrderingList);
456
457        $answers_gui->setCorrectnessTrueElementList(
458            $solutionOrderingList->getParityTrueElementList($this->object->getOrderingElementList())
459        );
460
461        $solution_html = $answers_gui->getHTML();
462
463        $template = new ilTemplate("tpl.il_as_qpl_nested_ordering_output_solution.html", true, true, "Modules/TestQuestionPool");
464        $template->setVariable('SOLUTION_OUTPUT', $solution_html);
465        if ($show_question_text == true) {
466            $template->setVariable("QUESTIONTEXT", $this->object->prepareTextareaOutput($this->object->getQuestion(), true));
467        }
468        $questionoutput = $template->get();
469
470        $solutiontemplate = new ilTemplate("tpl.il_as_tst_solution_output.html", true, true, "Modules/TestQuestionPool");
471        $solutiontemplate->setVariable("SOLUTION_OUTPUT", $questionoutput);
472
473        if ($show_feedback) {
474            $feedback = '';
475
476            if (!$this->isTestPresentationContext()) {
477                $fb = $this->getGenericFeedbackOutput($active_id, $pass);
478                $feedback .= strlen($fb) ? $fb : '';
479            }
480
481            $fb = $this->getSpecificFeedbackOutput(array());
482            $feedback .= strlen($fb) ? $fb : '';
483
484            if (strlen($feedback)) {
485                $cssClass = (
486                    $this->hasCorrectSolution($active_id, $pass) ?
487                    ilAssQuestionFeedback::CSS_CLASS_FEEDBACK_CORRECT : ilAssQuestionFeedback::CSS_CLASS_FEEDBACK_WRONG
488                );
489
490                $solutiontemplate->setVariable("ILC_FB_CSS_CLASS", $cssClass);
491                $solutiontemplate->setVariable("FEEDBACK", $this->object->prepareTextareaOutput($feedback, true));
492            }
493        }
494
495        if ($show_question_only) {
496            return $solutiontemplate->get();
497        }
498
499        return $this->getILIASPage($solutiontemplate->get());
500
501        // is this template still in use? it is not used at this point any longer!
502        // $template = new ilTemplate("tpl.il_as_qpl_ordering_output_solution.html", TRUE, TRUE, "Modules/TestQuestionPool");
503    }
504
505    public function getPreview($show_question_only = false, $showInlineFeedback = false)
506    {
507        if ($this->getPreviewSession() && $this->getPreviewSession()->hasParticipantSolution()) {
508            $solutionOrderingElementList = unserialize(
509                $this->getPreviewSession()->getParticipantsSolution()
510            );
511        } else {
512            $solutionOrderingElementList = $this->object->getShuffledOrderingElementList();
513        }
514
515        $answers = $this->object->buildNestedOrderingElementInputGui();
516        $answers->setNestingEnabled($this->object->isOrderingTypeNested());
517        $answers->setContext(ilAssNestedOrderingElementsInputGUI::CONTEXT_QUESTION_PREVIEW);
518        $answers->setInteractionEnabled($this->isInteractivePresentation());
519        $answers->setElementList($solutionOrderingElementList);
520
521        $template = new ilTemplate("tpl.il_as_qpl_ordering_output.html", true, true, "Modules/TestQuestionPool");
522
523        $template->setCurrentBlock('nested_ordering_output');
524        $template->setVariable('NESTED_ORDERING', $answers->getHTML());
525        $template->parseCurrentBlock();
526
527        $template->setVariable("QUESTIONTEXT", $this->object->prepareTextareaOutput($this->object->getQuestion(), true));
528
529        if ($show_question_only) {
530            return $template->get();
531        }
532
533        return $this->getILIASPage($template->get());
534
535        //$this->tpl->addJavascript("./Modules/TestQuestionPool/templates/default/ordering.js");
536    }
537
538    public function getPresentationJavascripts()
539    {
540        global $DIC; /* @var ILIAS\DI\Container $DIC */
541
542        $files = array();
543
544        if ($DIC['ilBrowser']->isMobile() || $DIC['ilBrowser']->isIpad()) {
545            $files[] = './libs/bower/bower_components/jqueryui-touch-punch/jquery.ui.touch-punch.min.js';
546        }
547
548        return $files;
549    }
550
551    // hey: prevPassSolutions - pass will be always available from now on
552    public function getTestOutput($activeId, $pass, $isPostponed = false, $userSolutionPost = false, $inlineFeedback = false)
553    // hey.
554    {
555        // hey: prevPassSolutions - fixed variable type, makes phpstorm stop crying
556        $userSolutionPost = is_array($userSolutionPost) ? $userSolutionPost : array();
557        // hey.
558
559        $orderingGUI = $this->object->buildNestedOrderingElementInputGui();
560        $orderingGUI->setNestingEnabled($this->object->isOrderingTypeNested());
561
562        $solutionOrderingElementList = $this->object->getSolutionOrderingElementListForTestOutput(
563            $orderingGUI,
564            $userSolutionPost,
565            $activeId,
566            $pass
567        );
568
569        $template = new ilTemplate('tpl.il_as_qpl_ordering_output.html', true, true, 'Modules/TestQuestionPool');
570
571        $orderingGUI->setContext(ilAssNestedOrderingElementsInputGUI::CONTEXT_USER_SOLUTION_SUBMISSION);
572        $orderingGUI->setElementList($solutionOrderingElementList);
573
574        $template->setCurrentBlock('nested_ordering_output');
575        $template->setVariable('NESTED_ORDERING', $orderingGUI->getHTML());
576        $template->parseCurrentBlock();
577
578        $template->setVariable('QUESTIONTEXT', $this->object->prepareTextareaOutput($this->object->getQuestion(), true));
579
580        $pageoutput = $this->outQuestionPage('', $isPostponed, $activeId, $template->get());
581
582        return $pageoutput;
583    }
584
585    protected function isInteractivePresentation()
586    {
587        if ($this->isRenderPurposePlayback()) {
588            return true;
589        }
590
591        if ($this->isRenderPurposeDemoplay()) {
592            return true;
593        }
594
595        return false;
596    }
597
598    /**
599     * Sets the ILIAS tabs for this question type
600     *
601     * @access public
602     *
603     * @todo:	MOVE THIS STEPS TO COMMON QUESTION CLASS assQuestionGUI
604     */
605    public function setQuestionTabs()
606    {
607        global $DIC;
608        $rbacsystem = $DIC['rbacsystem'];
609        $ilTabs = $DIC['ilTabs'];
610
611        $ilTabs->clearTargets();
612
613        $this->ctrl->setParameterByClass("ilAssQuestionPageGUI", "q_id", $_GET["q_id"]);
614        include_once "./Modules/TestQuestionPool/classes/class.assQuestion.php";
615        $q_type = $this->object->getQuestionType();
616
617        if (strlen($q_type)) {
618            $classname = $q_type . "GUI";
619            $this->ctrl->setParameterByClass(strtolower($classname), "sel_question_types", $q_type);
620            $this->ctrl->setParameterByClass(strtolower($classname), "q_id", $_GET["q_id"]);
621        }
622
623        if ($_GET["q_id"]) {
624            if ($rbacsystem->checkAccess('write', $_GET["ref_id"])) {
625                // edit page
626                $ilTabs->addTarget(
627                    "edit_page",
628                    $this->ctrl->getLinkTargetByClass("ilAssQuestionPageGUI", "edit"),
629                    array("edit", "insert", "exec_pg"),
630                    "",
631                    "",
632                    false
633                );
634            }
635
636            $this->addTab_QuestionPreview($ilTabs);
637        }
638
639        $force_active = false;
640        if ($rbacsystem->checkAccess('write', $_GET["ref_id"])) {
641            $url = "";
642            if ($classname) {
643                $url = $this->ctrl->getLinkTargetByClass($classname, "editQuestion");
644            }
645            $commands = $_POST["cmd"];
646            if (is_array($commands)) {
647                foreach ($commands as $key => $value) {
648                    if (preg_match("/^delete_.*/", $key, $matches)) {
649                        $force_active = true;
650                    }
651                }
652            }
653            // edit question properties
654            $ilTabs->addTarget(
655                "edit_question",
656                $url,
657                array("orderNestedTerms","orderNestedPictures","editQuestion", "save", "saveEdit", "addanswers", "removeanswers", "changeToPictures", "uploadElementImage", "changeToText", "upanswers", "downanswers", "originalSyncForm"),
658                $classname,
659                "",
660                $force_active
661            );
662        }
663
664        // add tab for question feedback within common class assQuestionGUI
665        $this->addTab_QuestionFeedback($ilTabs);
666
667        // add tab for question hint within common class assQuestionGUI
668        $this->addTab_QuestionHints($ilTabs);
669
670        // add tab for question's suggested solution within common class assQuestionGUI
671        $this->addTab_SuggestedSolution($ilTabs, $classname);
672
673        // Assessment of questions sub menu entry
674        if ($_GET["q_id"]) {
675            $ilTabs->addTarget(
676                "statistics",
677                $this->ctrl->getLinkTargetByClass($classname, "assessment"),
678                array("assessment"),
679                $classname,
680                ""
681            );
682        }
683
684        $this->addBackTab($ilTabs);
685    }
686
687    public function getSpecificFeedbackOutput($userSolution)
688    {
689        if (!$this->object->feedbackOBJ->specificAnswerFeedbackExists()) {
690            return '';
691        }
692
693        $tpl = new ilTemplate('tpl.il_as_qpl_ordering_elem_fb.html', true, true, 'Modules/TestQuestionPool');
694
695        foreach ($this->object->getOrderingElementList() as $element) {
696            $feedback = $this->object->feedbackOBJ->getSpecificAnswerFeedbackTestPresentation(
697                $this->object->getId(),
698                0,
699                $element->getPosition()
700            );
701
702            if ($this->object->isImageOrderingType()) {
703                $imgSrc = $this->object->getImagePathWeb() . $element->getContent();
704                $tpl->setCurrentBlock('image');
705                $tpl->setVariable('IMG_SRC', $imgSrc);
706            } else {
707                $tpl->setCurrentBlock('text');
708            }
709            $tpl->setVariable('CONTENT', $element->getContent());
710            $tpl->parseCurrentBlock();
711
712            $tpl->setCurrentBlock('element');
713            $tpl->setVariable('FEEDBACK', $feedback);
714            $tpl->parseCurrentBlock();
715        }
716
717        return $this->object->prepareTextareaOutput($tpl->get(), true);
718    }
719
720    /**
721     * @param $form
722     * @throws ilTestQuestionPoolException
723     */
724    protected function persistAuthoringForm($form)
725    {
726        $this->writeQuestionGenericPostData();
727        $this->writeQuestionSpecificPostData($form);
728        $this->writeAnswerSpecificPostData($form);
729        $this->saveTaxonomyAssignments();
730    }
731
732    private function getOldLeveledOrdering()
733    {
734        global $DIC;
735        $ilDB = $DIC['ilDB'];
736
737        $res = $ilDB->queryF(
738            'SELECT depth FROM qpl_a_ordering WHERE question_fi = %s ORDER BY solution_key ASC',
739            array('integer'),
740            array($this->object->getId())
741        );
742        while ($row = $ilDB->fetchAssoc($res)) {
743            $this->old_ordering_depth[] = $row['depth'];
744        }
745        return $this->old_ordering_depth;
746    }
747
748    /**
749     * Returns a list of postvars which will be suppressed in the form output when used in scoring adjustment.
750     * The form elements will be shown disabled, so the users see the usual form but can only edit the settings, which
751     * make sense in the given context.
752     *
753     * E.g. array('cloze_type', 'image_filename')
754     *
755     * @return string[]
756     */
757    public function getAfterParticipationSuppressionAnswerPostVars()
758    {
759        return array();
760    }
761
762    /**
763     * Returns a list of postvars which will be suppressed in the form output when used in scoring adjustment.
764     * The form elements will be shown disabled, so the users see the usual form but can only edit the settings, which
765     * make sense in the given context.
766     *
767     * E.g. array('cloze_type', 'image_filename')
768     *
769     * @return string[]
770     */
771    public function getAfterParticipationSuppressionQuestionPostVars()
772    {
773        return array();
774    }
775
776    /**
777     * Returns an html string containing a question specific representation of the answers so far
778     * given in the test for use in the right column in the scoring adjustment user interface.
779     *
780     * @param array $relevant_answers
781     *
782     * @return string
783     */
784    public function getAggregatedAnswersView($relevant_answers)
785    {
786        $aggView = $this->aggregateAnswers(
787            $relevant_answers,
788            $this->object->getOrderingElementList()
789        );
790
791        return  $this->renderAggregateView($aggView)->get();
792    }
793
794    public function aggregateAnswers($relevant_answers_chosen, $answers_defined_on_question)
795    {
796        $passdata = array(); // Regroup answers into units of passes.
797        foreach ($relevant_answers_chosen as $answer_chosen) {
798            $passdata[$answer_chosen['active_fi'] . '-' . $answer_chosen['pass']][$answer_chosen['value2']] = $answer_chosen['value1'];
799        }
800
801        $variants = array(); // Determine unique variants.
802        foreach ($passdata as $key => $data) {
803            $hash = md5(implode('-', $data));
804            $value_set = false;
805            foreach ($variants as $vkey => $variant) {
806                if ($variant['hash'] == $hash) {
807                    $variant['count']++;
808                    $value_set = true;
809                }
810            }
811            if (!$value_set) {
812                $variants[$key]['hash'] = $hash;
813                $variants[$key]['count'] = 1;
814            }
815        }
816
817        $aggregate = array(); // Render aggregate from variant.
818        foreach ($variants as $key => $variant_entry) {
819            $variant = $passdata[$key];
820
821            foreach ($variant as $variant_key => $variant_line) {
822                $i = 0;
823                $aggregated_info_for_answer['count'] = $variant_entry['count'];
824                foreach ($answers_defined_on_question as $element) {
825                    $i++;
826
827                    if ($this->object->isImageOrderingType()) {
828                        $element->setImageThumbnailPrefix($this->object->getThumbPrefix());
829                        $element->setImagePathWeb($this->object->getImagePathWeb());
830                        $element->setImagePathFs($this->object->getImagePath());
831
832                        $src = $element->getPresentationImageUrl();
833                        $alt = $element->getContent();
834                        $content = "<img src='{$src}' alt='{$alt}' title='{$alt}'/>";
835                    } else {
836                        $content = $element->getContent();
837                    }
838
839                    $aggregated_info_for_answer[$i . ' - ' . $content]
840                        = $passdata[$key][$i];
841                }
842            }
843            $aggregate[] = $aggregated_info_for_answer;
844        }
845        return $aggregate;
846    }
847
848    /**
849     * @param $aggregate
850     *
851     * @return ilTemplate
852     */
853    public function renderAggregateView($aggregate)
854    {
855        $tpl = new ilTemplate('tpl.il_as_aggregated_answers_table.html', true, true, "Modules/TestQuestionPool");
856
857        foreach ($aggregate as $line_data) {
858            $tpl->setCurrentBlock('aggregaterow');
859            $count = array_shift($line_data);
860            $html = '<ul>';
861            foreach ($line_data as $key => $line) {
862                $html .= '<li>' . ++$line . '&nbsp;-&nbsp;' . $key . '</li>';
863            }
864            $html .= '</ul>';
865            $tpl->setVariable('COUNT', $count);
866            $tpl->setVariable('OPTION', $html);
867
868            $tpl->parseCurrentBlock();
869        }
870        return $tpl;
871    }
872
873    protected function getAnswerStatisticOrderingElementHtml(ilAssOrderingElement $element)
874    {
875        if ($this->object->isImageOrderingType()) {
876            $element->setImageThumbnailPrefix($this->object->getThumbPrefix());
877            $element->setImagePathWeb($this->object->getImagePathWeb());
878            $element->setImagePathFs($this->object->getImagePath());
879
880            $src = $element->getPresentationImageUrl();
881            $alt = $element->getContent();
882            $content = "<img src='{$src}' alt='{$alt}' title='{$alt}'/>";
883        } else {
884            $content = $element->getContent();
885        }
886
887        return $content;
888    }
889
890    protected function getAnswerStatisticOrderingVariantHtml(ilAssOrderingElementList $list)
891    {
892        $html = '<ul>';
893
894        $lastIndent = 0;
895        $firstElem = true;
896
897        foreach ($list as $elem) {
898            if ($elem->getIndentation() > $lastIndent) {
899                $html .= '<ul><li>';
900            } elseif ($elem->getIndentation() < $lastIndent) {
901                $html .= '</li></ul><li>';
902            } elseif (!$firstElem) {
903                $html .= '</li><li>';
904            } else {
905                $html .= '<li>';
906            }
907
908            $html .= $this->getAnswerStatisticOrderingElementHtml($elem);
909
910            $firstElem = false;
911            $lastIndent = $elem->getIndentation();
912        }
913
914        $html .= '</li>';
915
916        for ($i = $lastIndent; $i > 0; $i--) {
917            $html .= '</ul></li>';
918        }
919
920        $html .= '</ul>';
921
922        return $html;
923    }
924
925    public function getAnswersFrequency($relevantAnswers, $questionIndex)
926    {
927        $answersByActiveAndPass = array();
928
929        foreach ($relevantAnswers as $row) {
930            $key = $row['active_fi'] . ':' . $row['pass'];
931
932            if (!isset($answersByActiveAndPass[$key])) {
933                $answersByActiveAndPass[$key] = array();
934            }
935
936            $answersByActiveAndPass[$key][$row['value1']] = $row['value2'];
937        }
938
939        $solutionLists = array();
940
941        foreach ($answersByActiveAndPass as $indexedSolutions) {
942            $solutionLists[] = $this->object->getSolutionOrderingElementList($indexedSolutions);
943        }
944
945        /* @var ilAssOrderingElementList[] $answers */
946        $answers = array();
947
948        foreach ($solutionLists as $orderingElementList) {
949            $hash = $orderingElementList->getHash();
950
951            if (!isset($answers[$hash])) {
952                $variantHtml = $this->getAnswerStatisticOrderingVariantHtml(
953                    $orderingElementList
954                );
955
956                $answers[$hash] = array(
957                    'answer' => $variantHtml, 'frequency' => 0
958                );
959            }
960
961            $answers[$hash]['frequency']++;
962        }
963
964        return array_values($answers);
965    }
966
967    /**
968     * @param ilPropertyFormGUI $form
969     */
970    public function prepareReprintableCorrectionsForm(ilPropertyFormGUI $form)
971    {
972        $orderingInput = $form->getItemByPostVar(assOrderingQuestion::ORDERING_ELEMENT_FORM_FIELD_POSTVAR);
973        $orderingInput->prepareReprintable($this->object);
974    }
975
976    /**
977     * @param ilPropertyFormGUI $form
978     */
979    public function populateCorrectionsFormProperties(ilPropertyFormGUI $form)
980    {
981        $points = new ilNumberInputGUI($this->lng->txt("points"), "points");
982        $points->allowDecimals(true);
983        $points->setValue($this->object->getPoints());
984        $points->setRequired(true);
985        $points->setSize(3);
986        $points->setMinValue(0);
987        $points->setMinvalueShouldBeGreater(true);
988        $form->addItem($points);
989
990        $header = new ilFormSectionHeaderGUI();
991        $header->setTitle($this->lng->txt('oq_header_ordering_elements'));
992        $form->addItem($header);
993
994        $orderingElementInput = $this->object->buildNestedOrderingElementInputGui();
995
996        $this->object->initOrderingElementAuthoringProperties($orderingElementInput);
997
998        $orderingElementInput->setElementList($this->object->getOrderingElementList());
999
1000        $form->addItem($orderingElementInput);
1001    }
1002
1003    /**
1004     * @param ilPropertyFormGUI $form
1005     */
1006    public function saveCorrectionsFormProperties(ilPropertyFormGUI $form)
1007    {
1008        $this->object->setPoints((float) $form->getInput('points'));
1009
1010        $submittedElementList = $this->object->fetchSolutionListFromSubmittedForm($form);
1011
1012        $curElementList = $this->object->getOrderingElementList();
1013
1014        $newElementList = new ilAssOrderingElementList();
1015        $newElementList->setQuestionId($this->object->getId());
1016
1017        foreach ($submittedElementList as $submittedElement) {
1018            if (!$curElementList->elementExistByRandomIdentifier($submittedElement->getRandomIdentifier())) {
1019                continue;
1020            }
1021
1022            $curElement = $curElementList->getElementByRandomIdentifier($submittedElement->getRandomIdentifier());
1023
1024            $curElement->setPosition($submittedElement->getPosition());
1025
1026            if ($this->object->isOrderingTypeNested()) {
1027                $curElement->setIndentation($submittedElement->getIndentation());
1028            }
1029
1030            $newElementList->addElement($curElement);
1031        }
1032
1033        $this->object->setOrderingElementList($newElementList);
1034    }
1035}
1036