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->getSelfAssessmentEditingMode()) {
304            $element_height = new ilNumberInputGUI($this->lng->txt("element_height"), "element_height");
305            $element_height->setValue($this->object->getElementHeight());
306            $element_height->setRequired(false);
307            $element_height->setMaxLength(6);
308            $element_height->setMinValue(20);
309            $element_height->setSize(6);
310            $element_height->setInfo($this->lng->txt("element_height_info"));
311            $form->addItem($element_height);
312        }
313
314        if ($this->object->isImageOrderingType()) {
315            $geometry = new ilNumberInputGUI($this->lng->txt("thumb_geometry"), "thumb_geometry");
316            $geometry->setValue($this->object->getThumbGeometry());
317            $geometry->setRequired(true);
318            $geometry->setMaxLength(6);
319            $geometry->setMinValue(20);
320            $geometry->setSize(6);
321            $geometry->setInfo($this->lng->txt("thumb_geometry_info"));
322            $form->addItem($geometry);
323        }
324
325        // points
326        $points = new ilNumberInputGUI($this->lng->txt("points"), "points");
327        $points->allowDecimals(true);
328        $points->setValue($this->object->getPoints());
329        $points->setRequired(true);
330        $points->setSize(3);
331        $points->setMinValue(0);
332        $points->setMinvalueShouldBeGreater(true);
333        $form->addItem($points);
334
335        return $form;
336    }
337
338    /**
339     * {@inheritdoc}
340     */
341    protected function writePostData($forceSaving = false)
342    {
343        $savingAllowed = true; // assume saving allowed first
344
345        if (!$forceSaving) {
346            // this case seems to be a regular save call, so we consider
347            // the validation result for the decision of saving as well
348
349            // inits {this->editForm} and performs validation
350            $form = $this->buildEditForm();
351            $form->setValuesByPost(); // manipulation and distribution of values
352
353            if (!$form->checkInput()) { // manipulations regular style input propeties
354                $form->prepareValuesReprintable($this->object);
355                $this->renderEditForm($form);
356
357                // consequence of vaidation
358                $savingAllowed = false;
359            }
360        } elseif (!$this->isSaveCommand()) {
361            // this case handles form workflow actions like the mode/view switching requests,
362            // so saving must not be skipped, even for inputs invalid by business rules
363
364            $form = $this->buildEditForm();
365            $form->setValuesByPost(); // manipulation and distribution of values
366            $form->checkInput(); // manipulations regular style input propeties
367        }
368
369        if ($savingAllowed) {
370            $this->persistAuthoringForm($form);
371
372            return 0; // return 0 = all fine, was saved either forced or validated
373        }
374
375        return 1; // return 1 = something went wrong, no saving happened
376    }
377
378    /**
379     * Creates an output of the edit form for the question
380     */
381    public function editQuestion($checkonly = false)
382    {
383        $this->renderEditForm($this->buildEditForm());
384    }
385
386    /**
387     * @return ilAssOrderingQuestionAuthoringFormGUI
388     */
389    protected function buildEditForm()
390    {
391        require_once 'Modules/TestQuestionPool/classes/forms/class.ilAssOrderingQuestionAuthoringFormGUI.php';
392        $form = new ilAssOrderingQuestionAuthoringFormGUI();
393        $this->editForm = $form;
394
395        $form->setFormAction($this->ctrl->getFormAction($this));
396        $form->setTitle($this->outQuestionType());
397        $form->setMultipart(($this->object->getOrderingType() == OQ_PICTURES) ? true : false);
398        $form->setTableWidth("100%");
399        $form->setId("ordering");
400        // title, author, description, question, working time (assessment mode)
401        $this->addBasicQuestionFormProperties($form);
402        $this->populateQuestionSpecificFormPart($form);
403        $this->populateAnswerSpecificFormPart($form);
404
405        $this->populateTaxonomyFormSection($form);
406
407        $form->addSpecificOrderingQuestionCommandButtons($this->object);
408        $form->addGenericAssessmentQuestionCommandButtons($this->object);
409
410        return $form;
411    }
412
413    /**
414     * Question type specific support of intermediate solution output
415     * The function getSolutionOutput respects getUseIntermediateSolution()
416     * @return bool
417     */
418    public function supportsIntermediateSolutionOutput()
419    {
420        return true;
421    }
422
423    /**
424     * Get the question solution output
425     *
426     * @param integer $active_id             The active user id
427     * @param integer $pass                  The test pass
428     * @param boolean $graphicalOutput       Show visual feedback for right/wrong answers
429     * @param boolean $result_output         Show the reached points for parts of the question
430     * @param boolean $show_question_only    Show the question without the ILIAS content around
431     * @param boolean $show_feedback         Show the question feedback
432     * @param boolean $show_correct_solution Show the correct solution instead of the user solution
433     * @param boolean $show_manual_scoring   Show specific information for the manual scoring output
434     * @param bool    $show_question_text
435     *
436     * @return string The solution output of the question as HTML code
437     */
438    public function getSolutionOutput(
439        $active_id,
440        $pass = null,
441        $graphicalOutput = false,
442        $result_output = false,
443        $show_question_only = true,
444        $show_feedback = false,
445        $forceCorrectSolution = false,
446        $show_manual_scoring = false,
447        $show_question_text = true
448    ) {
449        $solutionOrderingList = $this->object->getOrderingElementListForSolutionOutput(
450            $forceCorrectSolution,
451            $active_id,
452            $pass,
453            $this->getUseIntermediateSolution()
454        );
455
456        $answers_gui = $this->object->buildNestedOrderingElementInputGui();
457
458        if ($forceCorrectSolution) {
459            $answers_gui->setContext(ilAssNestedOrderingElementsInputGUI::CONTEXT_CORRECT_SOLUTION_PRESENTATION);
460        } else {
461            $answers_gui->setContext(ilAssNestedOrderingElementsInputGUI::CONTEXT_USER_SOLUTION_PRESENTATION);
462        }
463
464        $answers_gui->setInteractionEnabled(false);
465
466        $answers_gui->setElementList($solutionOrderingList);
467
468        $answers_gui->setCorrectnessTrueElementList(
469            $solutionOrderingList->getParityTrueElementList($this->object->getOrderingElementList())
470        );
471
472        $solution_html = $answers_gui->getHTML();
473
474        $template = new ilTemplate("tpl.il_as_qpl_nested_ordering_output_solution.html", true, true, "Modules/TestQuestionPool");
475        $template->setVariable('SOLUTION_OUTPUT', $solution_html);
476        if ($show_question_text == true) {
477            $template->setVariable("QUESTIONTEXT", $this->object->prepareTextareaOutput($this->object->getQuestion(), true));
478        }
479        $questionoutput = $template->get();
480
481        $solutiontemplate = new ilTemplate("tpl.il_as_tst_solution_output.html", true, true, "Modules/TestQuestionPool");
482        $solutiontemplate->setVariable("SOLUTION_OUTPUT", $questionoutput);
483
484        if ($show_feedback) {
485            $feedback = '';
486
487            if (!$this->isTestPresentationContext()) {
488                $fb = $this->getGenericFeedbackOutput($active_id, $pass);
489                $feedback .= strlen($fb) ? $fb : '';
490            }
491
492            $fb = $this->getSpecificFeedbackOutput(array());
493            $feedback .= strlen($fb) ? $fb : '';
494
495            if (strlen($feedback)) {
496                $cssClass = (
497                    $this->hasCorrectSolution($active_id, $pass) ?
498                    ilAssQuestionFeedback::CSS_CLASS_FEEDBACK_CORRECT : ilAssQuestionFeedback::CSS_CLASS_FEEDBACK_WRONG
499                );
500
501                $solutiontemplate->setVariable("ILC_FB_CSS_CLASS", $cssClass);
502                $solutiontemplate->setVariable("FEEDBACK", $this->object->prepareTextareaOutput($feedback, true));
503            }
504        }
505
506        if ($show_question_only) {
507            return $solutiontemplate->get();
508        }
509
510        return $this->getILIASPage($solutiontemplate->get());
511
512        // is this template still in use? it is not used at this point any longer!
513        // $template = new ilTemplate("tpl.il_as_qpl_ordering_output_solution.html", TRUE, TRUE, "Modules/TestQuestionPool");
514    }
515
516    public function getPreview($show_question_only = false, $showInlineFeedback = false)
517    {
518        if ($this->getPreviewSession() && $this->getPreviewSession()->hasParticipantSolution()) {
519            $solutionOrderingElementList = unserialize(
520                $this->getPreviewSession()->getParticipantsSolution()
521            );
522        } else {
523            $solutionOrderingElementList = $this->object->getShuffledOrderingElementList();
524        }
525
526        $answers = $this->object->buildNestedOrderingElementInputGui();
527        $answers->setNestingEnabled($this->object->isOrderingTypeNested());
528        $answers->setContext(ilAssNestedOrderingElementsInputGUI::CONTEXT_QUESTION_PREVIEW);
529        $answers->setInteractionEnabled($this->isInteractivePresentation());
530        $answers->setElementList($solutionOrderingElementList);
531
532        $template = new ilTemplate("tpl.il_as_qpl_ordering_output.html", true, true, "Modules/TestQuestionPool");
533
534        $template->setCurrentBlock('nested_ordering_output');
535        $template->setVariable('NESTED_ORDERING', $answers->getHTML());
536        $template->parseCurrentBlock();
537
538        $template->setVariable("QUESTIONTEXT", $this->object->prepareTextareaOutput($this->object->getQuestion(), true));
539
540        if ($show_question_only) {
541            return $template->get();
542        }
543
544        return $this->getILIASPage($template->get());
545
546        //$this->tpl->addJavascript("./Modules/TestQuestionPool/templates/default/ordering.js");
547    }
548
549    // hey: prevPassSolutions - pass will be always available from now on
550    public function getTestOutput($activeId, $pass, $isPostponed = false, $userSolutionPost = false, $inlineFeedback = false)
551    // hey.
552    {
553        // hey: prevPassSolutions - fixed variable type, makes phpstorm stop crying
554        $userSolutionPost = is_array($userSolutionPost) ? $userSolutionPost : array();
555        // hey.
556
557        $orderingGUI = $this->object->buildNestedOrderingElementInputGui();
558        $orderingGUI->setNestingEnabled($this->object->isOrderingTypeNested());
559
560        $solutionOrderingElementList = $this->object->getSolutionOrderingElementListForTestOutput(
561            $orderingGUI,
562            $userSolutionPost,
563            $activeId,
564            $pass
565        );
566
567        $template = new ilTemplate('tpl.il_as_qpl_ordering_output.html', true, true, 'Modules/TestQuestionPool');
568
569        $orderingGUI->setContext(ilAssNestedOrderingElementsInputGUI::CONTEXT_USER_SOLUTION_SUBMISSION);
570        $orderingGUI->setElementList($solutionOrderingElementList);
571
572        $template->setCurrentBlock('nested_ordering_output');
573        $template->setVariable('NESTED_ORDERING', $orderingGUI->getHTML());
574        $template->parseCurrentBlock();
575
576        $template->setVariable('QUESTIONTEXT', $this->object->prepareTextareaOutput($this->object->getQuestion(), true));
577
578        $pageoutput = $this->outQuestionPage('', $isPostponed, $activeId, $template->get());
579
580        return $pageoutput;
581    }
582
583    protected function isInteractivePresentation()
584    {
585        if ($this->isRenderPurposePlayback()) {
586            return true;
587        }
588
589        if ($this->isRenderPurposeDemoplay()) {
590            return true;
591        }
592
593        return false;
594    }
595
596    /**
597     * Sets the ILIAS tabs for this question type
598     *
599     * @access public
600     *
601     * @todo:	MOVE THIS STEPS TO COMMON QUESTION CLASS assQuestionGUI
602     */
603    public function setQuestionTabs()
604    {
605        global $DIC;
606        $rbacsystem = $DIC['rbacsystem'];
607        $ilTabs = $DIC['ilTabs'];
608
609        $ilTabs->clearTargets();
610
611        $this->ctrl->setParameterByClass("ilAssQuestionPageGUI", "q_id", $_GET["q_id"]);
612        include_once "./Modules/TestQuestionPool/classes/class.assQuestion.php";
613        $q_type = $this->object->getQuestionType();
614
615        if (strlen($q_type)) {
616            $classname = $q_type . "GUI";
617            $this->ctrl->setParameterByClass(strtolower($classname), "sel_question_types", $q_type);
618            $this->ctrl->setParameterByClass(strtolower($classname), "q_id", $_GET["q_id"]);
619        }
620
621        if ($_GET["q_id"]) {
622            if ($rbacsystem->checkAccess('write', $_GET["ref_id"])) {
623                // edit page
624                $ilTabs->addTarget(
625                    "edit_page",
626                    $this->ctrl->getLinkTargetByClass("ilAssQuestionPageGUI", "edit"),
627                    array("edit", "insert", "exec_pg"),
628                    "",
629                    "",
630                    false
631                );
632            }
633
634            $this->addTab_QuestionPreview($ilTabs);
635        }
636
637        $force_active = false;
638        if ($rbacsystem->checkAccess('write', $_GET["ref_id"])) {
639            $url = "";
640            if ($classname) {
641                $url = $this->ctrl->getLinkTargetByClass($classname, "editQuestion");
642            }
643            $commands = $_POST["cmd"];
644            if (is_array($commands)) {
645                foreach ($commands as $key => $value) {
646                    if (preg_match("/^delete_.*/", $key, $matches)) {
647                        $force_active = true;
648                    }
649                }
650            }
651            // edit question properties
652            $ilTabs->addTarget(
653                "edit_question",
654                $url,
655                array("orderNestedTerms","orderNestedPictures","editQuestion", "save", "saveEdit", "addanswers", "removeanswers", "changeToPictures", "uploadElementImage", "changeToText", "upanswers", "downanswers", "originalSyncForm"),
656                $classname,
657                "",
658                $force_active
659            );
660        }
661
662        // add tab for question feedback within common class assQuestionGUI
663        $this->addTab_QuestionFeedback($ilTabs);
664
665        // add tab for question hint within common class assQuestionGUI
666        $this->addTab_QuestionHints($ilTabs);
667
668        // add tab for question's suggested solution within common class assQuestionGUI
669        $this->addTab_SuggestedSolution($ilTabs, $classname);
670
671        // Assessment of questions sub menu entry
672        if ($_GET["q_id"]) {
673            $ilTabs->addTarget(
674                "statistics",
675                $this->ctrl->getLinkTargetByClass($classname, "assessment"),
676                array("assessment"),
677                $classname,
678                ""
679            );
680        }
681
682        $this->addBackTab($ilTabs);
683    }
684
685    public function getSpecificFeedbackOutput($userSolution)
686    {
687        if (!$this->object->feedbackOBJ->specificAnswerFeedbackExists()) {
688            return '';
689        }
690
691        $tpl = new ilTemplate('tpl.il_as_qpl_ordering_elem_fb.html', true, true, 'Modules/TestQuestionPool');
692
693        foreach ($this->object->getOrderingElementList() as $element) {
694            $feedback = $this->object->feedbackOBJ->getSpecificAnswerFeedbackTestPresentation(
695                $this->object->getId(),
696                0,
697                $element->getPosition()
698            );
699
700            if ($this->object->isImageOrderingType()) {
701                $imgSrc = $this->object->getImagePathWeb() . $element->getContent();
702                $tpl->setCurrentBlock('image');
703                $tpl->setVariable('IMG_SRC', $imgSrc);
704            } else {
705                $tpl->setCurrentBlock('text');
706            }
707            $tpl->setVariable('CONTENT', $element->getContent());
708            $tpl->parseCurrentBlock();
709
710            $tpl->setCurrentBlock('element');
711            $tpl->setVariable('FEEDBACK', $feedback);
712            $tpl->parseCurrentBlock();
713        }
714
715        return $this->object->prepareTextareaOutput($tpl->get(), true);
716    }
717
718    /**
719     * @param $form
720     * @throws ilTestQuestionPoolException
721     */
722    protected function persistAuthoringForm($form)
723    {
724        $this->writeQuestionGenericPostData();
725        $this->writeQuestionSpecificPostData($form);
726        $this->writeAnswerSpecificPostData($form);
727        $this->saveTaxonomyAssignments();
728    }
729
730    private function getOldLeveledOrdering()
731    {
732        global $DIC;
733        $ilDB = $DIC['ilDB'];
734
735        $res = $ilDB->queryF(
736            'SELECT depth FROM qpl_a_ordering WHERE question_fi = %s ORDER BY solution_key ASC',
737            array('integer'),
738            array($this->object->getId())
739        );
740        while ($row = $ilDB->fetchAssoc($res)) {
741            $this->old_ordering_depth[] = $row['depth'];
742        }
743        return $this->old_ordering_depth;
744    }
745
746    /**
747     * Returns a list of postvars which will be suppressed in the form output when used in scoring adjustment.
748     * The form elements will be shown disabled, so the users see the usual form but can only edit the settings, which
749     * make sense in the given context.
750     *
751     * E.g. array('cloze_type', 'image_filename')
752     *
753     * @return string[]
754     */
755    public function getAfterParticipationSuppressionAnswerPostVars()
756    {
757        return array();
758    }
759
760    /**
761     * Returns a list of postvars which will be suppressed in the form output when used in scoring adjustment.
762     * The form elements will be shown disabled, so the users see the usual form but can only edit the settings, which
763     * make sense in the given context.
764     *
765     * E.g. array('cloze_type', 'image_filename')
766     *
767     * @return string[]
768     */
769    public function getAfterParticipationSuppressionQuestionPostVars()
770    {
771        return array();
772    }
773
774    /**
775     * Returns an html string containing a question specific representation of the answers so far
776     * given in the test for use in the right column in the scoring adjustment user interface.
777     *
778     * @param array $relevant_answers
779     *
780     * @return string
781     */
782    public function getAggregatedAnswersView($relevant_answers)
783    {
784        $aggView = $this->aggregateAnswers(
785            $relevant_answers,
786            $this->object->getOrderingElementList()
787        );
788
789        return  $this->renderAggregateView($aggView)->get();
790    }
791
792    public function aggregateAnswers($relevant_answers_chosen, $answers_defined_on_question)
793    {
794        $passdata = array(); // Regroup answers into units of passes.
795        foreach ($relevant_answers_chosen as $answer_chosen) {
796            $passdata[$answer_chosen['active_fi'] . '-' . $answer_chosen['pass']][$answer_chosen['value2']] = $answer_chosen['value1'];
797        }
798
799        $variants = array(); // Determine unique variants.
800        foreach ($passdata as $key => $data) {
801            $hash = md5(implode('-', $data));
802            $value_set = false;
803            foreach ($variants as $vkey => $variant) {
804                if ($variant['hash'] == $hash) {
805                    $variant['count']++;
806                    $value_set = true;
807                }
808            }
809            if (!$value_set) {
810                $variants[$key]['hash'] = $hash;
811                $variants[$key]['count'] = 1;
812            }
813        }
814
815        $aggregate = array(); // Render aggregate from variant.
816        foreach ($variants as $key => $variant_entry) {
817            $variant = $passdata[$key];
818
819            foreach ($variant as $variant_key => $variant_line) {
820                $i = 0;
821                $aggregated_info_for_answer['count'] = $variant_entry['count'];
822                foreach ($answers_defined_on_question as $element) {
823                    $i++;
824
825                    if ($this->object->isImageOrderingType()) {
826                        $element->setImageThumbnailPrefix($this->object->getThumbPrefix());
827                        $element->setImagePathWeb($this->object->getImagePathWeb());
828                        $element->setImagePathFs($this->object->getImagePath());
829
830                        $src = $element->getPresentationImageUrl();
831                        $alt = $element->getContent();
832                        $content = "<img src='{$src}' alt='{$alt}' title='{$alt}'/>";
833                    } else {
834                        $content = $element->getContent();
835                    }
836
837                    $aggregated_info_for_answer[$i . ' - ' . $content]
838                        = $passdata[$key][$i];
839                }
840            }
841            $aggregate[] = $aggregated_info_for_answer;
842        }
843        return $aggregate;
844    }
845
846    /**
847     * @param $aggregate
848     *
849     * @return ilTemplate
850     */
851    public function renderAggregateView($aggregate)
852    {
853        $tpl = new ilTemplate('tpl.il_as_aggregated_answers_table.html', true, true, "Modules/TestQuestionPool");
854
855        foreach ($aggregate as $line_data) {
856            $tpl->setCurrentBlock('aggregaterow');
857            $count = array_shift($line_data);
858            $html = '<ul>';
859            foreach ($line_data as $key => $line) {
860                $html .= '<li>' . ++$line . '&nbsp;-&nbsp;' . $key . '</li>';
861            }
862            $html .= '</ul>';
863            $tpl->setVariable('COUNT', $count);
864            $tpl->setVariable('OPTION', $html);
865
866            $tpl->parseCurrentBlock();
867        }
868        return $tpl;
869    }
870
871    protected function getAnswerStatisticOrderingElementHtml(ilAssOrderingElement $element)
872    {
873        if ($this->object->isImageOrderingType()) {
874            $element->setImageThumbnailPrefix($this->object->getThumbPrefix());
875            $element->setImagePathWeb($this->object->getImagePathWeb());
876            $element->setImagePathFs($this->object->getImagePath());
877
878            $src = $element->getPresentationImageUrl();
879            $alt = $element->getContent();
880            $content = "<img src='{$src}' alt='{$alt}' title='{$alt}'/>";
881        } else {
882            $content = $element->getContent();
883        }
884
885        return $content;
886    }
887
888    protected function getAnswerStatisticOrderingVariantHtml(ilAssOrderingElementList $list)
889    {
890        $html = '<ul>';
891
892        $lastIndent = 0;
893        $firstElem = true;
894
895        foreach ($list as $elem) {
896            if ($elem->getIndentation() > $lastIndent) {
897                $html .= '<ul><li>';
898            } elseif ($elem->getIndentation() < $lastIndent) {
899                $html .= '</li></ul><li>';
900            } elseif (!$firstElem) {
901                $html .= '</li><li>';
902            } else {
903                $html .= '<li>';
904            }
905
906            $html .= $this->getAnswerStatisticOrderingElementHtml($elem);
907
908            $firstElem = false;
909            $lastIndent = $elem->getIndentation();
910        }
911
912        $html .= '</li>';
913
914        for ($i = $lastIndent; $i > 0; $i--) {
915            $html .= '</ul></li>';
916        }
917
918        $html .= '</ul>';
919
920        return $html;
921    }
922
923    public function getAnswersFrequency($relevantAnswers, $questionIndex)
924    {
925        $answersByActiveAndPass = array();
926
927        foreach ($relevantAnswers as $row) {
928            $key = $row['active_fi'] . ':' . $row['pass'];
929
930            if (!isset($answersByActiveAndPass[$key])) {
931                $answersByActiveAndPass[$key] = array();
932            }
933
934            $answersByActiveAndPass[$key][$row['value1']] = $row['value2'];
935        }
936
937        $solutionLists = array();
938
939        foreach ($answersByActiveAndPass as $indexedSolutions) {
940            $solutionLists[] = $this->object->getSolutionOrderingElementList($indexedSolutions);
941        }
942
943        /* @var ilAssOrderingElementList[] $answers */
944        $answers = array();
945
946        foreach ($solutionLists as $orderingElementList) {
947            $hash = $orderingElementList->getHash();
948
949            if (!isset($answers[$hash])) {
950                $variantHtml = $this->getAnswerStatisticOrderingVariantHtml(
951                    $orderingElementList
952                );
953
954                $answers[$hash] = array(
955                    'answer' => $variantHtml, 'frequency' => 0
956                );
957            }
958
959            $answers[$hash]['frequency']++;
960        }
961
962        return array_values($answers);
963    }
964
965    /**
966     * @param ilPropertyFormGUI $form
967     */
968    public function prepareReprintableCorrectionsForm(ilPropertyFormGUI $form)
969    {
970        $orderingInput = $form->getItemByPostVar(assOrderingQuestion::ORDERING_ELEMENT_FORM_FIELD_POSTVAR);
971        $orderingInput->prepareReprintable($this->object);
972    }
973
974    /**
975     * @param ilPropertyFormGUI $form
976     */
977    public function populateCorrectionsFormProperties(ilPropertyFormGUI $form)
978    {
979        $points = new ilNumberInputGUI($this->lng->txt("points"), "points");
980        $points->allowDecimals(true);
981        $points->setValue($this->object->getPoints());
982        $points->setRequired(true);
983        $points->setSize(3);
984        $points->setMinValue(0);
985        $points->setMinvalueShouldBeGreater(true);
986        $form->addItem($points);
987
988        $header = new ilFormSectionHeaderGUI();
989        $header->setTitle($this->lng->txt('oq_header_ordering_elements'));
990        $form->addItem($header);
991
992        $orderingElementInput = $this->object->buildNestedOrderingElementInputGui();
993
994        $this->object->initOrderingElementAuthoringProperties($orderingElementInput);
995
996        $orderingElementInput->setElementList($this->object->getOrderingElementList());
997
998        $form->addItem($orderingElementInput);
999    }
1000
1001    /**
1002     * @param ilPropertyFormGUI $form
1003     */
1004    public function saveCorrectionsFormProperties(ilPropertyFormGUI $form)
1005    {
1006        $this->object->setPoints((float) $form->getInput('points'));
1007
1008        $submittedElementList = $this->object->fetchSolutionListFromSubmittedForm($form);
1009
1010        $curElementList = $this->object->getOrderingElementList();
1011
1012        $newElementList = new ilAssOrderingElementList();
1013        $newElementList->setQuestionId($this->object->getId());
1014
1015        foreach ($submittedElementList as $submittedElement) {
1016            if (!$curElementList->elementExistByRandomIdentifier($submittedElement->getRandomIdentifier())) {
1017                continue;
1018            }
1019
1020            $curElement = $curElementList->getElementByRandomIdentifier($submittedElement->getRandomIdentifier());
1021
1022            $curElement->setPosition($submittedElement->getPosition());
1023
1024            if ($this->object->isOrderingTypeNested()) {
1025                $curElement->setIndentation($submittedElement->getIndentation());
1026            }
1027
1028            $newElementList->addElement($curElement);
1029        }
1030
1031        $this->object->setOrderingElementList($newElementList);
1032    }
1033}
1034