1<?php
2/* Copyright (c) 1998-2013 ILIAS open source, Extended GPL, see docs/LICENSE */
3
4require_once './Modules/TestQuestionPool/classes/class.assQuestion.php';
5require_once './Modules/Test/classes/inc.AssessmentConstants.php';
6require_once './Modules/TestQuestionPool/interfaces/interface.ilObjQuestionScoringAdjustable.php';
7require_once './Modules/TestQuestionPool/interfaces/interface.ilObjAnswerScoringAdjustable.php';
8require_once './Modules/TestQuestionPool/interfaces/interface.iQuestionCondition.php';
9require_once './Modules/TestQuestionPool/classes/class.ilUserQuestionResult.php';
10
11require_once 'Modules/TestQuestionPool/classes/questions/class.ilAssOrderingElementList.php';
12
13/**
14 * Class for ordering questions
15 *
16 * assOrderingQuestion is a class for ordering questions.
17 *
18 * @author	Helmut Schottmüller <helmut.schottmueller@mac.com>
19 * @author	Björn Heyser <bheyser@databay.de>
20 * @author	Maximilian Becker <mbecker@databay.de>
21 *
22 * @version		$Id$
23 *
24 * @ingroup		ModulesTestQuestionPool
25 */
26class assOrderingQuestion extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, iQuestionCondition
27{
28    const ORDERING_ELEMENT_FORM_FIELD_POSTVAR = 'order_elems';
29
30    const ORDERING_ELEMENT_FORM_CMD_UPLOAD_IMG = 'uploadElementImage';
31    const ORDERING_ELEMENT_FORM_CMD_REMOVE_IMG = 'removeElementImage';
32
33    /**
34     * @var ilAssOrderingElementList
35     */
36    protected $orderingElementList;
37
38    /**
39    * Type of ordering question
40    *
41    * There are two possible types of ordering questions: Ordering terms (=1)
42    * and Ordering pictures (=0).
43    *
44    * @var integer
45    */
46    public $ordering_type;
47
48    /**
49    * Maximum thumbnail geometry
50    *
51    * @var integer
52    */
53    public $thumb_geometry = 100;
54
55    /**
56    * Minimum element height
57    *
58    * @var integer
59    */
60    public $element_height;
61
62    public $old_ordering_depth = array();
63    public $leveled_ordering = array();
64
65    /**
66     * assOrderingQuestion constructor
67     *
68     * The constructor takes possible arguments an creates an instance of the assOrderingQuestion object.
69     *
70     * @param string  $title    A title string to describe the question
71     * @param string  $comment  A comment string to describe the question
72     * @param string  $author   A string containing the name of the questions author
73     * @param integer $owner    A numerical ID to identify the owner/creator
74     * @param string  $question The question string of the ordering test
75     * @param int     $ordering_type
76     */
77    public function __construct(
78        $title = "",
79        $comment = "",
80        $author = "",
81        $owner = -1,
82        $question = "",
83        $ordering_type = OQ_TERMS
84    ) {
85        parent::__construct($title, $comment, $author, $owner, $question);
86        $this->orderingElementList = new ilAssOrderingElementList();
87        $this->ordering_type = $ordering_type;
88    }
89
90    /**
91    * Returns true, if a ordering question is complete for use
92    *
93    * @return boolean True, if the ordering question is complete for use, otherwise false
94    */
95    public function isComplete()
96    {
97        if (!$this->getAuthor()) {
98            return false;
99        }
100
101        if (!$this->getTitle()) {
102            return false;
103        }
104
105        if (!$this->getQuestion()) {
106            return false;
107        }
108
109        if (!$this->getMaximumPoints()) {
110            return false;
111        }
112
113        if (!$this->getOrderingElementList()->countElements()) {
114            return false;
115        }
116
117        return true;
118    }
119
120    /**
121     * Saves a assOrderingQuestion object to a database
122     *
123     * @param string $original_id
124     *
125     * @internal param object $db A pear DB object
126     */
127    public function saveToDb($original_id = "")
128    {
129        global $DIC;
130        $ilDB = $DIC['ilDB'];
131
132        $this->saveQuestionDataToDb($original_id);
133        $this->saveAdditionalQuestionDataToDb();
134        $this->saveAnswerSpecificDataToDb();
135        parent::saveToDb($original_id);
136    }
137
138    /**
139    * Loads a assOrderingQuestion object from a database
140    *
141    * @param object $db A pear DB object
142    * @param integer $question_id A unique key which defines the multiple choice test in the database
143    * @access public
144    */
145    public function loadFromDb($question_id)
146    {
147        global $DIC;
148        $ilDB = $DIC['ilDB'];
149
150        $result = $ilDB->queryF(
151            "SELECT qpl_questions.*, " . $this->getAdditionalTableName() . ".* FROM qpl_questions LEFT JOIN " . $this->getAdditionalTableName() . " ON " . $this->getAdditionalTableName() . ".question_fi = qpl_questions.question_id WHERE qpl_questions.question_id = %s",
152            array("integer"),
153            array($question_id)
154        );
155        if ($result->numRows() == 1) {
156            $data = $ilDB->fetchAssoc($result);
157            $this->setId($question_id);
158            $this->setObjId($data["obj_fi"]);
159            $this->setTitle($data["title"]);
160            $this->setComment($data["description"]);
161            $this->setOriginalId($data["original_id"]);
162            $this->setAuthor($data["author"]);
163            $this->setNrOfTries($data['nr_of_tries']);
164            $this->setPoints($data["points"]);
165            $this->setOwner($data["owner"]);
166            include_once("./Services/RTE/classes/class.ilRTE.php");
167            $this->setQuestion(ilRTE::_replaceMediaObjectImageSrc($data["question_text"], 1));
168            $this->ordering_type = strlen($data["ordering_type"]) ? $data["ordering_type"] : OQ_TERMS;
169            $this->thumb_geometry = $data["thumb_geometry"];
170            $this->element_height = $data["element_height"];
171            $this->setEstimatedWorkingTime(substr($data["working_time"], 0, 2), substr($data["working_time"], 3, 2), substr($data["working_time"], 6, 2));
172
173            try {
174                $this->setAdditionalContentEditingMode($data['add_cont_edit_mode']);
175            } catch (ilTestQuestionPoolException $e) {
176            }
177        }
178
179        $this->orderingElementList->setQuestionId($this->getId());
180        $this->orderingElementList->loadFromDb();
181
182        parent::loadFromDb($question_id);
183    }
184
185    /**
186    * Duplicates an assOrderingQuestion
187    *
188    * @access public
189    */
190    public function duplicate($for_test = true, $title = "", $author = "", $owner = "", $testObjId = null)
191    {
192        if ($this->id <= 0) {
193            // The question has not been saved. It cannot be duplicated
194            return;
195        }
196        // duplicate the question in database
197        $this_id = $this->getId();
198        $thisObjId = $this->getObjId();
199
200        $clone = $this;
201        include_once("./Modules/TestQuestionPool/classes/class.assQuestion.php");
202        $original_id = assQuestion::_getOriginalId($this->id);
203        $clone->id = -1;
204
205        if ((int) $testObjId > 0) {
206            $clone->setObjId($testObjId);
207        }
208
209        if ($title) {
210            $clone->setTitle($title);
211        }
212        if ($author) {
213            $clone->setAuthor($author);
214        }
215        if ($owner) {
216            $clone->setOwner($owner);
217        }
218        if ($for_test) {
219            $clone->saveToDb($original_id);
220        } else {
221            $clone->saveToDb();
222        }
223
224        $clone->duplicateOrderlingElementList();
225
226        // copy question page content
227        $clone->copyPageOfQuestion($this_id);
228        // copy XHTML media objects
229        $clone->copyXHTMLMediaObjectsOfQuestion($this_id);
230        // duplicate the image
231        $clone->duplicateImages($this_id, $thisObjId, $clone->getId(), $testObjId);
232
233        $clone->onDuplicate($thisObjId, $this_id, $clone->getObjId(), $clone->getId());
234
235        return $clone->id;
236    }
237
238    protected function duplicateOrderlingElementList()
239    {
240        $this->getOrderingElementList()->setQuestionId($this->getId());
241        $this->getOrderingElementList()->distributeNewRandomIdentifiers();
242        $this->getOrderingElementList()->saveToDb();
243    }
244
245    /**
246    * Copies an assOrderingQuestion object
247    *
248    * @access public
249    */
250    public function copyObject($target_questionpool_id, $title = "")
251    {
252        if ($this->id <= 0) {
253            // The question has not been saved. It cannot be duplicated
254            return;
255        }
256        // duplicate the question in database
257        $clone = $this;
258        include_once("./Modules/TestQuestionPool/classes/class.assQuestion.php");
259        $original_id = assQuestion::_getOriginalId($this->id);
260        $clone->id = -1;
261        $source_questionpool_id = $this->getObjId();
262        $clone->setObjId($target_questionpool_id);
263        if ($title) {
264            $clone->setTitle($title);
265        }
266
267        $clone->saveToDb();
268
269        // copy question page content
270        $clone->copyPageOfQuestion($original_id);
271        // copy XHTML media objects
272        $clone->copyXHTMLMediaObjectsOfQuestion($original_id);
273        // duplicate the image
274        $clone->duplicateImages($original_id, $source_questionpool_id, $clone->getId(), $target_questionpool_id);
275
276        $clone->onCopy($source_questionpool_id, $original_id, $clone->getObjId(), $clone->getId());
277
278        return $clone->id;
279    }
280
281    public function createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle = "")
282    {
283        if ($this->id <= 0) {
284            // The question has not been saved. It cannot be duplicated
285            return;
286        }
287
288        include_once("./Modules/TestQuestionPool/classes/class.assQuestion.php");
289
290        $sourceQuestionId = $this->id;
291        $sourceParentId = $this->getObjId();
292
293        // duplicate the question in database
294        $clone = $this;
295        $clone->id = -1;
296
297        $clone->setObjId($targetParentId);
298
299        if ($targetQuestionTitle) {
300            $clone->setTitle($targetQuestionTitle);
301        }
302
303        $clone->saveToDb();
304        // copy question page content
305        $clone->copyPageOfQuestion($sourceQuestionId);
306        // copy XHTML media objects
307        $clone->copyXHTMLMediaObjectsOfQuestion($sourceQuestionId);
308        // duplicate the image
309        $clone->duplicateImages($sourceQuestionId, $sourceParentId, $clone->getId(), $clone->getObjId());
310
311        $clone->onCopy($sourceParentId, $sourceQuestionId, $clone->getObjId(), $clone->getId());
312
313        return $clone->id;
314    }
315
316    public function duplicateImages($src_question_id, $src_object_id, $dest_question_id, $dest_object_id)
317    {
318        global $DIC;
319        $ilLog = $DIC['ilLog'];
320        if ($this->getOrderingType() == OQ_PICTURES || $this->getOrderingType() == OQ_NESTED_PICTURES) {
321            $imagepath_original = $this->getImagePath($src_question_id, $src_object_id);
322            $imagepath = $this->getImagePath($dest_question_id, $dest_object_id);
323
324            if (!file_exists($imagepath)) {
325                ilUtil::makeDirParents($imagepath);
326            }
327            foreach ($this->getOrderingElementList() as $element) {
328                $filename = $element->getContent();
329                if (!@copy($imagepath_original . $filename, $imagepath . $filename)) {
330                    $ilLog->write("image could not be duplicated!!!!");
331                }
332                if (@file_exists($imagepath_original . $this->getThumbPrefix() . $filename)) {
333                    if (!@copy($imagepath_original . $this->getThumbPrefix() . $filename, $imagepath . $this->getThumbPrefix() . $filename)) {
334                        $ilLog->write("image thumbnail could not be duplicated!!!!");
335                    }
336                }
337            }
338        }
339    }
340
341    /**
342     * @deprecated (!)
343     * simply use the working method duplicateImages(), we do not search the difference here
344     * and we will delete this soon (!) currently no usage found, remove for il5.3
345     */
346    public function copyImages($question_id, $source_questionpool)
347    {
348        global $DIC;
349        $ilLog = $DIC['ilLog'];
350        if ($this->getOrderingType() == OQ_PICTURES) {
351            $imagepath = $this->getImagePath();
352            $imagepath_original = str_replace("/$this->id/images", "/$question_id/images", $imagepath);
353            $imagepath_original = str_replace("/$this->obj_id/", "/$source_questionpool/", $imagepath_original);
354            if (!file_exists($imagepath)) {
355                ilUtil::makeDirParents($imagepath);
356            }
357            foreach ($this->getOrderingElementList() as $element) {
358                $filename = $element->getContent();
359                if (!@copy($imagepath_original . $filename, $imagepath . $filename)) {
360                    $ilLog->write("Ordering Question image could not be copied: ${imagepath_original}${filename}");
361                }
362                if (@file_exists($imagepath_original . $this->getThumbPrefix() . $filename)) {
363                    if (!@copy($imagepath_original . $this->getThumbPrefix() . $filename, $imagepath . $this->getThumbPrefix() . $filename)) {
364                        $ilLog->write("Ordering Question image thumbnail could not be copied: $imagepath_original" . $this->getThumbPrefix() . $filename);
365                    }
366                }
367            }
368        }
369    }
370
371    /**
372    * Sets the ordering question type
373    *
374    * @param integer $ordering_type The question ordering type
375    * @access public
376    * @see $ordering_type
377    */
378    public function setOrderingType($ordering_type = OQ_TERMS)
379    {
380        $this->ordering_type = $ordering_type;
381    }
382
383    /**
384    * Returns the ordering question type
385    *
386    * @return integer The ordering question type
387    * @access public
388    * @see $ordering_type
389    */
390    public function getOrderingType()
391    {
392        return $this->ordering_type;
393    }
394
395    public function isOrderingTypeNested()
396    {
397        return in_array($this->getOrderingType(), array(OQ_NESTED_TERMS, OQ_NESTED_PICTURES));
398    }
399
400    public function isImageOrderingType()
401    {
402        return in_array($this->getOrderingType(), array(OQ_PICTURES, OQ_NESTED_PICTURES));
403    }
404
405    public function hasOrderingTypeUploadSupport()
406    {
407        return $this->getOrderingType() == OQ_PICTURES;
408    }
409
410    /**
411     * @param $forceCorrectSolution
412     * @param $activeId
413     * @param $passIndex
414     * @return ilAssOrderingElementList
415     */
416    public function getOrderingElementListForSolutionOutput($forceCorrectSolution, $activeId, $passIndex, $getUseIntermediateSolution = false)
417    {
418        if ($forceCorrectSolution || !$activeId || $passIndex === null) {
419            return $this->getOrderingElementList();
420        }
421
422        $solutionValues = $this->getSolutionValues($activeId, $passIndex, !$getUseIntermediateSolution);
423
424        if (!count($solutionValues)) {
425            return $this->getShuffledOrderingElementList();
426        }
427
428        return $this->getSolutionOrderingElementList($this->fetchIndexedValuesFromValuePairs($solutionValues));
429    }
430
431    /**
432     * @param ilAssNestedOrderingElementsInputGUI $inputGUI
433     * @param array $lastPost
434     * @param integer $activeId
435     * @param integer $pass
436     * @return ilAssOrderingElementList
437     * @throws ilTestException
438     * @throws ilTestQuestionPoolException
439     */
440    public function getSolutionOrderingElementListForTestOutput(ilAssNestedOrderingElementsInputGUI $inputGUI, $lastPost, $activeId, $pass)
441    {
442        if ($inputGUI->isPostSubmit($lastPost)) {
443            return $this->fetchSolutionListFromFormSubmissionData($lastPost);
444        }
445
446        // hey: prevPassSolutions - pass will be always available from now on
447        #if( $pass === null && !ilObjTest::_getUsePreviousAnswers($activeId, true) )
448        #// condition looks strange? yes - keep it null when previous solutions not enabled (!)
449        #{
450        #	$pass = ilObjTest::_getPass($activeId);
451        #}
452        // hey.
453
454        $indexedSolutionValues = $this->fetchIndexedValuesFromValuePairs(
455            // hey: prevPassSolutions - obsolete due to central check
456            $this->getTestOutputSolutions($activeId, $pass)
457            // hey.
458        );
459
460        if (count($indexedSolutionValues)) {
461            return $this->getSolutionOrderingElementList($indexedSolutionValues);
462        }
463
464        return $this->getShuffledOrderingElementList();
465    }
466
467    /**
468     * @param string $value1
469     * @param string $value2
470     * @return ilAssOrderingElement
471     */
472    protected function getSolutionValuePairBrandedOrderingElementByRandomIdentifier($value1, $value2)
473    {
474        $value2 = explode(':', $value2);
475
476        $randomIdentifier = $value2[0];
477        $selectedPosition = $value1;
478        $selectedIndentation = $value2[1];
479
480        $element = $this->getOrderingElementList()->getElementByRandomIdentifier($randomIdentifier)->getClone();
481
482        $element->setPosition($selectedPosition);
483        $element->setIndentation($selectedIndentation);
484
485        return $element;
486    }
487
488    /**
489     * @param string $value1
490     * @param string $value2
491     * @return ilAssOrderingElement
492     */
493    protected function getSolutionValuePairBrandedOrderingElementBySolutionIdentifier($value1, $value2)
494    {
495        $solutionIdentifier = $value1;
496        $selectedPosition = ($value2 - 1);
497        $selectedIndentation = 0;
498
499        $element = $this->getOrderingElementList()->getElementBySolutionIdentifier($solutionIdentifier)->getClone();
500
501        $element->setPosition($selectedPosition);
502        $element->setIndentation($selectedIndentation);
503
504        return $element;
505    }
506
507    /**
508     * @param array $valuePairs
509     * @return ilAssOrderingElementList
510     * @throws ilTestQuestionPoolException
511     */
512    public function getSolutionOrderingElementList($indexedSolutionValues)
513    {
514        $solutionOrderingList = new ilAssOrderingElementList();
515        $solutionOrderingList->setQuestionId($this->getId());
516
517        foreach ($indexedSolutionValues as $value1 => $value2) {
518            if ($this->isOrderingTypeNested()) {
519                $element = $this->getSolutionValuePairBrandedOrderingElementByRandomIdentifier($value1, $value2);
520            } else {
521                $element = $this->getSolutionValuePairBrandedOrderingElementBySolutionIdentifier($value1, $value2);
522            }
523
524            $solutionOrderingList->addElement($element);
525        }
526
527        if (!$this->getOrderingElementList()->hasSameElementSetByRandomIdentifiers($solutionOrderingList)) {
528            throw new ilTestQuestionPoolException('inconsistent solution values given');
529        }
530
531        return $solutionOrderingList;
532    }
533
534    /**
535     * @param $active_id
536     * @param $pass
537     * @return ilAssOrderingElementList
538     */
539    public function getShuffledOrderingElementList()
540    {
541        $shuffledRandomIdentifierIndex = $this->getShuffler()->shuffle(
542            $this->getOrderingElementList()->getRandomIdentifierIndex()
543        );
544
545        $shuffledElementList = $this->getOrderingElementList()->getClone();
546        $shuffledElementList->reorderByRandomIdentifiers($shuffledRandomIdentifierIndex);
547        $shuffledElementList->resetElementsIndentations();
548
549        return $shuffledElementList;
550    }
551
552    /**
553     * @return ilAssOrderingElementList
554     */
555    public function getOrderingElementList()
556    {
557        return $this->orderingElementList;
558    }
559
560    /**
561     * @param ilAssOrderingElementList $orderingElementList
562     */
563    public function setOrderingElementList($orderingElementList)
564    {
565        $this->orderingElementList = $orderingElementList;
566    }
567
568    /**
569     * @param $position
570     *
571     * TODO: still in use? should not since element moving is js supported!?
572     */
573    public function moveAnswerUp($position)
574    {
575        if (!$this->getOrderingElementList()->elementExistByPosition($position)) {
576            return false;
577        }
578
579        if ($this->getOrderingElementList()->isFirstElementPosition($position)) {
580            return false;
581        }
582
583        $this->getOrderingElementList()->moveElementByPositions($position, $position - 1);
584    }
585
586    /**
587     * @param $position
588     *
589     * TODO: still in use? should not since element moving is js supported!?
590     */
591    public function moveAnswerDown($position)
592    {
593        if (!$this->getOrderingElementList()->elementExistByPosition($position)) {
594            return false;
595        }
596
597        if ($this->getOrderingElementList()->isLastElementPosition($position)) {
598            return false;
599        }
600
601        $this->getOrderingElementList()->moveElementByPositions($position, $position + 1);
602
603        return true;
604    }
605
606    /**
607     * Returns the ordering element from the given position.
608     *
609     * @param int $position
610     * @return ilAssOrderingElement|null
611     */
612    public function getAnswer($index = 0)
613    {
614        if (!$this->getOrderingElementList()->elementExistByPosition($index)) {
615            return null;
616        }
617
618        return $this->getOrderingElementList()->getElementByPosition($index);
619    }
620
621    /**
622    * Deletes an answer with a given index. The index of the first
623    * answer is 0, the index of the second answer is 1 and so on.
624    *
625    * @param integer $index A nonnegative index of the n-th answer
626    * @access public
627    * @see $answers
628    */
629    public function deleteAnswer($randomIdentifier)
630    {
631        $this->getOrderingElementList()->removeElement(
632            $this->getOrderingElementList()->getElementByRandomIdentifier($randomIdentifier)
633        );
634        $this->getOrderingElementList()->saveToDb();
635    }
636
637    /**
638    * Returns the number of answers
639    *
640    * @return integer The number of answers of the ordering question
641    * @access public
642    * @see $answers
643    */
644    public function getAnswerCount()
645    {
646        return $this->getOrderingElementList()->countElements();
647    }
648
649    /**
650     * Returns the points, a learner has reached answering the question.
651     * The points are calculated from the given answers.
652     *
653     * @access public
654     * @param integer $active_id
655     * @param integer $pass
656     * @param boolean $returndetails (deprecated !!)
657     * @return integer/array $points/$details (array $details is deprecated !!)
658     */
659    public function calculateReachedPoints($active_id, $pass = null, $authorizedSolution = true, $returndetails = false)
660    {
661        if ($returndetails) {
662            throw new ilTestException('return details not implemented for ' . __METHOD__);
663        }
664
665        if (is_null($pass)) {
666            $pass = $this->getSolutionMaxPass($active_id);
667        }
668
669        $solutionValuePairs = $this->getSolutionValues($active_id, $pass, $authorizedSolution);
670
671        if (!count($solutionValuePairs)) {
672            return 0;
673        }
674
675        $indexedSolutionValues = $this->fetchIndexedValuesFromValuePairs($solutionValuePairs);
676        $solutionOrderingElementList = $this->getSolutionOrderingElementList($indexedSolutionValues);
677
678        return $this->calculateReachedPointsForSolution($solutionOrderingElementList);
679    }
680
681    public function calculateReachedPointsFromPreviewSession(ilAssQuestionPreviewSession $previewSession)
682    {
683        if (!$previewSession->hasParticipantSolution()) {
684            return 0;
685        }
686
687        $solutionOrderingElementList = unserialize(
688            $previewSession->getParticipantsSolution()
689        );
690
691        $reachedPoints = $this->calculateReachedPointsForSolution($solutionOrderingElementList);
692        $reachedPoints = $this->deductHintPointsFromReachedPoints($previewSession, $reachedPoints);
693
694        return $this->ensureNonNegativePoints($reachedPoints);
695    }
696
697    /**
698    * Returns the maximum points, a learner can reach answering the question
699    *
700    * @return double Points
701    * @see $points
702    */
703    public function getMaximumPoints()
704    {
705        return $this->getPoints();
706    }
707
708    /*
709    * Returns the encrypted save filename of a matching picture
710    * Images are saved with an encrypted filename to prevent users from
711    * cheating by guessing the solution from the image filename
712    *
713    * @param string $filename Original filename
714    * @return string Encrypted filename
715    */
716    public function getEncryptedFilename($filename)
717    {
718        $extension = "";
719        if (preg_match("/.*\\.(\\w+)$/", $filename, $matches)) {
720            $extension = $matches[1];
721        }
722        return md5($filename) . "." . $extension;
723    }
724
725    protected function cleanImagefiles()
726    {
727        if ($this->getOrderingType() == OQ_PICTURES) {
728            if (@file_exists($this->getImagePath())) {
729                $contents = ilUtil::getDir($this->getImagePath());
730                foreach ($contents as $f) {
731                    if (strcmp($f['type'], 'file') == 0) {
732                        $found = false;
733                        foreach ($this->getOrderingElementList() as $orderElement) {
734                            if (strcmp($f['entry'], $orderElement->getContent()) == 0) {
735                                $found = true;
736                            }
737                            if (strcmp($f['entry'], $this->getThumbPrefix() . $orderElement->getContent()) == 0) {
738                                $found = true;
739                            }
740                        }
741                        if (!$found) {
742                            if (@file_exists($this->getImagePath() . $f['entry'])) {
743                                @unlink($this->getImagePath() . $f['entry']);
744                            }
745                        }
746                    }
747                }
748            }
749        } else {
750            if (@file_exists($this->getImagePath())) {
751                ilUtil::delDir($this->getImagePath());
752            }
753        }
754    }
755
756    /*
757    * Deletes an imagefile from the system if the file is deleted manually
758    *
759    * @param string $filename Image file filename
760    * @return boolean Success
761    */
762    public function dropImageFile($imageFilename)
763    {
764        if (!strlen($imageFilename)) {
765            return false;
766        }
767
768        $result = @unlink($this->getImagePath() . $imageFilename);
769        $result = $result & @unlink($this->getImagePath() . $this->getThumbPrefix() . $imageFilename);
770
771        return $result;
772    }
773
774    public function isImageFileStored($imageFilename)
775    {
776        if (!strlen($imageFilename)) {
777            return false;
778        }
779
780        if (!file_exists($this->getImagePath() . $imageFilename)) {
781            return false;
782        }
783
784        return is_file($this->getImagePath() . $imageFilename);
785    }
786
787    public function isImageReplaced(ilAssOrderingElement $newElement, ilAssOrderingElement $oldElement)
788    {
789        if (!$this->hasOrderingTypeUploadSupport()) {
790            return false;
791        }
792
793        if (!$newElement->getContent()) {
794            return false;
795        }
796
797        return $newElement->getContent() != $oldElement->getContent();
798    }
799
800    /**
801    * Sets the image file and uploads the image to the object's image directory.
802    *
803    * @param string $image_filename Name of the original image file
804    * @param string $image_tempfilename Name of the temporary uploaded image file
805    * @return integer An errorcode if the image upload fails, 0 otherwise
806    * @access public
807    */
808    public function storeImageFile($uploadFile, $targetFile)
809    {
810        if (!strlen($uploadFile)) {
811            return false;
812        }
813
814        $this->ensureImagePathExists();
815
816        // store file with hashed name
817
818        if (!ilUtil::moveUploadedFile($uploadFile, $targetFile, $this->getImagePath() . $targetFile)) {
819            return false;
820        }
821
822        return true;
823    }
824
825    public function handleThumbnailCreation(ilAssOrderingElementList $elementList)
826    {
827        foreach ($elementList as $element) {
828            $this->createImageThumbnail($element);
829        }
830    }
831
832    public function createImageThumbnail(ilAssOrderingElement $element)
833    {
834        if ($this->getThumbGeometry()) {
835            $imageFile = $this->getImagePath() . $element->getContent();
836            $thumbFile = $this->getImagePath() . $this->getThumbPrefix() . $element->getContent();
837
838            ilUtil::convertImage($imageFile, $thumbFile, "JPEG", $this->getThumbGeometry());
839        }
840    }
841
842    /**
843    * Checks the data to be saved for consistency
844    *
845  * @return boolean True, if the check was ok, False otherwise
846    * @access public
847    * @see $answers
848    */
849    public function validateSolutionSubmit()
850    {
851        $submittedSolutionList = $this->getSolutionListFromPostSubmit();
852
853        if (!$submittedSolutionList->hasElements()) {
854            return true;
855        }
856
857        return $this->getOrderingElementList()->hasSameElementSetByRandomIdentifiers($submittedSolutionList);
858    }
859
860    /**
861     * Saves the learners input of the question to the database.
862     *
863     * @access public
864     * @param integer $active_id Active id of the user
865     * @param integer $pass Test pass
866     * @return boolean $status
867     */
868    public function saveWorkingData($active_id, $pass = null, $authorized = true)
869    {
870        $entered_values = 0;
871
872        if (is_null($pass)) {
873            include_once "./Modules/Test/classes/class.ilObjTest.php";
874            $pass = ilObjTest::_getPass($active_id);
875        }
876
877        $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(
878            function () use (&$entered_values, $active_id, $pass, $authorized) {
879                $this->removeCurrentSolution($active_id, $pass, $authorized);
880
881                foreach ($this->getSolutionListFromPostSubmit() as $orderingElement) {
882                    $value1 = $orderingElement->getStorageValue1($this->getOrderingType());
883                    $value2 = $orderingElement->getStorageValue2($this->getOrderingType());
884
885                    $this->saveCurrentSolution($active_id, $pass, $value1, trim($value2), $authorized);
886
887                    $entered_values++;
888                }
889            }
890        );
891
892        if ($entered_values) {
893            $this->log($active_id, 'log_user_entered_values');
894        } else {
895            $this->log($active_id, 'log_user_not_entered_values');
896        }
897
898        return true;
899    }
900
901    protected function savePreviewData(ilAssQuestionPreviewSession $previewSession)
902    {
903        if ($this->validateSolutionSubmit()) {
904            $previewSession->setParticipantsSolution(serialize($this->getSolutionListFromPostSubmit()));
905        }
906    }
907
908    public function saveAdditionalQuestionDataToDb()
909    {
910        /** @var ilDBInterface $ilDB */
911        global $DIC;
912        $ilDB = $DIC['ilDB'];
913
914        // save additional data
915        $ilDB->manipulateF(
916            "DELETE FROM " . $this->getAdditionalTableName() . " WHERE question_fi = %s",
917            array( "integer" ),
918            array( $this->getId() )
919        );
920
921        $ilDB->manipulateF(
922            "INSERT INTO " . $this->getAdditionalTableName() . " (question_fi, ordering_type, thumb_geometry, element_height)
923							VALUES (%s, %s, %s, %s)",
924            array( "integer", "text", "integer", "integer" ),
925            array(
926                                $this->getId(),
927                                $this->ordering_type,
928                                $this->getThumbGeometry(),
929                                ($this->getElementHeight() > 20) ? $this->getElementHeight() : null
930                            )
931        );
932    }
933
934    public function saveAnswerSpecificDataToDb()
935    {
936        $this->getOrderingElementList()->setQuestionId($this->getId());
937        $this->getOrderingElementList()->saveToDb();
938
939        if ($this->hasOrderingTypeUploadSupport()) {
940            $this->rebuildThumbnails();
941            $this->cleanImagefiles();
942        }
943    }
944
945    /**
946    * Returns the question type of the question
947    *
948    * @return integer The question type of the question
949    * @access public
950    */
951    public function getQuestionType()
952    {
953        return "assOrderingQuestion";
954    }
955
956    /**
957    * Returns the name of the additional question data table in the database
958    *
959    * @return string The additional table name
960    * @access public
961    */
962    public function getAdditionalTableName()
963    {
964        return "qpl_qst_ordering";
965    }
966
967    /**
968    * Returns the name of the answer table in the database
969    *
970    * @return string The answer table name
971    * @access public
972    */
973    public function getAnswerTableName()
974    {
975        return "qpl_a_ordering";
976    }
977
978    /**
979    * Collects all text in the question which could contain media objects
980    * which were created with the Rich Text Editor
981    */
982    public function getRTETextWithMediaObjects()
983    {
984        $text = parent::getRTETextWithMediaObjects();
985
986        foreach ($this->getOrderingElementList() as $orderingElement) {
987            $text .= $orderingElement->getContent();
988        }
989
990        return $text;
991    }
992
993    /**
994     * Returns the answers array
995     * @deprecated seriously, stop looking for this kind data at this point (!) look where it comes from and learn (!)
996     */
997    public function getOrderElements()
998    {
999        return $this->getOrderingElementList()->getRandomIdentifierIndexedElements();
1000    }
1001
1002    /**
1003    * Returns true if the question type supports JavaScript output
1004    *
1005    * @return boolean TRUE if the question type supports JavaScript output, FALSE otherwise
1006    * @access public
1007    */
1008    public function supportsJavascriptOutput()
1009    {
1010        return true;
1011    }
1012
1013    public function supportsNonJsOutput()
1014    {
1015        return false;
1016    }
1017
1018    /**
1019     * {@inheritdoc}
1020     */
1021    public function setExportDetailsXLS($worksheet, $startrow, $active_id, $pass)
1022    {
1023        parent::setExportDetailsXLS($worksheet, $startrow, $active_id, $pass);
1024
1025        $solutions = $this->getSolutionValues($active_id, $pass);
1026        $sol = array();
1027        foreach ($solutions as $solution) {
1028            $sol[$solution["value1"]] = $solution["value2"];
1029        }
1030        asort($sol);
1031        $sol = array_keys($sol);
1032
1033        $i = 1;
1034        foreach ($sol as $idx) {
1035            foreach ($solutions as $solution) {
1036                if ($solution["value1"] == $idx) {
1037                    $worksheet->setCell($startrow + $i, 0, $solution["value2"]);
1038                }
1039            }
1040            $element = $this->getOrderingElementList()->getElementBySolutionIdentifier($idx);
1041            $worksheet->setCell($startrow + $i, 1, $element->getContent());
1042            $i++;
1043        }
1044
1045        return $startrow + $i + 1;
1046    }
1047
1048    /*
1049    * Get the thumbnail geometry
1050    *
1051    * @return integer Geometry
1052    */
1053    public function getThumbGeometry()
1054    {
1055        return $this->thumb_geometry;
1056    }
1057
1058    public function getThumbSize()
1059    {
1060        return $this->getThumbGeometry();
1061    }
1062
1063    /*
1064    * Set the thumbnail geometry
1065    *
1066    * @param integer $a_geometry Geometry
1067    */
1068    public function setThumbGeometry($a_geometry)
1069    {
1070        $this->thumb_geometry = ($a_geometry < 1) ? 100 : $a_geometry;
1071    }
1072
1073    /*
1074    * Get the minimum element height
1075    *
1076    * @return integer Height
1077    */
1078    public function getElementHeight()
1079    {
1080        return $this->element_height;
1081    }
1082
1083    /*
1084    * Set the minimum element height
1085    *
1086    * @param integer $a_height Height
1087    */
1088    public function setElementHeight($a_height)
1089    {
1090        $this->element_height = ($a_height < 20) ? "" : $a_height;
1091    }
1092
1093    /*
1094    * Rebuild the thumbnail images with a new thumbnail size
1095    */
1096    public function rebuildThumbnails()
1097    {
1098        if ($this->getOrderingType() == OQ_PICTURES || $this->getOrderingType() == OQ_NESTED_PICTURES) {
1099            foreach ($this->getOrderElements() as $orderingElement) {
1100                $this->generateThumbForFile($this->getImagePath(), $orderingElement->getContent());
1101            }
1102        }
1103    }
1104
1105    public function getThumbPrefix()
1106    {
1107        return "thumb.";
1108    }
1109
1110    protected function generateThumbForFile($path, $file)
1111    {
1112        $filename = $path . $file;
1113        if (@file_exists($filename)) {
1114            $thumbpath = $path . $this->getThumbPrefix() . $file;
1115            $path_info = @pathinfo($filename);
1116            $ext = "";
1117            switch (strtoupper($path_info['extension'])) {
1118                case 'PNG':
1119                    $ext = 'PNG';
1120                    break;
1121                case 'GIF':
1122                    $ext = 'GIF';
1123                    break;
1124                default:
1125                    $ext = 'JPEG';
1126                    break;
1127            }
1128            ilUtil::convertImage($filename, $thumbpath, $ext, $this->getThumbGeometry());
1129        }
1130    }
1131
1132    /**
1133    * Returns a JSON representation of the question
1134    */
1135    public function toJSON()
1136    {
1137        include_once("./Services/RTE/classes/class.ilRTE.php");
1138        $result = array();
1139        $result['id'] = (int) $this->getId();
1140        $result['type'] = (string) $this->getQuestionType();
1141        $result['title'] = (string) $this->getTitle();
1142        $result['question'] = $this->formatSAQuestion($this->getQuestion());
1143        $result['nr_of_tries'] = (int) $this->getNrOfTries();
1144        $result['shuffle'] = (bool) true;
1145        $result['points'] = $this->getPoints();
1146        $result['feedback'] = array(
1147            'onenotcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)),
1148            'allcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true))
1149        );
1150        if ($this->getOrderingType() == OQ_PICTURES) {
1151            $result['path'] = $this->getImagePathWeb();
1152        }
1153
1154        $counter = 1;
1155        $answers = array();
1156        foreach ($this->getOrderingElementList() as $orderingElement) {
1157            $answers[$counter] = $orderingElement->getContent();
1158            $counter++;
1159        }
1160        $answers = $this->getShuffler()->shuffle($answers);
1161        $arr = array();
1162        foreach ($answers as $order => $answer) {
1163            array_push($arr, array(
1164                "answertext" => (string) $answer,
1165                "order" => (int) $order
1166            ));
1167        }
1168        $result['answers'] = $arr;
1169
1170        $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId());
1171        $result['mobs'] = $mobs;
1172
1173        return json_encode($result);
1174    }
1175
1176    /**
1177     * @return ilAssNestedOrderingElementsInputGUI|ilAssOrderingImagesInputGUI|ilAssOrderingTextsInputGUI
1178     * @throws ilTestQuestionPoolException
1179     */
1180    public function buildOrderingElementInputGui()
1181    {
1182        switch ($this->getOrderingType()) {
1183            case OQ_TERMS:
1184
1185                return $this->buildOrderingTextsInputGui();
1186
1187            case OQ_PICTURES:
1188
1189                return $this->buildOrderingImagesInputGui();
1190
1191            case OQ_NESTED_TERMS:
1192            case OQ_NESTED_PICTURES:
1193
1194                return $this->buildNestedOrderingElementInputGui();
1195
1196            default:
1197                throw new ilTestQuestionPoolException('unknown ordering mode');
1198        }
1199    }
1200
1201    /**
1202     * @param ilAssOrderingTextsInputGUI|ilAssOrderingImagesInputGUI|ilAssNestedOrderingElementsInputGUI $formField
1203     */
1204    public function initOrderingElementAuthoringProperties(ilFormPropertyGUI $formField)
1205    {
1206        switch (true) {
1207            case $formField instanceof ilAssNestedOrderingElementsInputGUI:
1208
1209                $formField->setInteractionEnabled(true);
1210                $formField->setNestingEnabled($this->isOrderingTypeNested());
1211                break;
1212
1213            case $formField instanceof ilAssOrderingTextsInputGUI:
1214            case $formField instanceof ilAssOrderingImagesInputGUI:
1215            default:
1216
1217                $formField->setEditElementOccuranceEnabled(true);
1218                $formField->setEditElementOrderEnabled(true);
1219        }
1220
1221        $formField->setRequired(true);
1222    }
1223
1224    /**
1225     * @param ilFormPropertyGUI $formField
1226     */
1227    public function initOrderingElementFormFieldLabels(ilFormPropertyGUI $formField)
1228    {
1229        $formField->setInfo($this->lng->txt('ordering_answer_sequence_info'));
1230        $formField->setTitle($this->lng->txt('answers'));
1231    }
1232
1233    /**
1234     * @return ilAssOrderingTextsInputGUI
1235     */
1236    public function buildOrderingTextsInputGui()
1237    {
1238        $formDataConverter = $this->buildOrderingTextsFormDataConverter();
1239
1240        require_once 'Modules/TestQuestionPool/classes/forms/class.ilAssOrderingTextsInputGUI.php';
1241
1242        $orderingElementInput = new ilAssOrderingTextsInputGUI(
1243            $formDataConverter,
1244            self::ORDERING_ELEMENT_FORM_FIELD_POSTVAR
1245        );
1246
1247        $this->initOrderingElementFormFieldLabels($orderingElementInput);
1248
1249        return $orderingElementInput;
1250    }
1251
1252    /**
1253     * @return ilAssOrderingImagesInputGUI
1254     */
1255    public function buildOrderingImagesInputGui()
1256    {
1257        $formDataConverter = $this->buildOrderingImagesFormDataConverter();
1258
1259        require_once 'Modules/TestQuestionPool/classes/forms/class.ilAssOrderingImagesInputGUI.php';
1260
1261        $orderingElementInput = new ilAssOrderingImagesInputGUI(
1262            $formDataConverter,
1263            self::ORDERING_ELEMENT_FORM_FIELD_POSTVAR
1264        );
1265
1266        $orderingElementInput->setImageRemovalCommand(self::ORDERING_ELEMENT_FORM_CMD_REMOVE_IMG);
1267        $orderingElementInput->setImageUploadCommand(self::ORDERING_ELEMENT_FORM_CMD_UPLOAD_IMG);
1268
1269        $this->initOrderingElementFormFieldLabels($orderingElementInput);
1270
1271        return $orderingElementInput;
1272    }
1273
1274    /**
1275     * @return ilAssNestedOrderingElementsInputGUI
1276     */
1277    public function buildNestedOrderingElementInputGui()
1278    {
1279        $formDataConverter = $this->buildNestedOrderingFormDataConverter();
1280
1281        require_once 'Modules/TestQuestionPool/classes/forms/class.ilAssNestedOrderingElementsInputGUI.php';
1282
1283        $orderingElementInput = new ilAssNestedOrderingElementsInputGUI(
1284            $formDataConverter,
1285            self::ORDERING_ELEMENT_FORM_FIELD_POSTVAR
1286        );
1287
1288        $orderingElementInput->setUniquePrefix($this->getId());
1289        $orderingElementInput->setOrderingType($this->getOrderingType());
1290        $orderingElementInput->setElementImagePath($this->getImagePathWeb());
1291        $orderingElementInput->setThumbPrefix($this->getThumbPrefix());
1292
1293        $this->initOrderingElementFormFieldLabels($orderingElementInput);
1294
1295        return $orderingElementInput;
1296    }
1297
1298    /**
1299     * @param ilPropertyFormGUI $form
1300     * @return ilAssOrderingElementList $submittedElementList
1301     */
1302    public function fetchSolutionListFromSubmittedForm(ilPropertyFormGUI $form)
1303    {
1304        return $form->getItemByPostVar(self::ORDERING_ELEMENT_FORM_FIELD_POSTVAR)->getElementList($this->getId());
1305    }
1306
1307    /**
1308     * @param array $userSolutionPost
1309     * @return ilAssOrderingElementList
1310     * @throws ilTestException
1311     */
1312    public function fetchSolutionListFromFormSubmissionData($userSolutionPost)
1313    {
1314        $orderingGUI = $this->buildNestedOrderingElementInputGui();
1315        $orderingGUI->setContext(ilAssNestedOrderingElementsInputGUI::CONTEXT_USER_SOLUTION_SUBMISSION);
1316        $orderingGUI->setValueByArray($userSolutionPost);
1317
1318        if (!$orderingGUI->checkInput()) {
1319            require_once 'Modules/Test/exceptions/class.ilTestException.php';
1320            throw new ilTestException('error on validating user solution post');
1321        }
1322
1323        require_once 'Modules/TestQuestionPool/classes/questions/class.ilAssOrderingElementList.php';
1324        $solutionOrderingElementList = ilAssOrderingElementList::buildInstance($this->getId());
1325
1326        $storedElementList = $this->getOrderingElementList();
1327
1328        foreach ($orderingGUI->getElementList($this->getId()) as $submittedElement) {
1329            $solutionElement = $storedElementList->getElementByRandomIdentifier(
1330                $submittedElement->getRandomIdentifier()
1331            )->getClone();
1332
1333            $solutionElement->setPosition($submittedElement->getPosition());
1334
1335            if ($this->isOrderingTypeNested()) {
1336                $solutionElement->setIndentation($submittedElement->getIndentation());
1337            }
1338
1339            $solutionOrderingElementList->addElement($solutionElement);
1340        }
1341
1342        return $solutionOrderingElementList;
1343    }
1344
1345    /**
1346     * @var ilAssOrderingElementList
1347     */
1348    private $postSolutionOrderingElementList = null;
1349
1350    /**
1351     * @return ilAssOrderingElementList
1352     */
1353    public function getSolutionListFromPostSubmit()
1354    {
1355        if ($this->postSolutionOrderingElementList === null) {
1356            $list = $this->fetchSolutionListFromFormSubmissionData($_POST);
1357            $this->postSolutionOrderingElementList = $list;
1358        }
1359
1360        return $this->postSolutionOrderingElementList;
1361    }
1362
1363    /**
1364     * @return array
1365     */
1366    public function getSolutionPostSubmit()
1367    {
1368        return $this->fetchSolutionSubmit($_POST);
1369    }
1370
1371    /**
1372     * @param $user_order
1373     * @param $nested_solution
1374     * @return int
1375     */
1376    protected function calculateReachedPointsForSolution(ilAssOrderingElementList $solutionOrderingElementList)
1377    {
1378        $reachedPoints = $this->getPoints();
1379
1380        foreach ($this->getOrderingElementList() as $correctElement) {
1381            $userElement = $solutionOrderingElementList->getElementByPosition($correctElement->getPosition());
1382
1383            if (!$correctElement->isSameElement($userElement)) {
1384                $reachedPoints = 0;
1385                break;
1386            }
1387        }
1388
1389        return $reachedPoints;
1390    }
1391
1392    /***
1393     * @param object 	$child
1394     * @param integer 	$ordering_depth
1395     * @param bool 		$with_random_id
1396     */
1397    public function getLeveledOrdering()
1398    {
1399        return $this->leveled_ordering;
1400    }
1401
1402    public function getOldLeveledOrdering()
1403    {
1404        global $DIC;
1405        $ilDB = $DIC['ilDB'];
1406
1407        $res = $ilDB->queryF(
1408            'SELECT depth FROM qpl_a_ordering WHERE question_fi = %s ORDER BY position ASC',
1409            array('integer'),
1410            array($this->getId())
1411        );
1412        while ($row = $ilDB->fetchAssoc($res)) {
1413            $this->old_ordering_depth[] = $row['depth'];
1414        }
1415        return $this->old_ordering_depth;
1416    }
1417
1418    /***
1419     * @param integer $a_random_id
1420     * @return integer
1421     */
1422    public function lookupSolutionOrderByRandomid($a_random_id)
1423    {
1424        global $DIC;
1425        $ilDB = $DIC['ilDB'];
1426
1427        $res = $ilDB->queryF(
1428            'SELECT solution_key FROM qpl_a_ordering WHERE random_id = %s',
1429            array('integer'),
1430            array($a_random_id)
1431        );
1432        $row = $ilDB->fetchAssoc($res);
1433
1434        return $row['solution_key'];
1435    }
1436
1437    public function updateLeveledOrdering($a_index, $a_answer_text, $a_depth)
1438    {
1439        global $DIC;
1440        $ilDB = $DIC['ilDB'];
1441
1442        $ilDB->update(
1443            'qpl_a_ordering',
1444            array('solution_key' => array('integer', $a_index),
1445              'depth' => array('integer', $a_depth)),
1446            array('answertext' => array('text', $a_answer_text))
1447        );
1448
1449
1450        return true;
1451    }
1452
1453    /**
1454     * Get all available operations for a specific question
1455     *
1456     * @param string $expression
1457     *
1458     * @internal param string $expression_type
1459     * @return array
1460     */
1461    public function getOperators($expression)
1462    {
1463        require_once "./Modules/TestQuestionPool/classes/class.ilOperatorsExpressionMapping.php";
1464        return ilOperatorsExpressionMapping::getOperatorsByExpression($expression);
1465    }
1466
1467    /**
1468     * Get all available expression types for a specific question
1469     * @return array
1470     */
1471    public function getExpressionTypes()
1472    {
1473        return array(
1474            iQuestionCondition::PercentageResultExpression,
1475            iQuestionCondition::NumericResultExpression,
1476            iQuestionCondition::OrderingResultExpression,
1477            iQuestionCondition::EmptyAnswerExpression,
1478        );
1479    }
1480
1481    /**
1482    * Get the user solution for a question by active_id and the test pass
1483    *
1484    * @param int $active_id
1485    * @param int $pass
1486    *
1487    * @return ilUserQuestionResult
1488    */
1489    public function getUserQuestionResult($active_id, $pass)
1490    {
1491        /** @var ilDBInterface $ilDB */
1492        global $DIC;
1493        $ilDB = $DIC['ilDB'];
1494        $result = new ilUserQuestionResult($this, $active_id, $pass);
1495
1496        $maxStep = $this->lookupMaxStep($active_id, $pass);
1497
1498        if ($maxStep !== null) {
1499            $data = $ilDB->queryF(
1500                "SELECT value1, value2 FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s AND step = %s ORDER BY value1 ASC ",
1501                array("integer", "integer", "integer","integer"),
1502                array($active_id, $pass, $this->getId(), $maxStep)
1503            );
1504        } else {
1505            $data = $ilDB->queryF(
1506                "SELECT value1, value2 FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s ORDER BY value1 ASC ",
1507                array("integer", "integer", "integer"),
1508                array($active_id, $pass, $this->getId())
1509            );
1510        }
1511
1512        $elements = array();
1513        while ($row = $ilDB->fetchAssoc($data)) {
1514            $newKey = explode(":", $row["value2"]);
1515
1516            foreach ($this->getOrderingElementList() as $answer) {
1517                // Images nut supported
1518                if (!$this->isOrderingTypeNested()) {
1519                    if ($answer->getSolutionIdentifier() == $row["value1"]) {
1520                        $elements[$row["value2"]] = $answer->getSolutionIdentifier() + 1;
1521                        break;
1522                    }
1523                } else {
1524                    if ($answer->getRandomIdentifier() == $newKey[0]) {
1525                        $elements[$row["value1"]] = $answer->getSolutionIdentifier() + 1;
1526                        break;
1527                    }
1528                }
1529            }
1530        }
1531
1532        ksort($elements);
1533
1534        foreach (array_values($elements) as $element) {
1535            $result->addKeyValue($element, $element);
1536        }
1537
1538        $points = $this->calculateReachedPoints($active_id, $pass);
1539        $max_points = $this->getMaximumPoints();
1540
1541        $result->setReachedPercentage(($points / $max_points) * 100);
1542
1543        return $result;
1544    }
1545
1546    /**
1547     * If index is null, the function returns an array with all anwser options
1548     * Else it returns the specific answer option
1549     *
1550     * @param null|int $index
1551     *
1552     * @return array|ASS_AnswerSimple
1553     */
1554    public function getAvailableAnswerOptions($index = null)
1555    {
1556        if ($index !== null) {
1557            return $this->getOrderingElementList()->getElementByPosition($index);
1558        }
1559
1560        return $this->getOrderingElementList()->getElements();
1561    }
1562
1563    /**
1564     * {@inheritdoc}
1565     */
1566    protected function afterSyncWithOriginal($origQuestionId, $dupQuestionId, $origParentObjId, $dupParentObjId)
1567    {
1568        parent::afterSyncWithOriginal($origQuestionId, $dupQuestionId, $origParentObjId, $dupParentObjId);
1569        $this->duplicateImages($dupQuestionId, $dupParentObjId, $origQuestionId, $origParentObjId);
1570    }
1571
1572    // fau: testNav - new function getTestQuestionConfig()
1573    /**
1574     * Get the test question configuration
1575     * @return ilTestQuestionConfig
1576     */
1577    // hey: refactored identifiers
1578    public function buildTestPresentationConfig()
1579    // hey.
1580    {
1581        // hey: refactored identifiers
1582        return parent::buildTestPresentationConfig()
1583        // hey.
1584            ->setIsUnchangedAnswerPossible(true)
1585            ->setUseUnchangedAnswerLabel($this->lng->txt('tst_unchanged_order_is_correct'));
1586    }
1587    // fau.
1588
1589    protected function ensureImagePathExists()
1590    {
1591        if (!file_exists($this->getImagePath())) {
1592            ilUtil::makeDirParents($this->getImagePath());
1593        }
1594    }
1595
1596    /**
1597     * @return array
1598     */
1599    public function fetchSolutionSubmit($formSubmissionDataStructure)
1600    {
1601        $solutionSubmit = array();
1602
1603        if (isset($formSubmissionDataStructure['orderresult'])) {
1604            $orderresult = $formSubmissionDataStructure['orderresult'];
1605
1606            if (strlen($orderresult)) {
1607                $orderarray = explode(":", $orderresult);
1608                $ordervalue = 1;
1609                foreach ($orderarray as $index) {
1610                    $idmatch = null;
1611                    if (preg_match("/id_(\\d+)/", $index, $idmatch)) {
1612                        $randomid = $idmatch[1];
1613                        foreach ($this->getOrderingElementList() as $answeridx => $answer) {
1614                            if ($answer->getRandomIdentifier() == $randomid) {
1615                                $solutionSubmit[$answeridx] = $ordervalue;
1616                                $ordervalue++;
1617                            }
1618                        }
1619                    }
1620                }
1621            }
1622        } elseif ($this->getOrderingType() == OQ_NESTED_TERMS || $this->getOrderingType() == OQ_NESTED_PICTURES) {
1623            $index = 0;
1624            foreach ($formSubmissionDataStructure['content'] as $randomId => $content) {
1625                $indentation = $formSubmissionDataStructure['indentation'];
1626
1627                $value1 = $index++;
1628                $value2 = implode(':', array($randomId, $indentation));
1629
1630                $solutionSubmit[$value1] = $value2;
1631            }
1632        } else {
1633            foreach ($formSubmissionDataStructure as $key => $value) {
1634                $matches = null;
1635                if (preg_match("/^order_(\d+)/", $key, $matches)) {
1636                    if (!(preg_match("/initial_value_\d+/", $value))) {
1637                        if (strlen($value)) {
1638                            foreach ($this->getOrderingElementList() as $answeridx => $answer) {
1639                                if ($answer->getRandomIdentifier() == $matches[1]) {
1640                                    $solutionSubmit[$answeridx] = $value;
1641                                }
1642                            }
1643                        }
1644                    }
1645                }
1646            }
1647        }
1648
1649        return $solutionSubmit;
1650    }
1651
1652    /**
1653     * @return ilAssOrderingFormValuesObjectsConverter
1654     */
1655    protected function buildOrderingElementFormDataConverter()
1656    {
1657        require_once 'Modules/TestQuestionPool/classes/forms/class.ilAssOrderingFormValuesObjectsConverter.php';
1658        $converter = new ilAssOrderingFormValuesObjectsConverter();
1659        $converter->setPostVar(self::ORDERING_ELEMENT_FORM_FIELD_POSTVAR);
1660
1661        return $converter;
1662    }
1663
1664    /**
1665     * @return ilAssOrderingFormValuesObjectsConverter
1666     */
1667    protected function buildOrderingImagesFormDataConverter()
1668    {
1669        $formDataConverter = $this->buildOrderingElementFormDataConverter();
1670        $formDataConverter->setContext(ilAssOrderingFormValuesObjectsConverter::CONTEXT_MAINTAIN_ELEMENT_IMAGE);
1671
1672        $formDataConverter->setImageRemovalCommand(self::ORDERING_ELEMENT_FORM_CMD_REMOVE_IMG);
1673        $formDataConverter->setImageUrlPath($this->getImagePathWeb());
1674        $formDataConverter->setImageFsPath($this->getImagePath());
1675
1676        if ($this->getThumbSize() && $this->getThumbPrefix()) {
1677            $formDataConverter->setThumbnailPrefix($this->getThumbPrefix());
1678        }
1679        return $formDataConverter;
1680    }
1681
1682    /**
1683     * @return ilAssOrderingFormValuesObjectsConverter
1684     */
1685    protected function buildOrderingTextsFormDataConverter()
1686    {
1687        $formDataConverter = $this->buildOrderingElementFormDataConverter();
1688        $formDataConverter->setContext(ilAssOrderingFormValuesObjectsConverter::CONTEXT_MAINTAIN_ELEMENT_TEXT);
1689        return $formDataConverter;
1690    }
1691
1692    /**
1693     * @return ilAssOrderingFormValuesObjectsConverter
1694     */
1695    protected function buildNestedOrderingFormDataConverter()
1696    {
1697        $formDataConverter = $this->buildOrderingElementFormDataConverter();
1698        $formDataConverter->setContext(ilAssOrderingFormValuesObjectsConverter::CONTEXT_MAINTAIN_HIERARCHY);
1699
1700        if ($this->getOrderingType() == OQ_NESTED_PICTURES) {
1701            $formDataConverter->setImageRemovalCommand(self::ORDERING_ELEMENT_FORM_CMD_REMOVE_IMG);
1702            $formDataConverter->setImageUrlPath($this->getImagePathWeb());
1703
1704            if ($this->getThumbSize() && $this->getThumbPrefix()) {
1705                $formDataConverter->setThumbnailPrefix($this->getThumbPrefix());
1706            }
1707        }
1708
1709        return $formDataConverter;
1710    }
1711}
1712