ILIAS  release_8 Revision v8.24
class.assMultipleChoice.php
Go to the documentation of this file.
1<?php
2
19require_once './Modules/Test/classes/inc.AssessmentConstants.php';
20
37{
45 public $answers;
46
56
60
64 protected $selectionLimit;
65
69 public function setIsSingleline($isSingleline): void
70 {
72 }
73
77 public function getIsSingleline()
78 {
80 }
81
96 public function __construct(
97 $title = "",
98 $comment = "",
99 $author = "",
100 $owner = -1,
101 $question = "",
103 ) {
105 $this->output_type = $output_type;
106 $this->answers = array();
107 $this->shuffle = 1;
108 $this->selectionLimit = null;
109 $this->feedback_setting = 0;
110 }
111
115 public function getSelectionLimit(): ?int
116 {
118 }
119
123 public function setSelectionLimit($selectionLimit): void
124 {
125 $this->selectionLimit = $selectionLimit;
126 }
127
134 public function isComplete(): bool
135 {
136 if (strlen($this->title) and ($this->author) and ($this->question) and (count($this->answers)) and ($this->getMaximumPoints() > 0)) {
137 return true;
138 } else {
139 return false;
140 }
141 }
142
148 public function saveToDb($original_id = ""): void
149 {
150 if ($original_id == "") {
151 $this->saveQuestionDataToDb();
152 } else {
154 }
157
158 $this->ensureNoInvalidObligation($this->getId());
159 parent::saveToDb($original_id);
160 }
161
165 protected function rebuildThumbnails(): void
166 {
167 if ($this->isSingleline && ($this->getThumbSize())) {
168 foreach ($this->getAnswers() as $answer) {
169 if (strlen($answer->getImage())) {
170 $this->generateThumbForFile($this->getImagePath(), $answer->getImage());
171 }
172 }
173 }
174 }
175
179 public function getThumbPrefix(): string
180 {
181 return "thumb.";
182 }
183
188 protected function generateThumbForFile($path, $file): void
189 {
190 $filename = $path . $file;
191 if (@file_exists($filename)) {
192 $thumbpath = $path . $this->getThumbPrefix() . $file;
193 $path_info = @pathinfo($filename);
194 $ext = "";
195 switch (strtoupper($path_info['extension'])) {
196 case 'PNG':
197 $ext = 'PNG';
198 break;
199 case 'GIF':
200 $ext = 'GIF';
201 break;
202 default:
203 $ext = 'JPEG';
204 break;
205 }
206 ilShellUtil::convertImage($filename, $thumbpath, $ext, $this->getThumbSize());
207 }
208 }
209
215 public function loadFromDb($question_id): void
216 {
217 global $DIC;
218 $ilDB = $DIC['ilDB'];
219 $hasimages = 0;
220
221 $result = $ilDB->queryF(
222 "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",
223 array("integer"),
224 array($question_id)
225 );
226 if ($result->numRows() == 1) {
227 $data = $ilDB->fetchAssoc($result);
228 $this->setId($question_id);
229 $this->setObjId($data["obj_fi"]);
230 $this->setTitle($data["title"] ?? '');
231 $this->setNrOfTries($data['nr_of_tries']);
232 $this->setComment($data["description"] ?? '');
233 $this->setOriginalId($data["original_id"]);
234 $this->setAuthor($data["author"]);
235 $this->setPoints($data["points"]);
236 $this->setOwner($data["owner"]);
237 include_once("./Services/RTE/classes/class.ilRTE.php");
238 $this->setQuestion(ilRTE::_replaceMediaObjectImageSrc($data["question_text"] ?? '', 1));
239 $shuffle = (is_null($data['shuffle'])) ? true : $data['shuffle'];
240 $this->setShuffle((bool) $shuffle);
241 if ($data['thumb_size'] !== null && $data['thumb_size'] >= $this->getMinimumThumbSize()) {
242 $this->setThumbSize($data['thumb_size']);
243 }
244 $this->isSingleline = $data['allow_images'] === null || $data['allow_images'] === '0';
245 $this->lastChange = $data['tstamp'];
246 $this->setSelectionLimit((int) $data['selection_limit'] > 0 ? (int) $data['selection_limit'] : null);
247 $this->feedback_setting = $data['feedback_setting'];
248
249 try {
250 $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle']));
253 }
254
255 try {
256 $this->setAdditionalContentEditingMode($data['add_cont_edit_mode']);
258 }
259 }
260
261 $result = $ilDB->queryF(
262 "SELECT * FROM qpl_a_mc WHERE question_fi = %s ORDER BY aorder ASC",
263 array('integer'),
264 array($question_id)
265 );
266 include_once "./Modules/TestQuestionPool/classes/class.assAnswerMultipleResponseImage.php";
267 if ($result->numRows() > 0) {
268 while ($data = $ilDB->fetchAssoc($result)) {
269 $imagefilename = $this->getImagePath() . $data["imagefile"];
270 if (!@file_exists($imagefilename)) {
271 $data["imagefile"] = "";
272 }
273 include_once("./Services/RTE/classes/class.ilRTE.php");
274 $data["answertext"] = ilRTE::_replaceMediaObjectImageSrc($data["answertext"] ?? '', 1);
275
277 $data["answertext"],
278 $data["points"],
279 $data["aorder"],
280 $data["answer_id"]
281 );
282 $answer->setPointsUnchecked($data["points_unchecked"]);
283 $answer->setImage($data["imagefile"]);
284 array_push($this->answers, $answer);
285 }
286 }
287
288 parent::loadFromDb($question_id);
289 }
290
294 public function duplicate(bool $for_test = true, string $title = "", string $author = "", string $owner = "", $testObjId = null): int
295 {
296 if ($this->id <= 0) {
297 // The question has not been saved. It cannot be duplicated
298 return -1;
299 }
300 // duplicate the question in database
301 $this_id = $this->getId();
302 $thisObjId = $this->getObjId();
303
304 $clone = $this;
305 include_once("./Modules/TestQuestionPool/classes/class.assQuestion.php");
307 $clone->id = -1;
308
309 if ((int) $testObjId > 0) {
310 $clone->setObjId($testObjId);
311 }
312
313 if ($title) {
314 $clone->setTitle($title);
315 }
316
317 if ($author) {
318 $clone->setAuthor($author);
319 }
320 if ($owner) {
321 $clone->setOwner($owner);
322 }
323
324 if ($for_test) {
325 $clone->saveToDb($original_id);
326 } else {
327 $clone->saveToDb();
328 }
329
330 // copy question page content
331 $clone->copyPageOfQuestion($this_id);
332 // copy XHTML media objects
333 $clone->copyXHTMLMediaObjectsOfQuestion($this_id);
334 // duplicate the images
335 $clone->duplicateImages($this_id, $thisObjId);
336
337 $clone->onDuplicate($thisObjId, $this_id, $clone->getObjId(), $clone->getId());
338
339 return $clone->id;
340 }
341
345 public function copyObject($target_questionpool_id, $title = ""): int
346 {
347 if ($this->getId() <= 0) {
348 throw new RuntimeException('The question has not been saved. It cannot be duplicated');
349 }
350 // duplicate the question in database
351 $clone = $this;
352 include_once("./Modules/TestQuestionPool/classes/class.assQuestion.php");
354 $clone->id = -1;
355 $source_questionpool_id = $this->getObjId();
356 $clone->setObjId($target_questionpool_id);
357 if ($title) {
358 $clone->setTitle($title);
359 }
360 $clone->saveToDb();
361 // copy question page content
362 $clone->copyPageOfQuestion($original_id);
363 // copy XHTML media objects
364 $clone->copyXHTMLMediaObjectsOfQuestion($original_id);
365 // duplicate the image
366 $clone->copyImages($original_id, $source_questionpool_id);
367
368 $clone->onCopy($source_questionpool_id, $original_id, $clone->getObjId(), $clone->getId());
369
370 return $clone->id;
371 }
372
373 public function createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle = ""): int
374 {
375 if ($this->getId() <= 0) {
376 throw new RuntimeException('The question has not been saved. It cannot be duplicated');
377 }
378
379 include_once("./Modules/TestQuestionPool/classes/class.assQuestion.php");
380
381 $sourceQuestionId = $this->id;
382 $sourceParentId = $this->getObjId();
383
384 // duplicate the question in database
385 $clone = $this;
386 $clone->id = -1;
387
388 $clone->setObjId($targetParentId);
389
390 if ($targetQuestionTitle) {
391 $clone->setTitle($targetQuestionTitle);
392 }
393
394 $clone->saveToDb();
395 // copy question page content
396 $clone->copyPageOfQuestion($sourceQuestionId);
397 // copy XHTML media objects
398 $clone->copyXHTMLMediaObjectsOfQuestion($sourceQuestionId);
399 // duplicate the image
400 $clone->copyImages($sourceQuestionId, $sourceParentId);
401
402 $clone->onCopy($sourceParentId, $sourceQuestionId, $clone->getObjId(), $clone->getId());
403
404 return $clone->id;
405 }
406
413 public function getOutputType(): int
414 {
415 return $this->output_type;
416 }
417
425 public function setOutputType($output_type = OUTPUT_ORDER): void
426 {
427 $this->output_type = $output_type;
428 }
429
444 public function addAnswer(
445 $answertext = "",
446 $points = 0.0,
447 $points_unchecked = 0.0,
448 $order = 0,
449 $answerimage = "",
450 $answer_id = -1
451 ): void {
452 $answertext = $this->getHtmlQuestionContentPurifier()->purify($answertext);
453 if (array_key_exists($order, $this->answers)) {
454 // insert answer
455 $answer = new ASS_AnswerMultipleResponseImage($answertext, $points, $order, -1, 0);
456 $answer->setPointsUnchecked($points_unchecked);
457 $answer->setImage($answerimage);
458 $newchoices = array();
459 for ($i = 0; $i < $order; $i++) {
460 $newchoices[] = $this->answers[$i];
461 }
462 $newchoices[] = $answer;
463 for ($i = $order, $iMax = count($this->answers); $i < $iMax; $i++) {
464 $changed = $this->answers[$i];
465 $changed->setOrder($i + 1);
466 $newchoices[] = $changed;
467 }
468 $this->answers = $newchoices;
469 } else {
470 $answer = new ASS_AnswerMultipleResponseImage($answertext, $points, count($this->answers), (int) $answer_id, 0);
471 $answer->setPointsUnchecked($points_unchecked);
472 $answer->setImage($answerimage);
473 $this->answers[] = $answer;
474 }
475 }
476
483 public function getAnswerCount(): int
484 {
485 return count($this->answers);
486 }
487
496 public function getAnswer($index = 0): ?object
497 {
498 if ($index < 0) {
499 return null;
500 }
501 if (count($this->answers) < 1) {
502 return null;
503 }
504 if ($index >= count($this->answers)) {
505 return null;
506 }
507
508 return $this->answers[$index];
509 }
510
518 public function deleteAnswer($index = 0): void
519 {
520 if ($index < 0) {
521 return;
522 }
523 if (count($this->answers) < 1) {
524 return;
525 }
526 if ($index >= count($this->answers)) {
527 return;
528 }
529 $answer = $this->answers[$index];
530 if (strlen($answer->getImage())) {
531 $this->deleteImage($answer->getImage());
532 }
533 unset($this->answers[$index]);
534 $this->answers = array_values($this->answers);
535 for ($i = 0, $iMax = count($this->answers); $i < $iMax; $i++) {
536 if ($this->answers[$i]->getOrder() > $index) {
537 $this->answers[$i]->setOrder($i);
538 }
539 }
540 }
541
547 public function flushAnswers(): void
548 {
549 $this->answers = array();
550 }
551
557 public function getMaximumPoints(): float
558 {
559 $points = 0;
560 $allpoints = 0;
561 foreach ($this->answers as $key => $value) {
562 if ($value->getPoints() > $value->getPointsUnchecked()) {
563 $allpoints += $value->getPoints();
564 } else {
565 $allpoints += $value->getPointsUnchecked();
566 }
567 }
568 return $allpoints;
569 }
570
582 public function calculateReachedPoints($active_id, $pass = null, $authorizedSolution = true, $returndetails = false)
583 {
584 if ($returndetails) {
585 throw new ilTestException('return details not implemented for ' . __METHOD__);
586 }
587
588 global $DIC;
589 $ilDB = $DIC['ilDB'];
590
591 $found_values = array();
592 if (is_null($pass)) {
593 $pass = $this->getSolutionMaxPass($active_id);
594 }
595 $result = $this->getCurrentSolutionResultSet($active_id, $pass, $authorizedSolution);
596 while ($data = $ilDB->fetchAssoc($result)) {
597 if (strcmp($data["value1"], "") != 0) {
598 array_push($found_values, $data["value1"]);
599 }
600 }
601
602 $points = $this->calculateReachedPointsForSolution($found_values, $active_id);
603
604 return $points;
605 }
606
607 public function validateSolutionSubmit(): bool
608 {
609 $submit = $this->getSolutionSubmit();
610
611 if ($this->getSelectionLimit()) {
612 if (count($submit) > $this->getSelectionLimit()) {
613 $failureMsg = sprintf(
614 $this->lng->txt('ass_mc_sel_lim_exhausted_hint'),
615 $this->getSelectionLimit(),
616 $this->getAnswerCount()
617 );
618
619 $this->tpl->setOnScreenMessage('failure', $failureMsg, true);
620 return false;
621 }
622 }
623
624 return true;
625 }
626
627 protected function isForcedEmptySolution($solutionSubmit): bool
628 {
629 if (!count($solutionSubmit) && !empty($_POST['tst_force_form_diff_input'])) {
630 return true;
631 }
632
633 return false;
634 }
635
644 public function saveWorkingData($active_id, $pass = null, $authorized = true): bool
645 {
647 global $DIC;
648 $ilDB = $DIC['ilDB'];
649
650 if (is_null($pass)) {
651 include_once "./Modules/Test/classes/class.ilObjTest.php";
652 $pass = ilObjTest::_getPass($active_id);
653 }
654
655 $entered_values = 0;
656
657 $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(function () use (&$entered_values, $active_id, $pass, $authorized) {
658 $this->removeCurrentSolution($active_id, $pass, $authorized);
659
660 $solutionSubmit = $this->getSolutionSubmit();
661
662 foreach ($solutionSubmit as $value) {
663 if (strlen($value)) {
664 $this->saveCurrentSolution($active_id, $pass, $value, null, $authorized);
665 $entered_values++;
666 }
667 }
668
669 // fau: testNav - write a dummy entry for the evil mc questions with "None of the above" checked
670 if ($this->isForcedEmptySolution($solutionSubmit)) {
671 $this->saveCurrentSolution($active_id, $pass, 'mc_none_above', null, $authorized);
672 $entered_values++;
673 }
674 // fau.
675 });
676
677 if ($entered_values) {
678 include_once("./Modules/Test/classes/class.ilObjAssessmentFolder.php");
680 assQuestion::logAction($this->lng->txtlng(
681 "assessment",
682 "log_user_entered_values",
684 ), $active_id, $this->getId());
685 }
686 } else {
687 include_once("./Modules/Test/classes/class.ilObjAssessmentFolder.php");
689 assQuestion::logAction($this->lng->txtlng(
690 "assessment",
691 "log_user_not_entered_values",
693 ), $active_id, $this->getId());
694 }
695 }
696
697 return true;
698 }
699
700 public function saveAdditionalQuestionDataToDb()
701 {
703 global $DIC;
704 $ilDB = $DIC['ilDB'];
705 $oldthumbsize = 0;
706 if ($this->isSingleline && ($this->getThumbSize())) {
707 // get old thumbnail size
708 $result = $ilDB->queryF(
709 "SELECT thumb_size FROM " . $this->getAdditionalTableName() . " WHERE question_fi = %s",
710 ['integer'],
711 [$this->getId()]
712 );
713 if ($result->numRows() == 1) {
714 $data = $ilDB->fetchAssoc($result);
715 $oldthumbsize = $data['thumb_size'];
716 }
717 }
718
719 if (!$this->isSingleline) {
720 ilFileUtils::delDir($this->getImagePath());
721 }
722
723 // save additional data
724 $ilDB->replace(
725 $this->getAdditionalTableName(),
726 [
727 'shuffle' => array('text', $this->getShuffle()),
728 'allow_images' => array('text', $this->isSingleline ? 0 : 1),
729 'thumb_size' => array('integer', strlen($this->getThumbSize()) ? $this->getThumbSize() : null),
730 'selection_limit' => array('integer', $this->getSelectionLimit()),
731 'feedback_setting' => array('integer', $this->getSpecificFeedbackSetting())
732 ],
733 ['question_fi' => array('integer', $this->getId())]
734 );
735 }
736
742 public function saveAnswerSpecificDataToDb()
743 {
745 global $DIC;
746 $ilDB = $DIC['ilDB'];
747
748 // Get all feedback entries
749 $result = $ilDB->queryF(
750 "SELECT * FROM qpl_fb_specific WHERE question_fi = %s",
751 ['integer'],
752 [$this->getId()]
753 );
754 $db_feedback = $ilDB->fetchAll($result);
755
756 // Check if feedback exists and the regular editor is used and not the page editor
757 if (sizeof($db_feedback) >= 1 && $this->getAdditionalContentEditingMode() == 'default') {
758 // Get all existing answer data for question
759 $result = $ilDB->queryF(
760 "SELECT answer_id, aorder FROM qpl_a_mc WHERE question_fi = %s",
761 ['integer'],
762 [$this->getId()]
763 );
764 $db_answers = $ilDB->fetchAll($result);
765
766 // Collect old and new order entries by ids and order to calculate a diff/intersection and remove/update feedback
767 $post_answer_order_for_id = [];
768 foreach ($this->answers as $answer) {
769 // Only the first appearance of an id is used
770 if ($answer->getId() !== null && !in_array($answer->getId(), array_keys($post_answer_order_for_id))) {
771 // -1 is happening while import and also if a new multi line answer is generated
772 if ($answer->getId() == -1) {
773 continue;
774 }
775 $post_answer_order_for_id[$answer->getId()] = $answer->getOrder();
776 }
777 }
778
779 // If there is no usable ids from post, it's better to not touch the feedback
780 // This is useful since the import is also using this function or the first creation of a new question in general
781 if (sizeof($post_answer_order_for_id) >= 1) {
782 $db_answer_order_for_id = [];
783 $db_answer_id_for_order = [];
784 foreach ($db_answers as $db_answer) {
785 $db_answer_order_for_id[intval($db_answer['answer_id'])] = intval($db_answer['aorder']);
786 $db_answer_id_for_order[intval($db_answer['aorder'])] = intval($db_answer['answer_id']);
787 }
788
789 // Handle feedback
790 // the diff between the already existing answer ids from the Database and the answer ids from post
791 // feedback related to the answer ids should be deleted or in our case not recreated.
792 $db_answer_ids = array_keys($db_answer_order_for_id);
793 $post_answer_ids = array_keys($post_answer_order_for_id);
794 $diff_db_post_answer_ids = array_diff($db_answer_ids, $post_answer_ids);
795 $unused_answer_ids = array_keys($diff_db_post_answer_ids);
796
797 // Delete all feedback in the database
798 $this->feedbackOBJ->deleteSpecificAnswerFeedbacks($this->getId(), false);
799 // Recreate feedback
800 foreach ($db_feedback as $feedback_option) {
801 // skip feedback which answer is deleted
802 if (in_array(intval($feedback_option['answer']), $unused_answer_ids)) {
803 continue;
804 }
805
806 // Reorder feedback
807 $feedback_order_db = intval($feedback_option['answer']);
808 $db_answer_id = $db_answer_id_for_order[$feedback_order_db];
809 // This cuts feedback that currently would have no corresponding answer
810 // This case can happen while copying "broken" questions
811 // Or when saving a question with less answers than feedback
812 if (is_null($db_answer_id) || $db_answer_id < 0) {
813 continue;
814 }
815 $feedback_order_post = $post_answer_order_for_id[$db_answer_id];
816 $feedback_option['answer'] = $feedback_order_post;
817
818 // Recreate remaining feedback in database
819 $next_id = $ilDB->nextId('qpl_fb_specific');
820 $ilDB->manipulateF(
821 "INSERT INTO qpl_fb_specific (feedback_id, question_fi, answer, tstamp, feedback, question)
822 VALUES (%s, %s, %s, %s, %s, %s)",
823 ['integer', 'integer', 'integer', 'integer', 'text', 'integer'],
824 [
825 $next_id,
826 $feedback_option['question_fi'],
827 $feedback_option['answer'],
828 time(),
829 $feedback_option['feedback'],
830 $feedback_option['question']
831 ]
832 );
833 }
834 }
835 }
836
837 // Delete all entries in qpl_a_mc for question
838 $ilDB->manipulateF(
839 "DELETE FROM qpl_a_mc WHERE question_fi = %s",
840 ['integer'],
841 [$this->getId()]
842 );
843
844 // Recreate answers one by one
845 foreach ($this->answers as $key => $value) {
846 $answer_obj = $this->answers[$key];
847 $next_id = $ilDB->nextId('qpl_a_mc');
848 $ilDB->manipulateF(
849 "INSERT INTO qpl_a_mc (answer_id, question_fi, answertext, points, points_unchecked, aorder, imagefile, tstamp)
850 VALUES (%s, %s, %s, %s, %s, %s, %s, %s)",
851 ['integer', 'integer', 'text', 'float', 'float', 'integer', 'text', 'integer'],
852 [
853 $next_id,
854 $this->getId(),
855 ilRTE::_replaceMediaObjectImageSrc($answer_obj->getAnswertext(), 0),
856 $answer_obj->getPoints(),
857 $answer_obj->getPointsUnchecked(),
858 $answer_obj->getOrder(),
859 $answer_obj->getImage(),
860 time()
861 ]
862 );
863 }
864 $this->rebuildThumbnails();
865 }
866
867 public function syncWithOriginal(): void
868 {
869 if ($this->getOriginalId()) {
870 $this->syncImages();
871 parent::syncWithOriginal();
872 }
873 }
874
880 public function getQuestionType(): string
881 {
882 return "assMultipleChoice";
883 }
884
890 public function getAdditionalTableName(): string
891 {
892 return "qpl_qst_mc";
893 }
894
900 public function getAnswerTableName(): string
901 {
902 return "qpl_a_mc";
903 }
904
912 public function setImageFile($image_filename, $image_tempfilename = ""): int
913 {
914 $result = 0;
915 if (!empty($image_tempfilename)) {
916 $image_filename = str_replace(" ", "_", $image_filename);
917 $imagepath = $this->getImagePath();
918 if (!file_exists($imagepath)) {
919 ilFileUtils::makeDirParents($imagepath);
920 }
921 if (!ilFileUtils::moveUploadedFile($image_tempfilename, $image_filename, $imagepath . $image_filename)) {
922 $result = 2;
923 } else {
924 include_once "./Services/MediaObjects/classes/class.ilObjMediaObject.php";
925 $mimetype = ilObjMediaObject::getMimeType($imagepath . $image_filename);
926 if (!preg_match("/^image/", $mimetype)) {
927 unlink($imagepath . $image_filename);
928 $result = 1;
929 } else {
930 // create thumbnail file
931 if ($this->isSingleline && ($this->getThumbSize())) {
932 $this->generateThumbForFile($imagepath, $image_filename);
933 }
934 }
935 }
936 }
937 return $result;
938 }
939
945 protected function deleteImage($image_filename): void
946 {
947 $imagepath = $this->getImagePath();
948 @unlink($imagepath . $image_filename);
949 $thumbpath = $imagepath . $this->getThumbPrefix() . $image_filename;
950 @unlink($thumbpath);
951 }
952
953 public function duplicateImages($question_id, $objectId = null): void
954 {
956 global $DIC;
957 $ilLog = $DIC['ilLog'];
958
959 $imagepath = $this->getImagePath();
960 $imagepath_original = str_replace("/$this->id/images", "/$question_id/images", $imagepath);
961
962 if ((int) $objectId > 0) {
963 $imagepath_original = str_replace("/$this->obj_id/", "/$objectId/", $imagepath_original);
964 }
965
966 foreach ($this->answers as $answer) {
967 $filename = $answer->getImage();
968 if (strlen($filename)) {
969 if (!file_exists($imagepath)) {
970 ilFileUtils::makeDirParents($imagepath);
971 }
972
973 if (file_exists($imagepath_original . $filename)) {
974 if (!copy($imagepath_original . $filename, $imagepath . $filename)) {
975 $ilLog->warning(sprintf(
976 "Could not clone source image '%s' to '%s' (srcQuestionId: %s|tgtQuestionId: %s|srcParentObjId: %s|tgtParentObjId: %s)",
977 $imagepath_original . $filename,
978 $imagepath . $filename,
979 $question_id,
980 $this->id,
981 $objectId,
982 $this->obj_id
983 ));
984 }
985 }
986
987 if (file_exists($imagepath_original . $this->getThumbPrefix() . $filename)) {
988 if (!copy($imagepath_original . $this->getThumbPrefix() . $filename, $imagepath . $this->getThumbPrefix() . $filename)) {
989 $ilLog->warning(sprintf(
990 "Could not clone thumbnail source image '%s' to '%s' (srcQuestionId: %s|tgtQuestionId: %s|srcParentObjId: %s|tgtParentObjId: %s)",
991 $imagepath_original . $this->getThumbPrefix() . $filename,
992 $imagepath . $this->getThumbPrefix() . $filename,
993 $question_id,
994 $this->id,
995 $objectId,
996 $this->obj_id
997 ));
998 }
999 }
1000 }
1001 }
1002 }
1003
1004 public function copyImages($question_id, $source_questionpool): void
1005 {
1006 global $DIC;
1007 $ilLog = $DIC['ilLog'];
1008 $imagepath = $this->getImagePath();
1009 $imagepath_original = str_replace("/$this->id/images", "/$question_id/images", $imagepath);
1010 $imagepath_original = str_replace("/$this->obj_id/", "/$source_questionpool/", $imagepath_original);
1011 foreach ($this->answers as $answer) {
1012 $filename = $answer->getImage();
1013 if (strlen($filename)) {
1014 if (!file_exists($imagepath)) {
1015 ilFileUtils::makeDirParents($imagepath);
1016 }
1017 if (!@copy($imagepath_original . $filename, $imagepath . $filename)) {
1018 $ilLog->write("image could not be duplicated!!!!", $ilLog->ERROR);
1019 $ilLog->write("object: " . print_r($this, true), $ilLog->ERROR);
1020 }
1021 if (@file_exists($imagepath_original . $this->getThumbPrefix() . $filename)) {
1022 if (!@copy($imagepath_original . $this->getThumbPrefix() . $filename, $imagepath . $this->getThumbPrefix() . $filename)) {
1023 $ilLog->write("image thumbnail could not be duplicated!!!!", $ilLog->ERROR);
1024 $ilLog->write("object: " . print_r($this, true), $ilLog->ERROR);
1025 }
1026 }
1027 }
1028 }
1029 }
1030
1034 protected function syncImages(): void
1035 {
1036 global $DIC;
1037 $ilLog = $DIC['ilLog'];
1038 $imagepath = $this->getImagePath();
1039 $question_id = $this->getOriginalId();
1040 $originalObjId = parent::lookupParentObjId($this->getOriginalId());
1041 $imagepath_original = $this->getImagePath($question_id, $originalObjId);
1042
1043 ilFileUtils::delDir($imagepath_original);
1044 foreach ($this->answers as $answer) {
1045 $filename = $answer->getImage();
1046 if (strlen($filename)) {
1047 if (@file_exists($imagepath . $filename)) {
1048 if (!file_exists($imagepath)) {
1049 ilFileUtils::makeDirParents($imagepath);
1050 }
1051 if (!file_exists($imagepath_original)) {
1052 ilFileUtils::makeDirParents($imagepath_original);
1053 }
1054 if (!@copy($imagepath . $filename, $imagepath_original . $filename)) {
1055 $ilLog->write("image could not be duplicated!!!!", $ilLog->ERROR);
1056 $ilLog->write("object: " . print_r($this, true), $ilLog->ERROR);
1057 }
1058 }
1059 if (@file_exists($imagepath . $this->getThumbPrefix() . $filename)) {
1060 if (!@copy($imagepath . $this->getThumbPrefix() . $filename, $imagepath_original . $this->getThumbPrefix() . $filename)) {
1061 $ilLog->write("image thumbnail could not be duplicated!!!!", $ilLog->ERROR);
1062 $ilLog->write("object: " . print_r($this, true), $ilLog->ERROR);
1063 }
1064 }
1065 }
1066 }
1067 }
1068
1072 public function getRTETextWithMediaObjects(): string
1073 {
1074 $text = parent::getRTETextWithMediaObjects();
1075 foreach ($this->answers as $index => $answer) {
1076 $text .= $this->feedbackOBJ->getSpecificAnswerFeedbackContent($this->getId(), 0, $index);
1077 $answer_obj = $this->answers[$index];
1078 $text .= $answer_obj->getAnswertext();
1079 }
1080 return $text;
1081 }
1082
1086 public function &getAnswers(): array
1087 {
1088 return $this->answers;
1089 }
1090
1094 public function setExportDetailsXLS(ilAssExcelFormatHelper $worksheet, int $startrow, int $active_id, int $pass): int
1095 {
1096 parent::setExportDetailsXLS($worksheet, $startrow, $active_id, $pass);
1097
1098 $solution = $this->getSolutionValues($active_id, $pass);
1099
1100 $i = 1;
1101 foreach ($this->getAnswers() as $id => $answer) {
1102 $worksheet->setCell($startrow + $i, 0, $answer->getAnswertext());
1103 $worksheet->setBold($worksheet->getColumnCoord(0) . ($startrow + $i));
1104 $checked = false;
1105 foreach ($solution as $solutionvalue) {
1106 if ($id == $solutionvalue["value1"]) {
1107 $checked = true;
1108 }
1109 }
1110 if ($checked) {
1111 $worksheet->setCell($startrow + $i, 2, 1);
1112 } else {
1113 $worksheet->setCell($startrow + $i, 2, 0);
1114 }
1115 $i++;
1116 }
1117
1118 return $startrow + $i + 1;
1119 }
1120
1125 {
1126 foreach ($this->getAnswers() as $answer) {
1127 /* @var ASS_AnswerBinaryStateImage $answer */
1128 $answer->setAnswertext($migrator->migrateToLmContent($answer->getAnswertext()));
1129 }
1130 }
1131
1135 public function toJSON(): string
1136 {
1137 require_once './Services/RTE/classes/class.ilRTE.php';
1138 $result = array();
1139 $result['id'] = $this->getId();
1140 $result['type'] = (string) $this->getQuestionType();
1141 $result['title'] = $this->getTitleForHTMLOutput();
1142 $result['question'] = $this->formatSAQuestion($this->getQuestion());
1143 $result['nr_of_tries'] = $this->getNrOfTries();
1144 $result['shuffle'] = $this->getShuffle();
1145 $result['selection_limit'] = (int) $this->getSelectionLimit();
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
1151 $answers = array();
1152 $has_image = false;
1153 foreach ($this->getAnswers() as $key => $answer_obj) {
1154 if ((string) $answer_obj->getImage()) {
1155 $has_image = true;
1156 }
1157 array_push($answers, array(
1158 "answertext" => $this->formatSAQuestion($answer_obj->getAnswertext()),
1159 "points_checked" => (float) $answer_obj->getPointsChecked(),
1160 "points_unchecked" => (float) $answer_obj->getPointsUnchecked(),
1161 "order" => (int) $answer_obj->getOrder(),
1162 "image" => (string) $answer_obj->getImage(),
1163 "feedback" => $this->formatSAQuestion(
1164 $this->feedbackOBJ->getSpecificAnswerFeedbackExportPresentation($this->getId(), 0, $key)
1165 )
1166 ));
1167 }
1168 $result['answers'] = $answers;
1169
1170 if ($has_image) {
1171 $result['path'] = $this->getImagePathWeb();
1172 $result['thumb'] = $this->getThumbSize();
1173 }
1174
1175 $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId());
1176 $result['mobs'] = $mobs;
1177
1178 return json_encode($result);
1179 }
1180
1181 public function removeAnswerImage($index): void
1182 {
1183 $answer = $this->answers[$index];
1184 if (is_object($answer)) {
1185 $this->deleteImage($answer->getImage());
1186 $answer->setImage('');
1187 }
1188 }
1189
1191 {
1192 global $DIC;
1193 $ilUser = $DIC['ilUser'];
1194
1195 $multilineAnswerSetting = $ilUser->getPref("tst_multiline_answers");
1196 if ($multilineAnswerSetting != 1) {
1197 $multilineAnswerSetting = 0;
1198 }
1199 return $multilineAnswerSetting;
1200 }
1201
1202 public function setMultilineAnswerSetting($a_setting = 0): void
1203 {
1204 global $DIC;
1205 $ilUser = $DIC['ilUser'];
1206 $ilUser->writePref("tst_multiline_answers", $a_setting);
1207 }
1208
1218 public function setSpecificFeedbackSetting($a_feedback_setting): void
1219 {
1220 $this->feedback_setting = $a_feedback_setting;
1221 }
1222
1233 {
1234 if ($this->feedback_setting) {
1235 return $this->feedback_setting;
1236 } else {
1237 return 1;
1238 }
1239 }
1240
1242 {
1243 return 'feedback_correct_sc_mc';
1244 }
1245
1257 public function isAnswered(int $active_id, int $pass): bool
1258 {
1259 $numExistingSolutionRecords = assQuestion::getNumExistingSolutionRecords($active_id, $pass, $this->getId());
1260
1261 return $numExistingSolutionRecords > 0;
1262 }
1263
1275 public static function isObligationPossible(int $questionId): bool
1276 {
1278 global $DIC;
1279 $ilDB = $DIC['ilDB'];
1280
1281 $query = "
1282 SELECT SUM(points) points_for_checked_answers
1283 FROM qpl_a_mc
1284 WHERE question_fi = %s AND points > 0
1285 ";
1286
1287 $res = $ilDB->queryF($query, array('integer'), array($questionId));
1288
1289 $row = $ilDB->fetchAssoc($res);
1290
1291 return $row['points_for_checked_answers'] > 0;
1292 }
1293
1302 public function ensureNoInvalidObligation($questionId): void
1303 {
1305 global $DIC;
1306 $ilDB = $DIC['ilDB'];
1307
1308 $query = "
1309 SELECT SUM(qpl_a_mc.points) points_for_checked_answers,
1310 test_question_id
1311
1312 FROM tst_test_question
1313
1314 INNER JOIN qpl_a_mc
1315 ON qpl_a_mc.question_fi = tst_test_question.question_fi
1316
1317 WHERE tst_test_question.question_fi = %s
1318 AND tst_test_question.obligatory = 1
1319
1320 GROUP BY test_question_id
1321 ";
1322
1323 $res = $ilDB->queryF($query, array('integer'), array($questionId));
1324
1325 $updateTestQuestionIds = array();
1326
1327 while ($row = $ilDB->fetchAssoc($res)) {
1328 if ($row['points_for_checked_answers'] <= 0) {
1329 $updateTestQuestionIds[] = $row['test_question_id'];
1330 }
1331 }
1332
1333 if (count($updateTestQuestionIds)) {
1334 $test_question_id__IN__updateTestQuestionIds = $ilDB->in(
1335 'test_question_id',
1336 $updateTestQuestionIds,
1337 false,
1338 'integer'
1339 );
1340
1341 $query = "
1342 UPDATE tst_test_question
1343 SET obligatory = 0
1344 WHERE $test_question_id__IN__updateTestQuestionIds
1345 ";
1346
1347 $ilDB->manipulate($query);
1348 }
1349 }
1350
1351 protected function getSolutionSubmit(): array
1352 {
1353 $solutionSubmit = [];
1354 $post = $this->dic->http()->wrapper()->post();
1355
1356 foreach ($this->getAnswers() as $index => $a) {
1357 if ($post->has("multiple_choice_result_$index")) {
1358 $value = $post->retrieve("multiple_choice_result_$index", $this->dic->refinery()->kindlyTo()->string());
1359 if (is_numeric($value)) {
1360 $solutionSubmit[] = $value;
1361 }
1362 }
1363 }
1364 return $solutionSubmit;
1365 }
1366
1372 protected function calculateReachedPointsForSolution($found_values, $active_id = 0): float
1373 {
1374 if ($found_values == null) {
1375 $found_values = [];
1376 }
1377 $points = 0;
1378 foreach ($this->answers as $key => $answer) {
1379 if (in_array($key, $found_values)) {
1380 $points += $answer->getPoints();
1381 } else {
1382 $points += $answer->getPointsUnchecked();
1383 }
1384 }
1385 if ($active_id) {
1386 if (count($found_values) == 0) {
1387 $points = 0;
1388 }
1389 }
1390 return $points;
1391 }
1392
1401 public function getOperators($expression): array
1402 {
1404 }
1405
1410 public function getExpressionTypes(): array
1411 {
1412 return array(
1417 );
1418 }
1419
1428 public function getUserQuestionResult($active_id, $pass): ilUserQuestionResult
1429 {
1431 global $DIC;
1432 $ilDB = $DIC['ilDB'];
1433 $result = new ilUserQuestionResult($this, $active_id, $pass);
1434
1435 $maxStep = $this->lookupMaxStep($active_id, $pass);
1436
1437 if ($maxStep !== null) {
1438 $data = $ilDB->queryF(
1439 "SELECT value1+1 as value1 FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s AND step = %s",
1440 array("integer", "integer", "integer","integer"),
1441 array($active_id, $pass, $this->getId(), $maxStep)
1442 );
1443 } else {
1444 $data = $ilDB->queryF(
1445 "SELECT value1+1 as value1 FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s",
1446 array("integer", "integer", "integer"),
1447 array($active_id, $pass, $this->getId())
1448 );
1449 }
1450
1451 while ($row = $ilDB->fetchAssoc($data)) {
1452 $result->addKeyValue($row["value1"], $row["value1"]);
1453 }
1454
1455 $points = $this->calculateReachedPoints($active_id, $pass);
1456 $max_points = $this->getMaximumPoints();
1457
1458 $result->setReachedPercentage(($points / $max_points) * 100);
1459
1460 return $result;
1461 }
1462
1469 public function getAvailableAnswerOptions($index = null)
1470 {
1471 if ($index !== null) {
1472 return $this->getAnswer($index);
1473 } else {
1474 return $this->getAnswers();
1475 }
1476 }
1477
1479 {
1480 $config = parent::buildTestPresentationConfig();
1481 $config->setUseUnchangedAnswerLabel($this->lng->txt('tst_mc_label_none_above'));
1482 return $config;
1483 }
1484
1485 public function isSingleline(): bool
1486 {
1487 return (bool) $this->isSingleline;
1488 }
1489}
$id
plugin.php for ilComponentBuildPluginInfoObjectiveTest::testAddPlugins
Definition: plugin.php:23
$filename
Definition: buildRTE.php:78
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
buildTestPresentationConfig()
build basic test question configuration instance
syncImages()
Sync images of a MC question on synchronisation with the original question.
toJSON()
Returns a JSON representation of the question.
getSpecificFeedbackSetting()
Gets the current feedback settings in effect for the question.
copyImages($question_id, $source_questionpool)
saveToDb($original_id="")
Saves a assMultipleChoice object to a database.
loadFromDb($question_id)
Loads a assMultipleChoice object from a database.
getMaximumPoints()
Returns the maximum points, a learner can reach answering the question.
getAnswerCount()
Returns the number of answers.
getAnswerTableName()
Returns the name of the answer table in the database.
isForcedEmptySolution($solutionSubmit)
getAdditionalTableName()
Returns the name of the additional question data table in the database.
setSpecificFeedbackSetting($a_feedback_setting)
Sets the feedback settings in effect for the question.
setExportDetailsXLS(ilAssExcelFormatHelper $worksheet, int $startrow, int $active_id, int $pass)
{}
getAvailableAnswerOptions($index=null)
If index is null, the function returns an array with all anwser options Else it returns the specific ...
setSelectionLimit($selectionLimit)
calculateReachedPointsForSolution($found_values, $active_id=0)
addAnswer( $answertext="", $points=0.0, $points_unchecked=0.0, $order=0, $answerimage="", $answer_id=-1)
Adds a possible answer for a multiple choice question.
duplicate(bool $for_test=true, string $title="", string $author="", string $owner="", $testObjId=null)
Duplicates an assMultipleChoiceQuestion.
isAnswered(int $active_id, int $pass)
returns boolean wether the question is answered during test pass or not
getOutputType()
Gets the multiple choice output type which is either OUTPUT_ORDER (=0) or OUTPUT_RANDOM (=1).
copyObject($target_questionpool_id, $title="")
Copies an assMultipleChoice object.
createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle="")
lmMigrateQuestionTypeSpecificContent(ilAssSelfAssessmentMigrator $migrator)
setMultilineAnswerSetting($a_setting=0)
isComplete()
Returns true, if a multiple choice question is complete for use.
flushAnswers()
Deletes all answers.
deleteAnswer($index=0)
Deletes an answer with a given index.
getQuestionType()
Returns the question type of the question.
deleteImage($image_filename)
Deletes an image file.
setImageFile($image_filename, $image_tempfilename="")
Sets the image file and uploads the image to the object's image directory.
__construct( $title="", $comment="", $author="", $owner=-1, $question="", $output_type=OUTPUT_ORDER)
assMultipleChoice constructor
setIsSingleline($isSingleline)
setOutputType($output_type=OUTPUT_ORDER)
Sets the output type of the assMultipleChoice object.
calculateReachedPoints($active_id, $pass=null, $authorizedSolution=true, $returndetails=false)
Returns the points, a learner has reached answering the question.
getAnswer($index=0)
Returns an answer with a given index.
generateThumbForFile($path, $file)
getExpressionTypes()
Get all available expression types for a specific question.
getRTETextWithMediaObjects()
Collects all text in the question which could contain media objects which were created with the Rich ...
rebuildThumbnails()
Rebuild the thumbnail images with a new thumbnail size.
& getAnswers()
Returns a reference to the answers array.
getOperators($expression)
Get all available operations for a specific question.
Abstract basic class which is to be extended by the concrete assessment question type classes.
float $points
The maximum available points for the question.
setOriginalId(?int $original_id)
string $question
The question text.
static logAction(string $logtext, int $active_id, int $question_id)
setId(int $id=-1)
setAdditionalContentEditingMode(?string $additionalContentEditingMode)
setShuffle(?bool $shuffle=true)
setQuestion(string $question="")
getImagePath($question_id=null, $object_id=null)
Returns the image path for web accessable images of a question.
static _getOriginalId(int $question_id)
saveQuestionDataToDb(int $original_id=-1)
setAuthor(string $author="")
setThumbSize(int $a_size)
bool $shuffle
Indicates whether the answers will be shuffled or not.
setComment(string $comment="")
setObjId(int $obj_id=0)
setOwner(int $owner=-1)
setNrOfTries(int $a_nr_of_tries)
setLifecycle(ilAssQuestionLifecycle $lifecycle)
static getNumExistingSolutionRecords(int $activeId, int $pass, int $questionId)
setTitle(string $title="")
setPoints(float $points)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setCell($a_row, $a_col, $a_value, $datatype=null)
setBold(string $a_coords)
Set cell(s) to bold.
getColumnCoord(int $a_col)
Get column "name" from number.
static makeDirParents(string $a_dir)
Create a new directory and all parent directories.
static delDir(string $a_dir, bool $a_clean_only=false)
removes a dir and all its content (subdirs and files) recursively
static moveUploadedFile(string $a_file, string $a_name, string $a_target, bool $a_raise_errors=true, string $a_mode="move_uploaded")
move uploaded file
static _getMobsOfObject(string $a_type, int $a_id, int $a_usage_hist_nr=0, string $a_lang="-")
static getMimeType(string $a_file, bool $a_external=false)
get mime type for file
static _getPass($active_id)
Retrieves the actual pass of a given user for a given test.
static _replaceMediaObjectImageSrc(string $a_text, int $a_direction=0, string $nic='')
Replaces image source from mob image urls with the mob id or replaces mob id with the correct image s...
static convertImage(string $a_from, string $a_to, string $a_target_format="", string $a_geometry="", string $a_background_color="")
convert image
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
global $DIC
Definition: feed.php:28
$mobs
Definition: imgupload.php:70
$ilUser
Definition: imgupload.php:34
const OUTPUT_ORDER
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
saveAnswerSpecificDataToDb()
Saves the answer specific records into a question types answer table.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
saveAdditionalQuestionDataToDb()
Saves a record to the question types additional data table.
$path
Definition: ltiservices.php:32
$res
Definition: ltiservices.php:69
$post
Definition: ltitoken.php:49
$index
Definition: metadata.php:145
if(!array_key_exists('PATH_INFO', $_SERVER)) $config
Definition: metadata.php:85
$i
Definition: metadata.php:41
__construct(Container $dic, ilPlugin $plugin)
@inheritDoc
string $key
Consumer key/client ID value.
Definition: System.php:193
$a
thx to https://mlocati.github.io/php-cs-fixer-configurator for the examples
$query