ILIAS  release_9 Revision v9.13-25-g2c18ec4c24f
class.assMultipleChoice.php
Go to the documentation of this file.
1 <?php
2 
19 require_once './Modules/Test/classes/inc.AssessmentConstants.php';
20 
22 
39 {
48  public $answers;
49 
58  public $output_type;
59 
60  public $isSingleline;
61  public $lastChange;
63 
67  protected $selectionLimit;
68 
72  public function setIsSingleline($isSingleline): void
73  {
74  $this->isSingleline = $isSingleline;
75  }
76 
80  public function getIsSingleline()
81  {
82  return $this->isSingleline;
83  }
84 
99  public function __construct(
100  $title = "",
101  $comment = "",
102  $author = "",
103  $owner = -1,
104  $question = "",
106  ) {
108  $this->output_type = $output_type;
109  $this->answers = [];
110  $this->shuffle = 1;
111  $this->selectionLimit = null;
112  $this->feedback_setting = 0;
113  }
114 
118  public function getSelectionLimit(): ?int
119  {
120  return $this->selectionLimit;
121  }
122 
126  public function setSelectionLimit($selectionLimit): void
127  {
128  $this->selectionLimit = $selectionLimit;
129  }
130 
137  public function isComplete(): bool
138  {
139  return $this->title !== ''
140  && $this->author !== ''
141  && $this->question !== ''
142  && $this->getAnswerCount() > 0
143  && $this->getMaximumPoints() >= 0;
144  }
145 
151  public function saveToDb($original_id = ""): void
152  {
153  if ($original_id == "") {
154  $this->saveQuestionDataToDb();
155  } else {
157  }
160 
161  $this->ensureNoInvalidObligation($this->getId());
162  parent::saveToDb($original_id);
163  }
164 
170  public function loadFromDb($question_id): void
171  {
172  global $DIC;
173  $ilDB = $DIC['ilDB'];
174  $hasimages = 0;
175 
176  $result = $ilDB->queryF(
177  "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",
178  ["integer"],
179  [$question_id]
180  );
181  if ($result->numRows() == 1) {
182  $data = $ilDB->fetchAssoc($result);
183  $this->setId($question_id);
184  $this->setObjId($data["obj_fi"]);
185  $this->setTitle($data["title"] ?? '');
186  $this->setNrOfTries($data['nr_of_tries']);
187  $this->setComment($data["description"] ?? '');
188  $this->setOriginalId($data["original_id"]);
189  $this->setAuthor($data["author"]);
190  $this->setPoints($data["points"]);
191  $this->setOwner($data["owner"]);
192  $this->setQuestion(ilRTE::_replaceMediaObjectImageSrc($data["question_text"] ?? '', 1));
193  $shuffle = (is_null($data['shuffle'])) ? true : $data['shuffle'];
194  $this->setShuffle((bool) $shuffle);
195  if ($data['thumb_size'] !== null && $data['thumb_size'] >= $this->getMinimumThumbSize()) {
196  $this->setThumbSize($data['thumb_size']);
197  }
198  $this->isSingleline = $data['allow_images'] === null || $data['allow_images'] === '0';
199  $this->lastChange = $data['tstamp'];
200  $this->setSelectionLimit((int) $data['selection_limit'] > 0 ? (int) $data['selection_limit'] : null);
201  $this->feedback_setting = $data['feedback_setting'];
202 
203  try {
207  }
208 
209  try {
210  $this->setAdditionalContentEditingMode($data['add_cont_edit_mode']);
211  } catch (ilTestQuestionPoolException $e) {
212  }
213  }
214 
215  $result = $ilDB->queryF(
216  "SELECT * FROM qpl_a_mc WHERE question_fi = %s ORDER BY aorder ASC",
217  ['integer'],
218  [$question_id]
219  );
220  if ($result->numRows() > 0) {
221  while ($data = $ilDB->fetchAssoc($result)) {
222  $imagefilename = $this->getImagePath() . $data["imagefile"];
223  if (!file_exists($imagefilename)) {
224  $data["imagefile"] = null;
225  }
226  $data["answertext"] = ilRTE::_replaceMediaObjectImageSrc($data["answertext"] ?? '', 1);
227 
228  $answer = new ASS_AnswerMultipleResponseImage(
229  $data["answertext"],
230  $data["points"],
231  $data["aorder"],
232  $data["answer_id"]
233  );
234  $answer->setPointsUnchecked($data["points_unchecked"]);
235  $answer->setImage($data["imagefile"] ? $data["imagefile"] : null);
236  array_push($this->answers, $answer);
237  }
238  }
239 
240  parent::loadFromDb($question_id);
241  }
242 
246  public function duplicate(bool $for_test = true, string $title = "", string $author = "", int $owner = -1, $testObjId = null): int
247  {
248  if ($this->id <= 0) {
249  // The question has not been saved. It cannot be duplicated
250  return -1;
251  }
252  // duplicate the question in database
253  $this_id = $this->getId();
254  $thisObjId = $this->getObjId();
255 
256  $clone = $this;
257 
258  $original_id = $this->questioninfo->getOriginalId($this->id);
259  $clone->id = -1;
260 
261  if ((int) $testObjId > 0) {
262  $clone->setObjId($testObjId);
263  }
264 
265  if ($title) {
266  $clone->setTitle($title);
267  }
268 
269  if ($author) {
270  $clone->setAuthor($author);
271  }
272  if ($owner) {
273  $clone->setOwner($owner);
274  }
275 
276  if ($for_test) {
277  $clone->saveToDb($original_id);
278  } else {
279  $clone->saveToDb();
280  }
281 
282  // copy question page content
283  $clone->copyPageOfQuestion($this_id);
284  // copy XHTML media objects
285  $clone->copyXHTMLMediaObjectsOfQuestion($this_id);
286  // duplicate the images
287  $clone->duplicateImages($this_id, $thisObjId);
288 
289  $clone->onDuplicate($thisObjId, $this_id, $clone->getObjId(), $clone->getId());
290 
291  return $clone->id;
292  }
293 
297  public function copyObject($target_questionpool_id, $title = ""): int
298  {
299  if ($this->getId() <= 0) {
300  throw new RuntimeException('The question has not been saved. It cannot be duplicated');
301  }
302  // duplicate the question in database
303  $clone = $this;
304 
305  $original_id = $this->questioninfo->getOriginalId($this->id);
306  $clone->id = -1;
307  $source_questionpool_id = $this->getObjId();
308  $clone->setObjId($target_questionpool_id);
309  if ($title) {
310  $clone->setTitle($title);
311  }
312  $clone->saveToDb();
313  // copy question page content
314  $clone->copyPageOfQuestion($original_id);
315  // copy XHTML media objects
316  $clone->copyXHTMLMediaObjectsOfQuestion($original_id);
317  // duplicate the image
318  $clone->copyImages($original_id, $source_questionpool_id);
319 
320  $clone->onCopy($source_questionpool_id, $original_id, $clone->getObjId(), $clone->getId());
321 
322  return $clone->id;
323  }
324 
325  public function createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle = ""): int
326  {
327  if ($this->getId() <= 0) {
328  throw new RuntimeException('The question has not been saved. It cannot be duplicated');
329  }
330 
331  $sourceQuestionId = $this->id;
332  $sourceParentId = $this->getObjId();
333 
334  // duplicate the question in database
335  $clone = $this;
336  $clone->id = -1;
337 
338  $clone->setObjId($targetParentId);
339 
340  if ($targetQuestionTitle) {
341  $clone->setTitle($targetQuestionTitle);
342  }
343 
344  $clone->saveToDb();
345  // copy question page content
346  $clone->copyPageOfQuestion($sourceQuestionId);
347  // copy XHTML media objects
348  $clone->copyXHTMLMediaObjectsOfQuestion($sourceQuestionId);
349  // duplicate the image
350  $clone->copyImages($sourceQuestionId, $sourceParentId);
351 
352  $clone->onCopy($sourceParentId, $sourceQuestionId, $clone->getObjId(), $clone->getId());
353 
354  return $clone->id;
355  }
356 
371  public function addAnswer(
372  $answertext = "",
373  $points = 0.0,
374  $points_unchecked = 0.0,
375  $order = 0,
376  $answerimage = null,
377  $answer_id = -1
378  ): void {
379  $answertext = $this->getHtmlQuestionContentPurifier()->purify($answertext);
380  if (array_key_exists($order, $this->answers)) {
381  // insert answer
382  $answer = new ASS_AnswerMultipleResponseImage($answertext, $points, $order, -1, 0);
383  $answer->setPointsUnchecked($points_unchecked);
384  $answer->setImage($answerimage);
385  $newchoices = [];
386  for ($i = 0; $i < $order; $i++) {
387  $newchoices[] = $this->answers[$i];
388  }
389  $newchoices[] = $answer;
390  for ($i = $order, $iMax = count($this->answers); $i < $iMax; $i++) {
391  $changed = $this->answers[$i];
392  $changed->setOrder($i + 1);
393  $newchoices[] = $changed;
394  }
395  $this->answers = $newchoices;
396  } else {
397  $answer = new ASS_AnswerMultipleResponseImage($answertext, $points, count($this->answers), (int) $answer_id, 0);
398  $answer->setPointsUnchecked($points_unchecked);
399  $answer->setImage($answerimage);
400  $this->answers[] = $answer;
401  }
402  }
403 
410  public function getAnswerCount(): int
411  {
412  return count($this->answers);
413  }
414 
423  public function getAnswer($index = 0): ?object
424  {
425  if ($index < 0) {
426  return null;
427  }
428  if (count($this->answers) < 1) {
429  return null;
430  }
431  if ($index >= count($this->answers)) {
432  return null;
433  }
434 
435  return $this->answers[$index];
436  }
437 
445  public function deleteAnswer($index = 0): void
446  {
447  if ($index < 0) {
448  return;
449  }
450  if (count($this->answers) < 1) {
451  return;
452  }
453  if ($index >= count($this->answers)) {
454  return;
455  }
456  $answer = $this->answers[$index];
457  if ($answer->hasImage()) {
458  $this->deleteImage($answer->getImage());
459  }
460  unset($this->answers[$index]);
461  $this->answers = array_values($this->answers);
462  for ($i = 0, $iMax = count($this->answers); $i < $iMax; $i++) {
463  if ($this->answers[$i]->getOrder() > $index) {
464  $this->answers[$i]->setOrder($i);
465  }
466  }
467  }
468 
474  public function flushAnswers(): void
475  {
476  $this->answers = [];
477  }
478 
484  public function getMaximumPoints(): float
485  {
486  $total_max_points = 0.0;
487  foreach ($this->getAnswers() as $answer) {
488  $total_max_points += max($answer->getPointsChecked(), $answer->getPointsUnchecked());
489  }
490  return $total_max_points;
491  }
492 
504  public function calculateReachedPoints($active_id, $pass = null, $authorizedSolution = true, $returndetails = false): float
505  {
506  if ($returndetails) {
507  throw new ilTestException('return details not implemented for ' . __METHOD__);
508  }
509 
510  global $DIC;
511  $ilDB = $DIC['ilDB'];
512 
513  $found_values = [];
514  if (is_null($pass)) {
515  $pass = $this->getSolutionMaxPass($active_id);
516  }
517  $result = $this->getCurrentSolutionResultSet($active_id, $pass, $authorizedSolution);
518  while ($data = $ilDB->fetchAssoc($result)) {
519  if (strcmp($data["value1"], "") != 0) {
520  array_push($found_values, $data["value1"]);
521  }
522  }
523 
524  $points = $this->calculateReachedPointsForSolution($found_values, $active_id);
525 
526  return $points;
527  }
528 
529  public function validateSolutionSubmit(): bool
530  {
531  $submit = $this->getSolutionSubmit();
532 
533  if ($this->getSelectionLimit()) {
534  if (count($submit) > $this->getSelectionLimit()) {
535  $failureMsg = sprintf(
536  $this->lng->txt('ass_mc_sel_lim_exhausted_hint'),
537  $this->getSelectionLimit(),
538  $this->getAnswerCount()
539  );
540 
541  $this->tpl->setOnScreenMessage('failure', $failureMsg, true);
542  return false;
543  }
544  }
545 
546  return true;
547  }
548 
549  protected function isForcedEmptySolution($solutionSubmit): bool
550  {
551  if (!count($solutionSubmit) && !empty($_POST['tst_force_form_diff_input'])) {
552  return true;
553  }
554 
555  return false;
556  }
557 
566  public function saveWorkingData($active_id, $pass = null, $authorized = true): bool
567  {
569  global $DIC;
570  $ilDB = $DIC['ilDB'];
571 
572  if (is_null($pass)) {
573  $pass = ilObjTest::_getPass($active_id);
574  }
575 
576  $entered_values = 0;
577 
578  $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(function () use (&$entered_values, $active_id, $pass, $authorized) {
579  $this->removeCurrentSolution($active_id, $pass, $authorized);
580 
581  $solutionSubmit = $this->getSolutionSubmit();
582 
583  foreach ($solutionSubmit as $value) {
584  if (strlen($value)) {
585  $this->saveCurrentSolution($active_id, $pass, $value, null, $authorized);
586  $entered_values++;
587  }
588  }
589 
590  // fau: testNav - write a dummy entry for the evil mc questions with "None of the above" checked
591  if ($this->isForcedEmptySolution($solutionSubmit)) {
592  $this->saveCurrentSolution($active_id, $pass, 'mc_none_above', null, $authorized);
593  $entered_values++;
594  }
595  // fau.
596  });
597 
598  if ($entered_values) {
600  assQuestion::logAction($this->lng->txtlng(
601  "assessment",
602  "log_user_entered_values",
604  ), $active_id, $this->getId());
605  }
606  } else {
608  assQuestion::logAction($this->lng->txtlng(
609  "assessment",
610  "log_user_not_entered_values",
612  ), $active_id, $this->getId());
613  }
614  }
615 
616  return true;
617  }
618 
619  public function saveAdditionalQuestionDataToDb()
620  {
622  global $DIC;
623  $ilDB = $DIC['ilDB'];
624  $oldthumbsize = 0;
625  if ($this->isSingleline && ($this->getThumbSize())) {
626  // get old thumbnail size
627  $result = $ilDB->queryF(
628  "SELECT thumb_size FROM " . $this->getAdditionalTableName() . " WHERE question_fi = %s",
629  ['integer'],
630  [$this->getId()]
631  );
632  if ($result->numRows() == 1) {
633  $data = $ilDB->fetchAssoc($result);
634  $oldthumbsize = $data['thumb_size'];
635  }
636  }
637 
638  if (!$this->isSingleline) {
640  }
641 
642  // save additional data
643  $ilDB->replace(
644  $this->getAdditionalTableName(),
645  [
646  'shuffle' => ['text', $this->getShuffle()],
647  'allow_images' => ['text', $this->isSingleline ? 0 : 1],
648  'thumb_size' => ['integer', strlen($this->getThumbSize()) ? $this->getThumbSize() : null],
649  'selection_limit' => ['integer', $this->getSelectionLimit()],
650  'feedback_setting' => ['integer', $this->getSpecificFeedbackSetting()]
651  ],
652  ['question_fi' => ['integer', $this->getId()]]
653  );
654  }
655 
661  public function saveAnswerSpecificDataToDb()
662  {
664  global $DIC;
665  $ilDB = $DIC['ilDB'];
666 
667  // Get all feedback entries
668  $result = $ilDB->queryF(
669  "SELECT * FROM qpl_fb_specific WHERE question_fi = %s",
670  ['integer'],
671  [$this->getId()]
672  );
673  $db_feedback = $ilDB->fetchAll($result);
674 
675  // Check if feedback exists and the regular editor is used and not the page editor
676  if (sizeof($db_feedback) >= 1 && $this->getAdditionalContentEditingMode() == 'default') {
677  // Get all existing answer data for question
678  $result = $ilDB->queryF(
679  "SELECT answer_id, aorder FROM qpl_a_mc WHERE question_fi = %s",
680  ['integer'],
681  [$this->getId()]
682  );
683  $db_answers = $ilDB->fetchAll($result);
684 
685  // Collect old and new order entries by ids and order to calculate a diff/intersection and remove/update feedback
686  $post_answer_order_for_id = [];
687  foreach ($this->answers as $answer) {
688  // Only the first appearance of an id is used
689  if ($answer->getId() !== null && !in_array($answer->getId(), array_keys($post_answer_order_for_id))) {
690  // -1 is happening while import and also if a new multi line answer is generated
691  if ($answer->getId() == -1) {
692  continue;
693  }
694  $post_answer_order_for_id[$answer->getId()] = $answer->getOrder();
695  }
696  }
697 
698  // If there is no usable ids from post, it's better to not touch the feedback
699  // This is useful since the import is also using this function or the first creation of a new question in general
700  if (sizeof($post_answer_order_for_id) >= 1) {
701  $db_answer_order_for_id = [];
702  $db_answer_id_for_order = [];
703  foreach ($db_answers as $db_answer) {
704  $db_answer_order_for_id[intval($db_answer['answer_id'])] = intval($db_answer['aorder']);
705  $db_answer_id_for_order[intval($db_answer['aorder'])] = intval($db_answer['answer_id']);
706  }
707 
708  // Handle feedback
709  // the diff between the already existing answer ids from the Database and the answer ids from post
710  // feedback related to the answer ids should be deleted or in our case not recreated.
711  $db_answer_ids = array_keys($db_answer_order_for_id);
712  $post_answer_ids = array_keys($post_answer_order_for_id);
713  $diff_db_post_answer_ids = array_diff($db_answer_ids, $post_answer_ids);
714  $unused_answer_ids = array_keys($diff_db_post_answer_ids);
715 
716  // Delete all feedback in the database
717  $this->feedbackOBJ->deleteSpecificAnswerFeedbacks($this->getId(), false);
718  // Recreate feedback
719  foreach ($db_feedback as $feedback_option) {
720  // skip feedback which answer is deleted
721  if (in_array(intval($feedback_option['answer']), $unused_answer_ids)) {
722  continue;
723  }
724 
725  // Reorder feedback
726  $feedback_order_db = intval($feedback_option['answer']);
727  $db_answer_id = $db_answer_id_for_order[$feedback_order_db] ?? null;
728  // This cuts feedback that currently would have no corresponding answer
729  // This case can happen while copying "broken" questions
730  // Or when saving a question with less answers than feedback
731  if (is_null($db_answer_id) || $db_answer_id < 0) {
732  continue;
733  }
734  $feedback_order_post = $post_answer_order_for_id[$db_answer_id];
735  $feedback_option['answer'] = $feedback_order_post;
736 
737  // Recreate remaining feedback in database
738  $next_id = $ilDB->nextId('qpl_fb_specific');
739  $ilDB->manipulateF(
740  "INSERT INTO qpl_fb_specific (feedback_id, question_fi, answer, tstamp, feedback, question)
741  VALUES (%s, %s, %s, %s, %s, %s)",
742  ['integer', 'integer', 'integer', 'integer', 'text', 'integer'],
743  [
744  $next_id,
745  $feedback_option['question_fi'],
746  $feedback_option['answer'],
747  time(),
748  $feedback_option['feedback'],
749  $feedback_option['question']
750  ]
751  );
752  }
753  }
754  }
755 
756  // Delete all entries in qpl_a_mc for question
757  $ilDB->manipulateF(
758  "DELETE FROM qpl_a_mc WHERE question_fi = %s",
759  ['integer'],
760  [$this->getId()]
761  );
762 
763  // Recreate answers one by one
764  foreach ($this->answers as $key => $value) {
765  $answer_obj = $this->answers[$key];
766  $next_id = $ilDB->nextId('qpl_a_mc');
767  $ilDB->manipulateF(
768  "INSERT INTO qpl_a_mc (answer_id, question_fi, answertext, points, points_unchecked, aorder, imagefile, tstamp)
769  VALUES (%s, %s, %s, %s, %s, %s, %s, %s)",
770  ['integer', 'integer', 'text', 'float', 'float', 'integer', 'text', 'integer'],
771  [
772  $next_id,
773  $this->getId(),
774  ilRTE::_replaceMediaObjectImageSrc($answer_obj->getAnswertext(), 0),
775  $answer_obj->getPoints(),
776  $answer_obj->getPointsUnchecked(),
777  $answer_obj->getOrder(),
778  $answer_obj->getImage(),
779  time()
780  ]
781  );
782  }
783  }
784 
785  public function syncWithOriginal(): void
786  {
787  if ($this->questioninfo->getOriginalId($this->getId())) {
788  $this->syncImages();
789  parent::syncWithOriginal();
790  }
791  }
792 
798  public function getQuestionType(): string
799  {
800  return "assMultipleChoice";
801  }
802 
808  public function getAdditionalTableName(): string
809  {
810  return "qpl_qst_mc";
811  }
812 
818  public function getAnswerTableName(): string
819  {
820  return "qpl_a_mc";
821  }
822 
830  public function setImageFile($image_filename, $image_tempfilename = ""): int
831  {
832  $result = 0;
833  if (!empty($image_tempfilename)) {
834  $image_filename = str_replace(" ", "_", $image_filename);
835  $imagepath = $this->getImagePath();
836  if (!file_exists($imagepath)) {
837  ilFileUtils::makeDirParents($imagepath);
838  }
839  if (!ilFileUtils::moveUploadedFile($image_tempfilename, $image_filename, $imagepath . $image_filename)) {
840  $result = 2;
841  } else {
842  $mimetype = ilObjMediaObject::getMimeType($imagepath . $image_filename);
843  if (!preg_match("/^image/", $mimetype)) {
844  unlink($imagepath . $image_filename);
845  $result = 1;
846  } else {
847  // create thumbnail file
848  if ($this->isSingleline && ($this->getThumbSize())) {
849  $this->generateThumbForFile(
850  $image_filename,
851  $this->getImagePath(),
852  $this->getThumbSize()
853  );
854  }
855  }
856  }
857  }
858  return $result;
859  }
860 
866  protected function deleteImage($image_filename): void
867  {
868  $imagepath = $this->getImagePath();
869  @unlink($imagepath . $image_filename);
870  $thumbpath = $imagepath . $this->getThumbPrefix() . $image_filename;
871  @unlink($thumbpath);
872  }
873 
874  public function duplicateImages($question_id, $objectId = null): void
875  {
877  global $DIC;
878  $ilLog = $DIC['ilLog'];
879 
880  $imagepath = $this->getImagePath();
881  $imagepath_original = str_replace("/$this->id/images", "/$question_id/images", $imagepath);
882 
883  if ((int) $objectId > 0) {
884  $imagepath_original = str_replace("/$this->obj_id/", "/$objectId/", $imagepath_original);
885  }
886 
887  foreach ($this->answers as $answer) {
888  if ($answer->hasImage()) {
889  $filename = $answer->getImage();
890  if (!file_exists($imagepath)) {
891  ilFileUtils::makeDirParents($imagepath);
892  }
893 
894  if (file_exists($imagepath_original . $filename)) {
895  if (!copy($imagepath_original . $filename, $imagepath . $filename)) {
896  $ilLog->warning(sprintf(
897  "Could not clone source image '%s' to '%s' (srcQuestionId: %s|tgtQuestionId: %s|srcParentObjId: %s|tgtParentObjId: %s)",
898  $imagepath_original . $filename,
899  $imagepath . $filename,
900  $question_id,
901  $this->id,
902  $objectId,
903  $this->obj_id
904  ));
905  }
906  }
907 
908  if (file_exists($imagepath_original . $this->getThumbPrefix() . $filename)) {
909  if (!copy($imagepath_original . $this->getThumbPrefix() . $filename, $imagepath . $this->getThumbPrefix() . $filename)) {
910  $ilLog->warning(sprintf(
911  "Could not clone thumbnail source image '%s' to '%s' (srcQuestionId: %s|tgtQuestionId: %s|srcParentObjId: %s|tgtParentObjId: %s)",
912  $imagepath_original . $this->getThumbPrefix() . $filename,
913  $imagepath . $this->getThumbPrefix() . $filename,
914  $question_id,
915  $this->id,
916  $objectId,
917  $this->obj_id
918  ));
919  }
920  }
921  }
922  }
923  }
924 
925  public function copyImages($question_id, $source_questionpool): void
926  {
927  global $DIC;
928  $ilLog = $DIC['ilLog'];
929  $imagepath = $this->getImagePath();
930  $imagepath_original = str_replace("/$this->id/images", "/$question_id/images", $imagepath);
931  $imagepath_original = str_replace("/$this->obj_id/", "/$source_questionpool/", $imagepath_original);
932  foreach ($this->answers as $answer) {
933  if ($answer->hasImage()) {
934  $filename = $answer->getImage();
935  if (!file_exists($imagepath)) {
936  ilFileUtils::makeDirParents($imagepath);
937  }
938  if (!@copy($imagepath_original . $filename, $imagepath . $filename)) {
939  $ilLog->write("image could not be duplicated!!!!", $ilLog->ERROR);
940  $ilLog->write("object: " . print_r($this, true), $ilLog->ERROR);
941  }
942  if (@file_exists($imagepath_original . $this->getThumbPrefix() . $filename)) {
943  if (!@copy($imagepath_original . $this->getThumbPrefix() . $filename, $imagepath . $this->getThumbPrefix() . $filename)) {
944  $ilLog->write("image thumbnail could not be duplicated!!!!", $ilLog->ERROR);
945  $ilLog->write("object: " . print_r($this, true), $ilLog->ERROR);
946  }
947  }
948  }
949  }
950  }
951 
955  protected function syncImages(): void
956  {
957  global $DIC;
958  $ilLog = $DIC['ilLog'];
959  $imagepath = $this->getImagePath();
960  $question_id = $this->questioninfo->getOriginalId($this->getId());
961  $originalObjId = parent::lookupParentObjId($this->questioninfo->getOriginalId($this->getId()));
962  $imagepath_original = $this->getImagePath($question_id, $originalObjId);
963 
964  ilFileUtils::delDir($imagepath_original);
965  foreach ($this->answers as $answer) {
966  if ($answer->hasImage()) {
967  $filename = $answer->getImage();
968  if (@file_exists($imagepath . $filename)) {
969  if (!file_exists($imagepath)) {
970  ilFileUtils::makeDirParents($imagepath);
971  }
972  if (!file_exists($imagepath_original)) {
973  ilFileUtils::makeDirParents($imagepath_original);
974  }
975  if (!@copy($imagepath . $filename, $imagepath_original . $filename)) {
976  $ilLog->write("image could not be duplicated!!!!", $ilLog->ERROR);
977  $ilLog->write("object: " . print_r($this, true), $ilLog->ERROR);
978  }
979  }
980  if (@file_exists($imagepath . $this->getThumbPrefix() . $filename)) {
981  if (!@copy($imagepath . $this->getThumbPrefix() . $filename, $imagepath_original . $this->getThumbPrefix() . $filename)) {
982  $ilLog->write("image thumbnail could not be duplicated!!!!", $ilLog->ERROR);
983  $ilLog->write("object: " . print_r($this, true), $ilLog->ERROR);
984  }
985  }
986  }
987  }
988  }
989 
993  public function getRTETextWithMediaObjects(): string
994  {
995  $text = parent::getRTETextWithMediaObjects();
996  foreach ($this->answers as $index => $answer) {
997  $text .= $this->feedbackOBJ->getSpecificAnswerFeedbackContent($this->getId(), 0, $index);
998  $answer_obj = $this->answers[$index];
999  $text .= $answer_obj->getAnswertext();
1000  }
1001  return $text;
1002  }
1003 
1007  public function &getAnswers(): array
1008  {
1009  return $this->answers;
1010  }
1011 
1012  public function setAnswers(array $answers): void
1013  {
1014  $this->answers = $answers;
1015  }
1016 
1020  public function setExportDetailsXLSX(ilAssExcelFormatHelper $worksheet, int $startrow, int $col, int $active_id, int $pass): int
1021  {
1022  parent::setExportDetailsXLSX($worksheet, $startrow, $col, $active_id, $pass);
1023 
1024  $solution = $this->getSolutionValues($active_id, $pass);
1025 
1026  $i = 1;
1027  foreach ($this->getAnswers() as $id => $answer) {
1028  $worksheet->setCell($startrow + $i, $col, $answer->getAnswertext());
1029  $worksheet->setBold($worksheet->getColumnCoord($col) . ($startrow + $i));
1030  $checked = false;
1031  foreach ($solution as $solutionvalue) {
1032  if ($id == $solutionvalue["value1"]) {
1033  $checked = true;
1034  }
1035  }
1036  if ($checked) {
1037  $worksheet->setCell($startrow + $i, $col + 2, 1);
1038  } else {
1039  $worksheet->setCell($startrow + $i, $col + 2, 0);
1040  }
1041  $i++;
1042  }
1043 
1044  return $startrow + $i + 1;
1045  }
1046 
1051  {
1052  foreach ($this->getAnswers() as $answer) {
1053  /* @var ASS_AnswerBinaryStateImage $answer */
1054  $answer->setAnswertext($migrator->migrateToLmContent($answer->getAnswertext()));
1055  }
1056  }
1057 
1061  public function toJSON(): string
1062  {
1063  $result = [];
1064  $result['id'] = $this->getId();
1065  $result['type'] = (string) $this->getQuestionType();
1066  $result['title'] = $this->getTitleForHTMLOutput();
1067  $result['question'] = $this->formatSAQuestion($this->getQuestion());
1068  $result['nr_of_tries'] = $this->getNrOfTries();
1069  $result['shuffle'] = $this->getShuffle();
1070  $result['selection_limit'] = (int) $this->getSelectionLimit();
1071  $result['feedback'] = [
1072  'onenotcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)),
1073  'allcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true))
1074  ];
1075 
1076  $answers = [];
1077  $has_image = false;
1078  foreach ($this->getAnswers() as $key => $answer_obj) {
1079  if ((string) $answer_obj->getImage()) {
1080  $has_image = true;
1081  }
1082  array_push($answers, [
1083  "answertext" => $this->formatSAQuestion($answer_obj->getAnswertext()),
1084  "points_checked" => (float) $answer_obj->getPointsChecked(),
1085  "points_unchecked" => (float) $answer_obj->getPointsUnchecked(),
1086  "order" => (int) $answer_obj->getOrder(),
1087  "image" => (string) $answer_obj->getImage(),
1088  "feedback" => $this->formatSAQuestion(
1089  $this->feedbackOBJ->getSpecificAnswerFeedbackExportPresentation($this->getId(), 0, $key)
1090  )
1091  ]);
1092  }
1093  $result['answers'] = $answers;
1094 
1095  if ($has_image) {
1096  $result['path'] = $this->getImagePathWeb();
1097  $result['thumb'] = $this->getThumbSize();
1098  }
1099 
1100  $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId());
1101  $result['mobs'] = $mobs;
1102 
1103  return json_encode($result);
1104  }
1105 
1106  public function removeAnswerImage($index): void
1107  {
1108  $answer = $this->answers[$index];
1109  if (is_object($answer)) {
1110  $this->deleteImage($answer->getImage());
1111  $answer->setImage(null);
1112  }
1113  }
1114 
1115  public function getMultilineAnswerSetting(): int
1116  {
1117  global $DIC;
1118  $ilUser = $DIC['ilUser'];
1119 
1120  $multilineAnswerSetting = $ilUser->getPref("tst_multiline_answers");
1121  if ($multilineAnswerSetting != 1) {
1122  $multilineAnswerSetting = 0;
1123  }
1124  return $multilineAnswerSetting;
1125  }
1126 
1127  public function setMultilineAnswerSetting($a_setting = 0): void
1128  {
1129  global $DIC;
1130  $ilUser = $DIC['ilUser'];
1131  $ilUser->writePref("tst_multiline_answers", $a_setting);
1132  }
1133 
1143  public function setSpecificFeedbackSetting($a_feedback_setting): void
1144  {
1145  $this->feedback_setting = $a_feedback_setting;
1146  }
1147 
1157  public function getSpecificFeedbackSetting(): int
1158  {
1159  if ($this->feedback_setting) {
1160  return $this->feedback_setting;
1161  } else {
1162  return 1;
1163  }
1164  }
1165 
1167  {
1168  return 'feedback_correct_sc_mc';
1169  }
1170 
1182  public static function isObligationPossible(int $questionId): bool
1183  {
1185  global $DIC;
1186  $ilDB = $DIC['ilDB'];
1187 
1188  $query = "
1189  SELECT SUM(points) points_for_checked_answers
1190  FROM qpl_a_mc
1191  WHERE question_fi = %s AND points > 0
1192  ";
1193 
1194  $res = $ilDB->queryF($query, ['integer'], [$questionId]);
1195 
1196  $row = $ilDB->fetchAssoc($res);
1197 
1198  return $row['points_for_checked_answers'] > 0;
1199  }
1200 
1209  public function ensureNoInvalidObligation($questionId): void
1210  {
1212  global $DIC;
1213  $ilDB = $DIC['ilDB'];
1214 
1215  $query = "
1216  SELECT SUM(qpl_a_mc.points) points_for_checked_answers,
1217  test_question_id
1218 
1219  FROM tst_test_question
1220 
1221  INNER JOIN qpl_a_mc
1222  ON qpl_a_mc.question_fi = tst_test_question.question_fi
1223 
1224  WHERE tst_test_question.question_fi = %s
1225  AND tst_test_question.obligatory = 1
1226 
1227  GROUP BY test_question_id
1228  ";
1229 
1230  $res = $ilDB->queryF($query, ['integer'], [$questionId]);
1231 
1232  $updateTestQuestionIds = [];
1233 
1234  while ($row = $ilDB->fetchAssoc($res)) {
1235  if ($row['points_for_checked_answers'] <= 0) {
1236  $updateTestQuestionIds[] = $row['test_question_id'];
1237  }
1238  }
1239 
1240  if (count($updateTestQuestionIds)) {
1241  $test_question_id__IN__updateTestQuestionIds = $ilDB->in(
1242  'test_question_id',
1243  $updateTestQuestionIds,
1244  false,
1245  'integer'
1246  );
1247 
1248  $query = "
1249  UPDATE tst_test_question
1250  SET obligatory = 0
1251  WHERE $test_question_id__IN__updateTestQuestionIds
1252  ";
1253 
1254  $ilDB->manipulate($query);
1255  }
1256  }
1257 
1258  protected function getSolutionSubmit(): array
1259  {
1260  $solutionSubmit = [];
1261  $post = $this->dic->http()->wrapper()->post();
1262 
1263  foreach ($this->getAnswers() as $index => $a) {
1264  if ($post->has("multiple_choice_result_$index")) {
1265  $value = $post->retrieve("multiple_choice_result_$index", $this->dic->refinery()->kindlyTo()->string());
1266  if (is_numeric($value)) {
1267  $solutionSubmit[] = $value;
1268  }
1269  }
1270  }
1271  return $solutionSubmit;
1272  }
1273 
1279  protected function calculateReachedPointsForSolution($found_values, $active_id = 0): float
1280  {
1281  if ($found_values == null) {
1282  $found_values = [];
1283  }
1284  $points = 0;
1285  foreach ($this->answers as $key => $answer) {
1286  if (in_array($key, $found_values)) {
1287  $points += $answer->getPoints();
1288  } else {
1289  $points += $answer->getPointsUnchecked();
1290  }
1291  }
1292  if ($active_id) {
1293  if (count($found_values) == 0) {
1294  $points = 0;
1295  }
1296  }
1297  return $points;
1298  }
1299 
1308  public function getOperators($expression): array
1309  {
1311  }
1312 
1317  public function getExpressionTypes(): array
1318  {
1319  return [
1324  ];
1325  }
1326 
1335  public function getUserQuestionResult($active_id, $pass): ilUserQuestionResult
1336  {
1338  global $DIC;
1339  $ilDB = $DIC['ilDB'];
1340  $result = new ilUserQuestionResult($this, $active_id, $pass);
1341 
1342  $maxStep = $this->lookupMaxStep($active_id, $pass);
1343 
1344  if ($maxStep > 0) {
1345  $data = $ilDB->queryF(
1346  "SELECT value1+1 as value1 FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s AND step = %s",
1347  ["integer", "integer", "integer","integer"],
1348  [$active_id, $pass, $this->getId(), $maxStep]
1349  );
1350  } else {
1351  $data = $ilDB->queryF(
1352  "SELECT value1+1 as value1 FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s",
1353  ["integer", "integer", "integer"],
1354  [$active_id, $pass, $this->getId()]
1355  );
1356  }
1357 
1358  while ($row = $ilDB->fetchAssoc($data)) {
1359  $result->addKeyValue($row["value1"], $row["value1"]);
1360  }
1361 
1362  $points = $this->calculateReachedPoints($active_id, $pass);
1363  $max_points = $this->getMaximumPoints();
1364 
1365  $result->setReachedPercentage(($points / $max_points) * 100);
1366 
1367  return $result;
1368  }
1369 
1376  public function getAvailableAnswerOptions($index = null)
1377  {
1378  if ($index !== null) {
1379  return $this->getAnswer($index);
1380  } else {
1381  return $this->getAnswers();
1382  }
1383  }
1384 
1386  {
1387  $config = parent::buildTestPresentationConfig();
1388  $config->setUseUnchangedAnswerLabel($this->lng->txt('tst_mc_label_none_above'));
1389  return $config;
1390  }
1391 
1392  public function isSingleline(): bool
1393  {
1394  return (bool) $this->isSingleline;
1395  }
1396 }
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...
flushAnswers()
Deletes all answers.
getSolutionValues($active_id, $pass=null, bool $authorized=true)
Loads solutions of a given user from the database an returns it.
calculateReachedPointsForSolution($found_values, $active_id=0)
setNrOfTries(int $a_nr_of_tries)
duplicate(bool $for_test=true, string $title="", string $author="", int $owner=-1, $testObjId=null)
Duplicates an assMultipleChoiceQuestion.
$res
Definition: ltiservices.php:69
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
getRTETextWithMediaObjects()
Collects all text in the question which could contain media objects which were created with the Rich ...
toJSON()
Returns a JSON representation of the question.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static _getPass($active_id)
Retrieves the actual pass of a given user for a given test.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
isComplete()
Returns true, if a multiple choice question is complete for use.
copyObject($target_questionpool_id, $title="")
Copies an assMultipleChoice object.
createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle="")
saveAdditionalQuestionDataToDb()
Saves a record to the question types additional data table.
calculateReachedPoints($active_id, $pass=null, $authorizedSolution=true, $returndetails=false)
Returns the points, a learner has reached answering the question.
Abstract basic class which is to be extended by the concrete assessment question type classes...
static isObligationPossible(int $questionId)
setOwner(int $owner=-1)
saveWorkingData(int $active_id, int $pass, bool $authorized=true)
Saves the learners input of the question to the database.
getColumnCoord(int $a_col)
Get column "name" from number.
bool $shuffle
Indicates whether the answers will be shuffled or not.
ASS_AnswerBinaryStateImage is a class for answers with a binary state indicator (checked/unchecked, set/unset) and an image file.
setSelectionLimit($selectionLimit)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
getImagePathWeb()
Returns the web image path for web accessable images of a question.
setThumbSize(int $a_size)
getQuestionType()
Returns the question type of the question.
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)
getUserQuestionResult($active_id, $pass)
Get the user solution for a question by active_id and the test pass.
loadFromDb($question_id)
Loads a assMultipleChoice object from a database.
static makeDirParents(string $a_dir)
Create a new directory and all parent directories.
setComment(string $comment="")
Class for multiple choice tests.
float $points
The maximum available points for the question.
lmMigrateQuestionTypeSpecificContent(ilAssSelfAssessmentMigrator $migrator)
Base Exception for all Exceptions relating to Modules/Test.
saveToDb($original_id="")
Saves a assMultipleChoice object to a database.
deleteAnswer($index=0)
Deletes an answer with a given index.
global $DIC
Definition: feed.php:28
addAnswer( $answertext="", $points=0.0, $points_unchecked=0.0, $order=0, $answerimage=null, $answer_id=-1)
Adds a possible answer for a multiple choice question.
saveCurrentSolution(int $active_id, int $pass, $value1, $value2, bool $authorized=true, $tstamp=0)
setBold(string $a_coords)
Set cell(s) to bold.
__construct(VocabulariesInterface $vocabularies)
getImagePath($question_id=null, $object_id=null)
Returns the image path for web accessable images of a question.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static getMimeType(string $a_file, bool $a_external=false)
get mime type for file
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
getAnswerCount()
Returns the number of answers.
static logAction(string $logtext, int $active_id, int $question_id)
static delDir(string $a_dir, bool $a_clean_only=false)
removes a dir and all its content (subdirs and files) recursively
getSpecificFeedbackSetting()
Gets the current feedback settings in effect for the question.
isForcedEmptySolution($solutionSubmit)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
string $key
Consumer key/client ID value.
Definition: System.php:193
setExportDetailsXLSX(ilAssExcelFormatHelper $worksheet, int $startrow, int $col, 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 ...
__construct( $title="", $comment="", $author="", $owner=-1, $question="", $output_type=OUTPUT_ORDER)
assMultipleChoice constructor
syncImages()
Sync images of a MC question on synchronisation with the original question.
getOperators($expression)
Get all available operations for a specific question.
setIsSingleline($isSingleline)
static moveUploadedFile(string $a_file, string $a_name, string $a_target, bool $a_raise_errors=true, string $a_mode="move_uploaded")
move uploaded file
getAnswer($index=0)
Returns an answer with a given index.
setPoints(float $points)
setObjId(int $obj_id=0)
& getAnswers()
Returns a reference to the answers array.
string $question
The question text.
setSpecificFeedbackSetting($a_feedback_setting)
Sets the feedback settings in effect for the question.
static _getMobsOfObject(string $a_type, int $a_id, int $a_usage_hist_nr=0, string $a_lang="-")
deleteImage($image_filename)
Deletes an image file.
getExpressionTypes()
Get all available expression types for a specific question.
$filename
Definition: buildRTE.php:78
saveAnswerSpecificDataToDb()
Saves the answer specific records into a question types answer table.
getMaximumPoints()
Returns the maximum points, a learner can reach answering the question.
saveQuestionDataToDb(int $original_id=-1)
setImageFile($image_filename, $image_tempfilename="")
Sets the image file and uploads the image to the object&#39;s image directory.
getSolutionMaxPass(int $active_id)
removeCurrentSolution(int $active_id, int $pass, bool $authorized=true)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setId(int $id=-1)
getAdditionalTableName()
Returns the name of the additional question data table in the database.
setOriginalId(?int $original_id)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setTitle(string $title="")
$a
thx to https://mlocati.github.io/php-cs-fixer-configurator for the examples
setLifecycle(ilAssQuestionLifecycle $lifecycle)
getAnswerTableName()
Returns the name of the answer table in the database.
getCurrentSolutionResultSet(int $active_id, int $pass, bool $authorized=true)
const OUTPUT_ORDER
ILIAS DI LoggingServices $ilLog
lookupMaxStep(int $active_id, int $pass)
setAuthor(string $author="")
$post
Definition: ltitoken.php:49
setShuffle(?bool $shuffle=true)
setAdditionalContentEditingMode(?string $additionalContentEditingMode)
setMultilineAnswerSetting($a_setting=0)
copyImages($question_id, $source_questionpool)
setQuestion(string $question="")