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/TestQuestionPool/interfaces/interface.ilObjQuestionScoringAdjustable.php';
6require_once 'Modules/TestQuestionPool/interfaces/interface.ilObjAnswerScoringAdjustable.php';
7require_once 'Modules/TestQuestionPool/interfaces/interface.ilAssSpecificFeedbackOptionLabelProvider.php';
8
9/**
10 * @author		Björn Heyser <bheyser@databay.de>
11 * @version		$Id$
12 *
13 * @package     Modules/TestQuestionPool
14 */
15class assKprimChoice extends assQuestion implements ilObjQuestionScoringAdjustable, ilObjAnswerScoringAdjustable, ilAssSpecificFeedbackOptionLabelProvider
16{
17    const NUM_REQUIRED_ANSWERS = 4;
18
19    const PARTIAL_SCORING_NUM_CORRECT_ANSWERS = 3;
20
21    const ANSWER_TYPE_SINGLE_LINE = 'singleLine';
22    const ANSWER_TYPE_MULTI_LINE = 'multiLine';
23
24    const OPTION_LABEL_RIGHT_WRONG = 'right_wrong';
25    const OPTION_LABEL_PLUS_MINUS = 'plus_minus';
26    const OPTION_LABEL_APPLICABLE_OR_NOT = 'applicable_or_not';
27    const OPTION_LABEL_ADEQUATE_OR_NOT = 'adequate_or_not';
28    const OPTION_LABEL_CUSTOM = 'customlabel';
29
30    const DEFAULT_THUMB_SIZE = 150;
31    const THUMB_PREFIX = 'thumb.';
32
33    private $shuffleAnswersEnabled;
34
35    private $answerType;
36
37    private $thumbSize;
38
39    private $scorePartialSolutionEnabled;
40
41    private $optionLabel;
42
43    private $customTrueOptionLabel;
44
45    private $customFalseOptionLabel;
46
47    private $specificFeedbackSetting;
48
49    private $answers;
50
51    public function __construct($title = '', $comment = '', $author = '', $owner = -1, $question = '')
52    {
53        parent::__construct($title, $comment, $author, $owner, $question);
54
55        $this->shuffleAnswersEnabled = true;
56        $this->answerType = self::ANSWER_TYPE_SINGLE_LINE;
57        $this->thumbSize = self::DEFAULT_THUMB_SIZE;
58        $this->scorePartialSolutionEnabled = true;
59        $this->optionLabel = self::OPTION_LABEL_RIGHT_WRONG;
60        $this->customTrueOptionLabel = '';
61        $this->customFalseOptionLabel = '';
62
63        require_once 'Modules/TestQuestionPool/classes/feedback/class.ilAssConfigurableMultiOptionQuestionFeedback.php';
64        $this->specificFeedbackSetting = ilAssConfigurableMultiOptionQuestionFeedback::FEEDBACK_SETTING_ALL;
65
66        $this->answers = array();
67
68        $this->setPoints('');
69    }
70
71    public function getQuestionType()
72    {
73        return 'assKprimChoice';
74    }
75
76    public function getAdditionalTableName()
77    {
78        return "qpl_qst_kprim";
79    }
80
81    public function getAnswerTableName()
82    {
83        return "qpl_a_kprim";
84    }
85
86    public function setShuffleAnswersEnabled($shuffleAnswersEnabled)
87    {
88        $this->shuffleAnswersEnabled = $shuffleAnswersEnabled;
89    }
90
91    public function isShuffleAnswersEnabled()
92    {
93        return $this->shuffleAnswersEnabled;
94    }
95
96    public function setAnswerType($answerType)
97    {
98        $this->answerType = $answerType;
99    }
100
101    public function getAnswerType()
102    {
103        return $this->answerType;
104    }
105
106    public function setThumbSize($thumbSize)
107    {
108        $this->thumbSize = $thumbSize;
109    }
110
111    public function getThumbSize()
112    {
113        return $this->thumbSize;
114    }
115
116    public function setScorePartialSolutionEnabled($scorePartialSolutionEnabled)
117    {
118        $this->scorePartialSolutionEnabled = $scorePartialSolutionEnabled;
119    }
120
121    public function isScorePartialSolutionEnabled()
122    {
123        return $this->scorePartialSolutionEnabled;
124    }
125
126    public function setOptionLabel($optionLabel)
127    {
128        $this->optionLabel = $optionLabel;
129    }
130
131    public function getOptionLabel()
132    {
133        return $this->optionLabel;
134    }
135
136    public function setCustomTrueOptionLabel($customTrueOptionLabel)
137    {
138        $this->customTrueOptionLabel = $customTrueOptionLabel;
139    }
140
141    public function getCustomTrueOptionLabel()
142    {
143        return $this->customTrueOptionLabel;
144    }
145
146    public function setCustomFalseOptionLabel($customFalseOptionLabel)
147    {
148        $this->customFalseOptionLabel = $customFalseOptionLabel;
149    }
150
151    public function getCustomFalseOptionLabel()
152    {
153        return $this->customFalseOptionLabel;
154    }
155
156    public function setSpecificFeedbackSetting($specificFeedbackSetting)
157    {
158        $this->specificFeedbackSetting = $specificFeedbackSetting;
159    }
160
161    public function getSpecificFeedbackSetting()
162    {
163        return $this->specificFeedbackSetting;
164    }
165
166    public function setAnswers($answers)
167    {
168        $this->answers = $answers;
169    }
170
171    public function getAnswers()
172    {
173        return $this->answers;
174    }
175
176    public function getAnswer($position)
177    {
178        foreach ($this->getAnswers() as $answer) {
179            if ($answer->getPosition() == $position) {
180                return $answer;
181            }
182        }
183
184        return null;
185    }
186
187    public function addAnswer(ilAssKprimChoiceAnswer $answer)
188    {
189        $this->answers[] = $answer;
190    }
191
192    public function loadFromDb($questionId)
193    {
194        $res = $this->db->queryF($this->buildQuestionDataQuery(), array('integer'), array($questionId));
195
196        while ($data = $this->db->fetchAssoc($res)) {
197            $this->setId($questionId);
198
199            $this->setOriginalId($data['original_id']);
200
201            $this->setObjId($data['obj_fi']);
202
203            $this->setTitle($data['title']);
204            $this->setNrOfTries($data['nr_of_tries']);
205            $this->setComment($data['description']);
206            $this->setAuthor($data['author']);
207            $this->setPoints($data['points']);
208            $this->setOwner($data['owner']);
209            $this->setEstimatedWorkingTimeFromDurationString($data['working_time']);
210            $this->setLastChange($data['tstamp']);
211            require_once 'Services/RTE/classes/class.ilRTE.php';
212            $this->setQuestion(ilRTE::_replaceMediaObjectImageSrc($data['question_text'], 1));
213
214            $this->setShuffleAnswersEnabled((bool) $data['shuffle_answers']);
215
216            if ($this->isValidAnswerType($data['answer_type'])) {
217                $this->setAnswerType($data['answer_type']);
218            }
219
220            if (is_numeric($data['thumb_size'])) {
221                $this->setThumbSize((int) $data['thumb_size']);
222            }
223
224            if ($this->isValidOptionLabel($data['opt_label'])) {
225                $this->setOptionLabel($data['opt_label']);
226            }
227
228            $this->setCustomTrueOptionLabel($data['custom_true']);
229            $this->setCustomFalseOptionLabel($data['custom_false']);
230
231            if ($data['score_partsol'] !== null) {
232                $this->setScorePartialSolutionEnabled((bool) $data['score_partsol']);
233            }
234
235            if (isset($data['feedback_setting'])) {
236                $this->setSpecificFeedbackSetting((int) $data['feedback_setting']);
237            }
238
239            try {
240                $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle']));
241            } catch (ilTestQuestionPoolInvalidArgumentException $e) {
242                $this->setLifecycle(ilAssQuestionLifecycle::getDraftInstance());
243            }
244
245            try {
246                $this->setAdditionalContentEditingMode($data['add_cont_edit_mode']);
247            } catch (ilTestQuestionPoolException $e) {
248            }
249        }
250
251        $this->loadAnswerData($questionId);
252
253        parent::loadFromDb($questionId);
254    }
255
256    private function loadAnswerData($questionId)
257    {
258        global $DIC;
259        $ilDB = $DIC['ilDB'];
260
261        $res = $this->db->queryF(
262            "SELECT * FROM {$this->getAnswerTableName()} WHERE question_fi = %s ORDER BY position ASC",
263            array('integer'),
264            array($questionId)
265        );
266
267        require_once 'Modules/TestQuestionPool/classes/class.ilAssKprimChoiceAnswer.php';
268        require_once 'Services/RTE/classes/class.ilRTE.php';
269
270        while ($data = $ilDB->fetchAssoc($res)) {
271            $answer = new ilAssKprimChoiceAnswer();
272
273            $answer->setPosition($data['position']);
274
275            $answer->setAnswertext(ilRTE::_replaceMediaObjectImageSrc($data['answertext'], 1));
276
277            $answer->setImageFile($data['imagefile']);
278            $answer->setThumbPrefix($this->getThumbPrefix());
279            $answer->setImageFsDir($this->getImagePath());
280            $answer->setImageWebDir($this->getImagePathWeb());
281
282            $answer->setCorrectness($data['correctness']);
283
284            $this->answers[$answer->getPosition()] = $answer;
285        }
286
287        for ($i = count($this->answers); $i < self::NUM_REQUIRED_ANSWERS; $i++) {
288            $answer = new ilAssKprimChoiceAnswer();
289
290            $answer->setPosition($i);
291
292            $this->answers[$answer->getPosition()] = $answer;
293        }
294    }
295
296    public function saveToDb($originalId = '')
297    {
298        $this->saveQuestionDataToDb($originalId);
299
300        $this->saveAdditionalQuestionDataToDb();
301        $this->saveAnswerSpecificDataToDb();
302
303        parent::saveToDb($originalId);
304    }
305
306    public function saveAdditionalQuestionDataToDb()
307    {
308        $this->db->replace(
309            $this->getAdditionalTableName(),
310            array(
311                'question_fi' => array('integer', $this->getId())
312            ),
313            array(
314                'shuffle_answers' => array('integer', (int) $this->isShuffleAnswersEnabled()),
315                'answer_type' => array('text', $this->getAnswerType()),
316                'thumb_size' => array('integer', (int) $this->getThumbSize()),
317                'opt_label' => array('text', $this->getOptionLabel()),
318                'custom_true' => array('text', $this->getCustomTrueOptionLabel()),
319                'custom_false' => array('text', $this->getCustomFalseOptionLabel()),
320                'score_partsol' => array('integer', (int) $this->isScorePartialSolutionEnabled()),
321                'feedback_setting' => array('integer', (int) $this->getSpecificFeedbackSetting())
322            )
323        );
324    }
325
326    public function saveAnswerSpecificDataToDb()
327    {
328        foreach ($this->getAnswers() as $answer) {
329            $this->db->replace(
330                $this->getAnswerTableName(),
331                array(
332                    'question_fi' => array('integer', (int) $this->getId()),
333                    'position' => array('integer', (int) $answer->getPosition())
334                ),
335                array(
336                    'answertext' => array('text', $answer->getAnswertext()),
337                    'imagefile' => array('text', $answer->getImageFile()),
338                    'correctness' => array('integer', (int) $answer->getCorrectness())
339                )
340            );
341        }
342
343        $this->rebuildThumbnails();
344    }
345
346    public function isComplete()
347    {
348        foreach (array($this->title, $this->author, $this->question) as $text) {
349            if (!strlen($text)) {
350                return false;
351            }
352        }
353
354        if ($this->getMaximumPoints() <= 0) {
355            return false;
356        }
357
358        foreach ($this->getAnswers() as $answer) {
359            /* @var ilAssKprimChoiceAnswer $answer */
360
361            if (is_null($answer->getCorrectness())) {
362                return false;
363            }
364
365            if (!strlen($answer->getAnswertext()) && !strlen($answer->getImageFile())) {
366                return false;
367            }
368        }
369
370        return true;
371    }
372
373    /**
374     * Saves the learners input of the question to the database.
375     *
376     * @access public
377     * @param integer $active_id Active id of the user
378     * @param integer $pass Test pass
379     * @return boolean $status
380     */
381    public function saveWorkingData($active_id, $pass = null, $authorized = true)
382    {
383        /** @var ilDBInterface $ilDB */
384        $ilDB = $GLOBALS['DIC']['ilDB'];
385
386        if (is_null($pass)) {
387            include_once "./Modules/Test/classes/class.ilObjTest.php";
388            $pass = ilObjTest::_getPass($active_id);
389        }
390
391        $entered_values = 0;
392
393        $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(function () use (&$entered_values, $active_id, $pass, $authorized) {
394            $this->removeCurrentSolution($active_id, $pass, $authorized);
395
396            $solutionSubmit = $this->getSolutionSubmit();
397
398            foreach ($solutionSubmit as $answerIndex => $answerValue) {
399                $this->saveCurrentSolution($active_id, $pass, (int) $answerIndex, (int) $answerValue, $authorized);
400                $entered_values++;
401            }
402        });
403
404        if ($entered_values) {
405            include_once("./Modules/Test/classes/class.ilObjAssessmentFolder.php");
406            if (ilObjAssessmentFolder::_enabledAssessmentLogging()) {
407                assQuestion::logAction($this->lng->txtlng("assessment", "log_user_entered_values", ilObjAssessmentFolder::_getLogLanguage()), $active_id, $this->getId());
408            }
409        } else {
410            include_once("./Modules/Test/classes/class.ilObjAssessmentFolder.php");
411            if (ilObjAssessmentFolder::_enabledAssessmentLogging()) {
412                assQuestion::logAction($this->lng->txtlng("assessment", "log_user_not_entered_values", ilObjAssessmentFolder::_getLogLanguage()), $active_id, $this->getId());
413            }
414        }
415
416        return true;
417    }
418
419    /**
420     * Returns the points, a learner has reached answering the question.
421     * The points are calculated from the given answers.
422     *
423     * @access public
424     * @param integer $active_id
425     * @param integer $pass
426     * @param boolean $returndetails (deprecated !!)
427     * @return integer/array $points/$details (array $details is deprecated !!)
428     */
429    public function calculateReachedPoints($active_id, $pass = null, $authorizedSolution = true, $returndetails = false)
430    {
431        if ($returndetails) {
432            throw new ilTestException('return details not implemented for ' . __METHOD__);
433        }
434
435        global $DIC;
436        $ilDB = $DIC['ilDB'];
437
438        $found_values = array();
439        if (is_null($pass)) {
440            $pass = $this->getSolutionMaxPass($active_id);
441        }
442
443        $result = $this->getCurrentSolutionResultSet($active_id, $pass, $authorizedSolution);
444
445        while ($data = $ilDB->fetchAssoc($result)) {
446            $found_values[(int) $data['value1']] = (int) $data['value2'];
447        }
448
449        $points = $this->calculateReachedPointsForSolution($found_values, $active_id);
450
451        return $points;
452    }
453
454    public function getValidAnswerTypes()
455    {
456        return array(self::ANSWER_TYPE_SINGLE_LINE, self::ANSWER_TYPE_MULTI_LINE);
457    }
458
459    public function isValidAnswerType($answerType)
460    {
461        $validTypes = $this->getValidAnswerTypes();
462        return in_array($answerType, $validTypes);
463    }
464
465    public function isSingleLineAnswerType($answerType)
466    {
467        return $answerType == assKprimChoice::ANSWER_TYPE_SINGLE_LINE;
468    }
469
470    /**
471     * @param ilLanguage $lng
472     * @return array
473     */
474    public function getAnswerTypeSelectOptions(ilLanguage $lng)
475    {
476        return array(
477            self::ANSWER_TYPE_SINGLE_LINE => $lng->txt('answers_singleline'),
478            self::ANSWER_TYPE_MULTI_LINE => $lng->txt('answers_multiline')
479        );
480    }
481
482    public function getValidOptionLabels()
483    {
484        return array(
485            self::OPTION_LABEL_RIGHT_WRONG,
486            self::OPTION_LABEL_PLUS_MINUS,
487            self::OPTION_LABEL_APPLICABLE_OR_NOT,
488            self::OPTION_LABEL_ADEQUATE_OR_NOT,
489            self::OPTION_LABEL_CUSTOM
490        );
491    }
492
493    public function getValidOptionLabelsTranslated(ilLanguage $lng)
494    {
495        return array(
496            self::OPTION_LABEL_RIGHT_WRONG => $lng->txt('option_label_right_wrong'),
497            self::OPTION_LABEL_PLUS_MINUS => $lng->txt('option_label_plus_minus'),
498            self::OPTION_LABEL_APPLICABLE_OR_NOT => $lng->txt('option_label_applicable_or_not'),
499            self::OPTION_LABEL_ADEQUATE_OR_NOT => $lng->txt('option_label_adequate_or_not'),
500            self::OPTION_LABEL_CUSTOM => $lng->txt('option_label_custom')
501        );
502    }
503
504    public function isValidOptionLabel($optionLabel)
505    {
506        $validLabels = $this->getValidOptionLabels();
507        return in_array($optionLabel, $validLabels);
508    }
509
510    public function getTrueOptionLabelTranslation(ilLanguage $lng, $optionLabel)
511    {
512        switch ($optionLabel) {
513            case self::OPTION_LABEL_RIGHT_WRONG:
514                return $lng->txt('option_label_right');
515
516            case self::OPTION_LABEL_PLUS_MINUS:
517                return $lng->txt('option_label_plus');
518
519            case self::OPTION_LABEL_APPLICABLE_OR_NOT:
520                return $lng->txt('option_label_applicable');
521
522            case self::OPTION_LABEL_ADEQUATE_OR_NOT:
523                return $lng->txt('option_label_adequate');
524
525            case self::OPTION_LABEL_CUSTOM:
526                return $this->getCustomTrueOptionLabel();
527        }
528    }
529
530    public function getFalseOptionLabelTranslation(ilLanguage $lng, $optionLabel)
531    {
532        switch ($optionLabel) {
533            case self::OPTION_LABEL_RIGHT_WRONG:
534                return $lng->txt('option_label_wrong');
535
536            case self::OPTION_LABEL_PLUS_MINUS:
537                return $lng->txt('option_label_minus');
538
539            case self::OPTION_LABEL_APPLICABLE_OR_NOT:
540                return $lng->txt('option_label_not_applicable');
541
542            case self::OPTION_LABEL_ADEQUATE_OR_NOT:
543                return $lng->txt('option_label_not_adequate');
544
545            case self::OPTION_LABEL_CUSTOM:
546                return $this->getCustomFalseOptionLabel();
547        }
548    }
549
550    public function getInstructionTextTranslation(ilLanguage $lng, $optionLabel)
551    {
552        return sprintf(
553            $lng->txt('kprim_instruction_text'),
554            $this->getTrueOptionLabelTranslation($lng, $optionLabel),
555            $this->getFalseOptionLabelTranslation($lng, $optionLabel)
556        );
557    }
558
559    public function isCustomOptionLabel($labelValue)
560    {
561        return $labelValue == self::OPTION_LABEL_CUSTOM;
562    }
563
564    public function getThumbPrefix()
565    {
566        return self::THUMB_PREFIX;
567    }
568
569    public function rebuildThumbnails()
570    {
571        if ($this->isSingleLineAnswerType($this->getAnswerType()) && $this->getThumbSize()) {
572            foreach ($this->getAnswers() as $answer) {
573                if (strlen($answer->getImageFile())) {
574                    $this->generateThumbForFile($answer->getImageFsDir(), $answer->getImageFile());
575                }
576            }
577        }
578    }
579
580    protected function generateThumbForFile($path, $file)
581    {
582        $filename = $path . $file;
583        if (@file_exists($filename)) {
584            $thumbpath = $path . $this->getThumbPrefix() . $file;
585            $path_info = @pathinfo($filename);
586            $ext = "";
587            switch (strtoupper($path_info['extension'])) {
588                case 'PNG':
589                    $ext = 'PNG';
590                    break;
591                case 'GIF':
592                    $ext = 'GIF';
593                    break;
594                default:
595                    $ext = 'JPEG';
596                    break;
597            }
598            ilUtil::convertImage($filename, $thumbpath, $ext, $this->getThumbSize());
599        }
600    }
601
602    public function handleFileUploads($answers, $files)
603    {
604        foreach ($answers as $answer) {
605            /* @var ilAssKprimChoiceAnswer $answer */
606
607            if (!isset($files[$answer->getPosition()])) {
608                continue;
609            }
610
611            $this->handleFileUpload($answer, $files[$answer->getPosition()]);
612        }
613    }
614
615    private function handleFileUpload(ilAssKprimChoiceAnswer $answer, $fileData)
616    {
617        $imagePath = $this->getImagePath();
618
619        if (!file_exists($imagePath)) {
620            ilUtil::makeDirParents($imagePath);
621        }
622
623        $filename = $this->buildHashedImageFilename($fileData['name'], true);
624
625        $answer->setImageFsDir($imagePath);
626        $answer->setImageFile($filename);
627
628        if (!ilUtil::moveUploadedFile($fileData['tmp_name'], $fileData['name'], $answer->getImageFsPath())) {
629            return 2;
630        }
631
632        return 0;
633    }
634
635    public function removeAnswerImage($position)
636    {
637        $answer = $this->getAnswer($position);
638
639        if (file_exists($answer->getImageFsPath())) {
640            ilUtil::delDir($answer->getImageFsPath());
641        }
642
643        if (file_exists($answer->getThumbFsPath())) {
644            ilUtil::delDir($answer->getThumbFsPath());
645        }
646
647        $answer->setImageFile(null);
648    }
649
650    protected function getSolutionSubmit()
651    {
652        $solutionSubmit = array();
653        foreach ($_POST as $key => $value) {
654            $matches = null;
655
656            if (preg_match("/^kprim_choice_result_(\d+)/", $key, $matches)) {
657                if (strlen($value)) {
658                    $solutionSubmit[$matches[1]] = $value;
659                }
660            }
661        }
662        return $solutionSubmit;
663    }
664
665    protected function calculateReachedPointsForSolution($found_values, $active_id = 0)
666    {
667        $numCorrect = 0;
668
669        foreach ($this->getAnswers() as $key => $answer) {
670            if (!isset($found_values[$answer->getPosition()])) {
671                continue;
672            }
673
674            if ($found_values[$answer->getPosition()] == $answer->getCorrectness()) {
675                $numCorrect++;
676            }
677        }
678
679        if ($numCorrect >= self::NUM_REQUIRED_ANSWERS) {
680            $points = $this->getPoints();
681        } elseif ($this->isScorePartialSolutionEnabled() && $numCorrect >= self::PARTIAL_SCORING_NUM_CORRECT_ANSWERS) {
682            $points = $this->getPoints() / 2;
683        } else {
684            $points = 0;
685        }
686
687        if ($active_id) {
688            include_once "./Modules/Test/classes/class.ilObjTest.php";
689            $mc_scoring = ilObjTest::_getMCScoring($active_id);
690            if (($mc_scoring == 0) && (count($found_values) == 0)) {
691                $points = 0;
692            }
693        }
694        return $points;
695    }
696
697    public function duplicate($for_test = true, $title = "", $author = "", $owner = "", $testObjId = null)
698    {
699        if ($this->id <= 0) {
700            // The question has not been saved. It cannot be duplicated
701            return;
702        }
703        // duplicate the question in database
704        $this_id = $this->getId();
705        $thisObjId = $this->getObjId();
706
707        $clone = $this;
708        include_once("./Modules/TestQuestionPool/classes/class.assQuestion.php");
709        $original_id = assQuestion::_getOriginalId($this->id);
710        $clone->id = -1;
711
712        if ((int) $testObjId > 0) {
713            $clone->setObjId($testObjId);
714        }
715
716        if ($title) {
717            $clone->setTitle($title);
718        }
719
720        if ($author) {
721            $clone->setAuthor($author);
722        }
723        if ($owner) {
724            $clone->setOwner($owner);
725        }
726
727        if ($for_test) {
728            $clone->saveToDb($original_id);
729        } else {
730            $clone->saveToDb();
731        }
732
733        // copy question page content
734        $clone->copyPageOfQuestion($this_id);
735        // copy XHTML media objects
736        $clone->copyXHTMLMediaObjectsOfQuestion($this_id);
737        // duplicate the images
738        $clone->cloneAnswerImages($this_id, $thisObjId, $clone->getId(), $clone->getObjId());
739
740        $clone->onDuplicate($thisObjId, $this_id, $clone->getObjId(), $clone->getId());
741
742        return $clone->id;
743    }
744
745    public function createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle = "")
746    {
747        if ($this->id <= 0) {
748            // The question has not been saved. It cannot be duplicated
749            return;
750        }
751
752        include_once("./Modules/TestQuestionPool/classes/class.assQuestion.php");
753
754        $sourceQuestionId = $this->id;
755        $sourceParentId = $this->getObjId();
756
757        // duplicate the question in database
758        $clone = $this;
759        $clone->id = -1;
760
761        $clone->setObjId($targetParentId);
762
763        if ($targetQuestionTitle) {
764            $clone->setTitle($targetQuestionTitle);
765        }
766
767        $clone->saveToDb();
768        // copy question page content
769        $clone->copyPageOfQuestion($sourceQuestionId);
770        // copy XHTML media objects
771        $clone->copyXHTMLMediaObjectsOfQuestion($sourceQuestionId);
772        // duplicate the image
773        $clone->cloneAnswerImages($sourceQuestionId, $sourceParentId, $clone->getId(), $clone->getObjId());
774
775        $clone->onCopy($sourceParentId, $sourceQuestionId, $targetParentId, $clone->getId());
776
777        return $clone->id;
778    }
779
780    /**
781     * Copies an assMultipleChoice object
782     */
783    public function copyObject($target_questionpool_id, $title = "")
784    {
785        if ($this->id <= 0) {
786            // The question has not been saved. It cannot be duplicated
787            return;
788        }
789        // duplicate the question in database
790        $clone = $this;
791        include_once("./Modules/TestQuestionPool/classes/class.assQuestion.php");
792        $original_id = assQuestion::_getOriginalId($this->id);
793        $clone->id = -1;
794        $source_questionpool_id = $this->getObjId();
795        $clone->setObjId($target_questionpool_id);
796        if ($title) {
797            $clone->setTitle($title);
798        }
799        $clone->saveToDb();
800        // copy question page content
801        $clone->copyPageOfQuestion($original_id);
802        // copy XHTML media objects
803        $clone->copyXHTMLMediaObjectsOfQuestion($original_id);
804        // duplicate the image
805        $clone->cloneAnswerImages($original_id, $source_questionpool_id, $clone->getId(), $clone->getObjId());
806
807        $clone->onCopy($source_questionpool_id, $original_id, $clone->getObjId(), $clone->getId());
808
809        return $clone->id;
810    }
811
812    protected function beforeSyncWithOriginal($origQuestionId, $dupQuestionId, $origParentObjId, $dupParentObjId)
813    {
814        parent::beforeSyncWithOriginal($origQuestionId, $dupQuestionId, $origParentObjId, $dupParentObjId);
815
816        $question = self::_instanciateQuestion($origQuestionId);
817
818        foreach ($question->getAnswers() as $answer) {
819            $question->removeAnswerImage($answer->getPosition());
820        }
821    }
822
823    protected function afterSyncWithOriginal($origQuestionId, $dupQuestionId, $origParentObjId, $dupParentObjId)
824    {
825        parent::afterSyncWithOriginal($origQuestionId, $dupQuestionId, $origParentObjId, $dupParentObjId);
826
827        $this->cloneAnswerImages($dupQuestionId, $dupParentObjId, $origQuestionId, $origParentObjId);
828    }
829
830    protected function cloneAnswerImages($sourceQuestionId, $sourceParentId, $targetQuestionId, $targetParentId)
831    {
832        /** @var $ilLog ilLogger */
833        global $DIC;
834        $ilLog = $DIC['ilLog'];
835
836        $sourcePath = $this->buildImagePath($sourceQuestionId, $sourceParentId);
837        $targetPath = $this->buildImagePath($targetQuestionId, $targetParentId);
838
839        foreach ($this->getAnswers() as $answer) {
840            $filename = $answer->getImageFile();
841
842            if (strlen($filename)) {
843                if (!file_exists($targetPath)) {
844                    ilUtil::makeDirParents($targetPath);
845                }
846
847                if (file_exists($sourcePath . $filename)) {
848                    if (!copy($sourcePath . $filename, $targetPath . $filename)) {
849                        $ilLog->warning(sprintf(
850                            "Could not clone source image '%s' to '%s' (srcQuestionId: %s|tgtQuestionId: %s|srcParentObjId: %s|tgtParentObjId: %s)",
851                            $sourcePath . $filename,
852                            $targetPath . $filename,
853                            $sourceQuestionId,
854                            $targetQuestionId,
855                            $sourceParentId,
856                            $targetParentId
857                        ));
858                    }
859                }
860
861                if (file_exists($sourcePath . $this->getThumbPrefix() . $filename)) {
862                    if (!copy($sourcePath . $this->getThumbPrefix() . $filename, $targetPath . $this->getThumbPrefix() . $filename)) {
863                        $ilLog->warning(sprintf(
864                            "Could not clone thumbnail source image '%s' to '%s' (srcQuestionId: %s|tgtQuestionId: %s|srcParentObjId: %s|tgtParentObjId: %s)",
865                            $sourcePath . $this->getThumbPrefix() . $filename,
866                            $targetPath . $this->getThumbPrefix() . $filename,
867                            $sourceQuestionId,
868                            $targetQuestionId,
869                            $sourceParentId,
870                            $targetParentId
871                        ));
872                    }
873                }
874            }
875        }
876    }
877
878    protected function getRTETextWithMediaObjects()
879    {
880        $combinedText = parent::getRTETextWithMediaObjects();
881
882        foreach ($this->getAnswers() as $answer) {
883            $combinedText .= $answer->getAnswertext();
884        }
885
886        return $combinedText;
887    }
888
889    /**
890     * @param ilAssSelfAssessmentMigrator $migrator
891     */
892    protected function lmMigrateQuestionTypeSpecificContent(ilAssSelfAssessmentMigrator $migrator)
893    {
894        foreach ($this->getAnswers() as $answer) {
895            /* @var ilAssKprimChoiceAnswer $answer */
896            $answer->setAnswertext($migrator->migrateToLmContent($answer->getAnswertext()));
897        }
898    }
899
900    /**
901     * Returns a JSON representation of the question
902     */
903    public function toJSON()
904    {
905        $this->lng->loadLanguageModule('assessment');
906
907        require_once './Services/RTE/classes/class.ilRTE.php';
908        $result = array();
909        $result['id'] = (int) $this->getId();
910        $result['type'] = (string) $this->getQuestionType();
911        $result['title'] = (string) $this->getTitle();
912        $result['question'] = $this->formatSAQuestion($this->getQuestion());
913        $result['instruction'] = $this->getInstructionTextTranslation(
914            $this->lng,
915            $this->getOptionLabel()
916        );
917        $result['nr_of_tries'] = (int) $this->getNrOfTries();
918        $result['shuffle'] = (bool) $this->isShuffleAnswersEnabled();
919        $result['feedback'] = array(
920            'onenotcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)),
921            'allcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true))
922        );
923
924        $result['trueOptionLabel'] = $this->getTrueOptionLabelTranslation($this->lng, $this->getOptionLabel());
925        $result['falseOptionLabel'] = $this->getFalseOptionLabelTranslation($this->lng, $this->getOptionLabel());
926
927        $result['num_allowed_failures'] = $this->getNumAllowedFailures();
928
929        $answers = array();
930        $has_image = false;
931
932        foreach ($this->getAnswers() as $key => $answer) {
933            if (strlen((string) $answer->getImageFile())) {
934                $has_image = true;
935            }
936
937            $answers[] = array(
938                'answertext' => (string) $this->formatSAQuestion($answer->getAnswertext()),
939                'correctness' => (bool) $answer->getCorrectness(),
940                'order' => (int) $answer->getPosition(),
941                'image' => (string) $answer->getImageFile(),
942                'feedback' => $this->formatSAQuestion(
943                    $this->feedbackOBJ->getSpecificAnswerFeedbackExportPresentation($this->getId(), 0, $key)
944                )
945            );
946        }
947
948        $result['answers'] = $answers;
949
950        if ($has_image) {
951            $result['path'] = $this->getImagePathWeb();
952            $result['thumb'] = $this->getThumbSize();
953        }
954
955        $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId());
956        $result['mobs'] = $mobs;
957
958        return json_encode($result);
959    }
960
961    private function getNumAllowedFailures()
962    {
963        if ($this->isScorePartialSolutionEnabled()) {
964            return self::NUM_REQUIRED_ANSWERS - self::PARTIAL_SCORING_NUM_CORRECT_ANSWERS;
965        }
966
967        return 0;
968    }
969
970    public function getSpecificFeedbackAllCorrectOptionLabel()
971    {
972        return 'feedback_correct_kprim';
973    }
974
975    public static function isObligationPossible($questionId)
976    {
977        return true;
978    }
979
980    public function isAnswered($active_id, $pass = null)
981    {
982        $numExistingSolutionRecords = assQuestion::getNumExistingSolutionRecords($active_id, $pass, $this->getId());
983
984        return $numExistingSolutionRecords >= 4;
985    }
986
987    /**
988     * {@inheritdoc}
989     */
990    public function setExportDetailsXLS($worksheet, $startrow, $active_id, $pass)
991    {
992        parent::setExportDetailsXLS($worksheet, $startrow, $active_id, $pass);
993
994        $solution = $this->getSolutionValues($active_id, $pass);
995
996        $i = 1;
997        foreach ($this->getAnswers() as $id => $answer) {
998            $worksheet->setCell($startrow + $i, 0, $answer->getAnswertext());
999            $worksheet->setBold($worksheet->getColumnCoord(0) . ($startrow + $i));
1000            $correctness = false;
1001            foreach ($solution as $solutionvalue) {
1002                if ($id == $solutionvalue['value1']) {
1003                    $correctness = $solutionvalue['value2'];
1004                    break;
1005                }
1006            }
1007            $worksheet->setCell($startrow + $i, 1, $correctness);
1008            $i++;
1009        }
1010
1011        return $startrow + $i + 1;
1012    }
1013
1014    public function moveAnswerDown($position)
1015    {
1016        if ($position < 0 || $position >= (self::NUM_REQUIRED_ANSWERS - 1)) {
1017            return false;
1018        }
1019
1020        for ($i = 0, $max = count($this->answers); $i < $max; $i++) {
1021            if ($i == $position) {
1022                $movingAnswer = $this->answers[$i];
1023                $targetAnswer = $this->answers[ $i + 1 ];
1024
1025                $movingAnswer->setPosition($position + 1);
1026                $targetAnswer->setPosition($position);
1027
1028                $this->answers[ $i + 1 ] = $movingAnswer;
1029                $this->answers[$i] = $targetAnswer;
1030            }
1031        }
1032    }
1033
1034    public function moveAnswerUp($position)
1035    {
1036        if ($position <= 0 || $position > (self::NUM_REQUIRED_ANSWERS - 1)) {
1037            return false;
1038        }
1039
1040        for ($i = 0, $max = count($this->answers); $i < $max; $i++) {
1041            if ($i == $position) {
1042                $movingAnswer = $this->answers[$i];
1043                $targetAnswer = $this->answers[ $i - 1 ];
1044
1045                $movingAnswer->setPosition($position - 1);
1046                $targetAnswer->setPosition($position);
1047
1048                $this->answers[ $i - 1 ] = $movingAnswer;
1049                $this->answers[$i] = $targetAnswer;
1050            }
1051        }
1052
1053        return true;
1054    }
1055}
1056