ILIAS  release_9 Revision v9.13-25-g2c18ec4c24f
class.assSingleChoice.php
Go to the documentation of this file.
1 <?php
2 
19 require_once './Modules/Test/classes/inc.AssessmentConstants.php';
20 
22 
37 {
39 
40  private bool $isSingleline = true;
41 
49  public $answers;
50 
59  public $output_type;
60 
67  protected $feedback_setting;
68 
83  public function __construct(
84  $title = "",
85  $comment = "",
86  $author = "",
87  $owner = -1,
88  $question = "",
90  ) {
92  $this->output_type = $output_type;
93  $this->answers = [];
94  $this->shuffle = 1;
95  $this->feedback_setting = 2;
96  }
97 
104  public function isComplete(): bool
105  {
106  if ($this->title !== ''
107  && $this->author !== null && $this->author !== ''
108  && $this->question !== null && $this->question !== ''
109  && $this->answers !== []
110  && $this->getMaximumPoints() > 0) {
111  foreach ($this->answers as $answer) {
112  if ($answer->getAnswertext() === '' && !$answer->hasImage()) {
113  return false;
114  }
115  }
116  return true;
117  } else {
118  return false;
119  }
120  }
121 
128  public function saveToDb($original_id = ""): void
129  {
131  global $DIC;
132  $ilDB = $DIC['ilDB'];
133 
134  if ($original_id == '') {
135  $this->saveQuestionDataToDb();
136  } else {
138  }
139  // kann das weg?
140  $oldthumbsize = 0;
141  if ($this->isSingleline && ($this->getThumbSize())) {
142  // get old thumbnail size
143  $result = $ilDB->queryF(
144  "SELECT thumb_size FROM " . $this->getAdditionalTableName() . " WHERE question_fi = %s",
145  ["integer"],
146  [$this->getId()]
147  );
148  if ($result->numRows() == 1) {
149  $data = $ilDB->fetchAssoc($result);
150  $oldthumbsize = $data['thumb_size'];
151  }
152  }
153 
154 
156 
158 
159  parent::saveToDb($original_id);
160  }
161 
169  public function loadFromDb($question_id): void
170  {
171  global $DIC;
172  $ilDB = $DIC['ilDB'];
173 
174  $result = $ilDB->queryF(
175  "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",
176  ["integer"],
177  [$question_id]
178  );
179  if ($result->numRows() == 1) {
180  $data = $ilDB->fetchAssoc($result);
181  $this->setId($question_id);
182  $this->setObjId($data["obj_fi"]);
183  $this->setTitle($data["title"] ?? '');
184  $this->setNrOfTries($data['nr_of_tries']);
185  $this->setComment($data["description"] ?? '');
186  $this->setOriginalId($data["original_id"]);
187  $this->setAuthor($data["author"]);
188  $this->setPoints($data["points"]);
189  $this->setOwner($data["owner"]);
190  $this->setQuestion(ilRTE::_replaceMediaObjectImageSrc($data["question_text"] ?? '', 1));
191  $shuffle = (is_null($data['shuffle'])) ? true : $data['shuffle'];
192  $this->setShuffle((bool) $shuffle);
193  if ($data['thumb_size'] !== null && $data['thumb_size'] >= $this->getMinimumThumbSize()) {
194  $this->setThumbSize($data['thumb_size']);
195  }
196  $this->isSingleline = $data['allow_images'] === null || $data['allow_images'] === '0';
197  $this->lastChange = $data['tstamp'];
198  $this->feedback_setting = $data['feedback_setting'];
199 
200  try {
204  }
205 
206  try {
207  $this->setAdditionalContentEditingMode($data['add_cont_edit_mode']);
208  } catch (ilTestQuestionPoolException $e) {
209  }
210  }
211 
212  $result = $ilDB->queryF(
213  "SELECT * FROM qpl_a_sc WHERE question_fi = %s ORDER BY aorder ASC",
214  ['integer'],
215  [$question_id]
216  );
217 
218  if ($result->numRows() > 0) {
219  while ($data = $ilDB->fetchAssoc($result)) {
220  $imagefilename = $this->getImagePath() . $data["imagefile"];
221  if (!file_exists($imagefilename)) {
222  $data["imagefile"] = null;
223  }
224 
225  $data["answertext"] = ilRTE::_replaceMediaObjectImageSrc($data["answertext"] ?? '', 1);
226  $image = new ASS_AnswerBinaryStateImage(
227  $data["answertext"],
228  $data["points"],
229  $data["aorder"],
230  true,
231  $data["imagefile"] ? $data["imagefile"] : null,
232  $data["answer_id"]
233  );
234  $this->answers[] = $image;
235  }
236  }
237 
238  parent::loadFromDb($question_id);
239  }
240 
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  $original_id = $this->questioninfo->getOriginalId($this->id);
258  $clone->id = -1;
259 
260  if ((int) $testObjId > 0) {
261  $clone->setObjId($testObjId);
262  }
263 
264  if ($title) {
265  $clone->setTitle($title);
266  }
267 
268  if ($author) {
269  $clone->setAuthor($author);
270  }
271  if ($owner) {
272  $clone->setOwner($owner);
273  }
274  if ($for_test) {
275  $clone->saveToDb($original_id);
276  } else {
277  $clone->saveToDb();
278  }
279 
280  // copy question page content
281  $clone->copyPageOfQuestion($this_id);
282 
283  // copy XHTML media objects
284  $clone->copyXHTMLMediaObjectsOfQuestion($this_id);
285  // duplicate the images
286  $clone->duplicateImages($this_id, $thisObjId);
287 
288  $clone->onDuplicate($thisObjId, $this_id, $clone->getObjId(), $clone->getId());
289 
290  return $clone->id;
291  }
292 
298  public function copyObject($target_questionpool_id, $title = ""): int
299  {
300  if ($this->getId() <= 0) {
301  throw new RuntimeException('The question has not been saved. It cannot be duplicated');
302  }
303  // duplicate the question in database
304  $clone = $this;
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 
370  public function addAnswer(
371  $answertext = "",
372  $points = 0.0,
373  $order = 0,
374  $answerimage = null,
375  $answer_id = -1
376  ): void {
377  $answertext = $this->getHtmlQuestionContentPurifier()->purify($answertext);
378  if (array_key_exists($order, $this->answers)) {
379  // insert answer
380  $answer = new ASS_AnswerBinaryStateImage($answertext, $points, $order, true, $answerimage, $answer_id);
381  $newchoices = [];
382  for ($i = 0; $i < $order; $i++) {
383  $newchoices[] = $this->answers[$i];
384  }
385  $newchoices[] = $answer;
386  for ($i = $order, $iMax = count($this->answers); $i < $iMax; $i++) {
387  $changed = $this->answers[$i];
388  $changed->setOrder($i + 1);
389  $newchoices[] = $changed;
390  }
391  $this->answers = $newchoices;
392  } else {
393  $answer = new ASS_AnswerBinaryStateImage(
394  $answertext,
395  $points,
396  count($this->answers),
397  true,
398  $answerimage,
399  $answer_id
400  );
401  $this->answers[] = $answer;
402  }
403  }
404 
412  public function getAnswerCount(): int
413  {
414  return count($this->answers);
415  }
416 
426  public function getAnswer($index = 0): ?object
427  {
428  if ($index < 0) {
429  return null;
430  }
431  if (count($this->answers) < 1) {
432  return null;
433  }
434  if ($index >= count($this->answers)) {
435  return null;
436  }
437 
438  return $this->answers[$index];
439  }
440 
449  public function deleteAnswer($index = 0): void
450  {
451  if ($index < 0) {
452  return;
453  }
454  if (count($this->answers) < 1) {
455  return;
456  }
457  if ($index >= count($this->answers)) {
458  return;
459  }
460  $answer = $this->answers[$index];
461  if ($answer->hasImage()) {
462  $this->deleteImage($answer->getImage());
463  }
464  unset($this->answers[$index]);
465  $this->answers = array_values($this->answers);
466  for ($i = 0, $iMax = count($this->answers); $i < $iMax; $i++) {
467  if ($this->answers[$i]->getOrder() > $index) {
468  $this->answers[$i]->setOrder($i);
469  }
470  }
471  }
472 
479  public function flushAnswers(): void
480  {
481  $this->answers = [];
482  }
483 
490  public function getMaximumPoints(): float
491  {
492  $points = 0;
493  foreach ($this->answers as $key => $value) {
494  if ($value->getPoints() > $points) {
495  $points = $value->getPoints();
496  }
497  }
498  return $points;
499  }
500 
511  public function calculateReachedPoints($active_id, $pass = null, $authorizedSolution = true, $returndetails = false): float
512  {
513  if ($returndetails) {
514  throw new ilTestException('return details not implemented for ' . __METHOD__);
515  }
516 
517  global $DIC;
518  $ilDB = $DIC['ilDB'];
519 
520  $found_values = [];
521  if (is_null($pass)) {
522  $pass = $this->getSolutionMaxPass($active_id);
523  }
524  $result = $this->getCurrentSolutionResultSet($active_id, $pass, $authorizedSolution);
525  while ($data = $ilDB->fetchAssoc($result)) {
526  if (strcmp($data["value1"], "") != 0) {
527  array_push($found_values, $data["value1"]);
528  }
529  }
530  $points = 0;
531  foreach ($this->answers as $key => $answer) {
532  if (count($found_values) > 0) {
533  if (in_array($key, $found_values)) {
534  $points += $answer->getPoints();
535  }
536  }
537  }
538 
539  return (float) $points;
540  }
541 
543  {
544  $participantSolution = $previewSession->getParticipantsSolution();
545 
546  $points = 0;
547 
548  foreach ($this->answers as $key => $answer) {
549  if (is_numeric($participantSolution) && $key == $participantSolution) {
550  $points = $answer->getPoints();
551  }
552  }
553 
554  $reachedPoints = $this->deductHintPointsFromReachedPoints($previewSession, $points);
555 
556  return $this->ensureNonNegativePoints($reachedPoints);
557  }
558 
566  public function saveWorkingData($active_id, $pass = null, $authorized = true): bool
567  {
568  global $DIC;
569  $ilDB = $DIC['ilDB'];
570  $ilUser = $DIC['ilUser'];
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, $ilDB, $active_id, $pass, $authorized) {
579  $result = $this->getCurrentSolutionResultSet($active_id, $pass, $authorized);
580 
581  $update = -1;
582  if ($ilDB->numRows($result)) {
583  $row = $ilDB->fetchAssoc($result);
584  $update = $row["solution_id"];
585  }
586 
587  $multiple_choice_result = $this->http->wrapper()->post()->has('multiple_choice_result') ?
588  $this->http->wrapper()->post()->retrieve('multiple_choice_result', $this->refinery->kindlyTo()->string()) :
589  '';
590 
591  if ($update != -1) {
592  if ($multiple_choice_result !== '') {
593  $this->updateCurrentSolution($update, $multiple_choice_result, null, $authorized);
594  $entered_values++;
595  } else {
596  $this->removeSolutionRecordById($update);
597  }
598  } else {
599  if ($multiple_choice_result !== '') {
600  $this->saveCurrentSolution($active_id, $pass, $multiple_choice_result, null, $authorized);
601  $entered_values++;
602  }
603  }
604  });
605 
606  if ($entered_values) {
608  assQuestion::logAction($this->lng->txtlng(
609  "assessment",
610  "log_user_entered_values",
612  ), $active_id, $this->getId());
613  }
614  } else {
616  assQuestion::logAction($this->lng->txtlng(
617  "assessment",
618  "log_user_not_entered_values",
620  ), $active_id, $this->getId());
621  }
622  }
623 
624  return true;
625  }
626 
627  protected function savePreviewData(ilAssQuestionPreviewSession $previewSession): void
628  {
629  $mc_result_key = 'multiple_choice_result' . $this->getId() . 'ID';
630  if (
631  $this->http->wrapper()->post()->has($mc_result_key) &&
632  ($mc_result = $this->http->wrapper()->post()->retrieve($mc_result_key, $this->refinery->kindlyTo()->string())) !== ''
633  ) {
634  $previewSession->setParticipantsSolution($mc_result);
635  } else {
636  $previewSession->setParticipantsSolution(null);
637  }
638  }
639 
640  public function saveAdditionalQuestionDataToDb()
641  {
643  global $DIC;
644  $ilDB = $DIC['ilDB'];
645 
646  // save additional data
647  $ilDB->manipulateF(
648  "DELETE FROM " . $this->getAdditionalTableName() . " WHERE question_fi = %s",
649  [ "integer" ],
650  [ $this->getId() ]
651  );
652 
653  $ilDB->manipulateF(
654  "INSERT INTO " . $this->getAdditionalTableName(
655  ) . " (question_fi, shuffle, allow_images, thumb_size) VALUES (%s, %s, %s, %s)",
656  [ "integer", "text", "text", "integer" ],
657  [
658  $this->getId(),
659  $this->getShuffle(),
660  ($this->isSingleline) ? "0" : "1",
661  $this->getThumbSize()
662  ]
663  );
664  }
665 
671  public function saveAnswerSpecificDataToDb()
672  {
674  global $DIC;
675  $ilDB = $DIC['ilDB'];
676 
677  if (!$this->isSingleline) {
679  }
680  // Get all feedback entries
681  $result = $ilDB->queryF(
682  "SELECT * FROM qpl_fb_specific WHERE question_fi = %s",
683  ['integer'],
684  [$this->getId()]
685  );
686  $db_feedback = $ilDB->fetchAll($result);
687 
688  // Check if feedback exists and the regular editor is used and not the page editor
689  if (sizeof($db_feedback) >= 1 && $this->getAdditionalContentEditingMode() == 'default') {
690  // Get all existing answer data for question
691  $result = $ilDB->queryF(
692  "SELECT answer_id, aorder FROM qpl_a_sc WHERE question_fi = %s",
693  ['integer'],
694  [$this->getId()]
695  );
696  $db_answers = $ilDB->fetchAll($result);
697 
698  // Collect old and new order entries by ids and order to calculate a diff/intersection and remove/update feedback
699  $post_answer_order_for_id = [];
700  foreach ($this->answers as $answer) {
701  // Only the first appearance of an id is used
702  if ($answer->getId() !== null && !in_array($answer->getId(), array_keys($post_answer_order_for_id))) {
703  // -1 is happening while import and also if a new multi line answer is generated
704  if ($answer->getId() == -1) {
705  continue;
706  }
707  $post_answer_order_for_id[$answer->getId()] = $answer->getOrder();
708  }
709  }
710 
711  // If there is no usable ids from post, it's better to not touch the feedback
712  // This is useful since the import is also using this function or the first creation of a new question in general
713  if (sizeof($post_answer_order_for_id) >= 1) {
714  $db_answer_order_for_id = [];
715  $db_answer_id_for_order = [];
716  foreach ($db_answers as $db_answer) {
717  $db_answer_order_for_id[intval($db_answer['answer_id'])] = intval($db_answer['aorder']);
718  $db_answer_id_for_order[intval($db_answer['aorder'])] = intval($db_answer['answer_id']);
719  }
720 
721  // Handle feedback
722  // the diff between the already existing answer ids from the Database and the answer ids from post
723  // feedback related to the answer ids should be deleted or in our case not recreated.
724  $db_answer_ids = array_keys($db_answer_order_for_id);
725  $post_answer_ids = array_keys($post_answer_order_for_id);
726  $diff_db_post_answer_ids = array_diff($db_answer_ids, $post_answer_ids);
727  $unused_answer_ids = array_keys($diff_db_post_answer_ids);
728 
729  // Delete all feedback in the database
730  $this->feedbackOBJ->deleteSpecificAnswerFeedbacks($this->getId(), false);
731  // Recreate feedback
732  foreach ($db_feedback as $feedback_option) {
733  // skip feedback which answer is deleted
734  if (in_array(intval($feedback_option['answer']), $unused_answer_ids)) {
735  continue;
736  }
737 
738  // Reorder feedback
739  $feedback_order_db = intval($feedback_option['answer']);
740  $db_answer_id = $db_answer_id_for_order[$feedback_order_db] ?? null;
741  // This cuts feedback that currently would have no corresponding answer
742  // This case can happen while copying "broken" questions
743  // Or when saving a question with less answers than feedback
744  if (is_null($db_answer_id) || $db_answer_id < 0) {
745  continue;
746  }
747  $feedback_order_post = $post_answer_order_for_id[$db_answer_id];
748  $feedback_option['answer'] = $feedback_order_post;
749 
750  // Recreate remaining feedback in database
751  $next_id = $ilDB->nextId('qpl_fb_specific');
752  $ilDB->manipulateF(
753  "INSERT INTO qpl_fb_specific (feedback_id, question_fi, answer, tstamp, feedback, question)
754  VALUES (%s, %s, %s, %s, %s, %s)",
755  ['integer', 'integer', 'integer', 'integer', 'text', 'integer'],
756  [
757  $next_id,
758  $feedback_option['question_fi'],
759  $feedback_option['answer'],
760  time(),
761  $feedback_option['feedback'],
762  $feedback_option['question']
763  ]
764  );
765  }
766  }
767  }
768 
769  // Delete all entries in qpl_a_sc for question
770  $ilDB->manipulateF(
771  "DELETE FROM qpl_a_sc WHERE question_fi = %s",
772  ['integer'],
773  [$this->getId()]
774  );
775 
776  // Recreate answers one by one
777  foreach ($this->answers as $key => $value) {
779  $answer_obj = $this->answers[$key];
780  $next_id = $ilDB->nextId('qpl_a_sc');
781  $ilDB->manipulateF(
782  "INSERT INTO qpl_a_sc (answer_id, question_fi, answertext, points, aorder, imagefile, tstamp) VALUES (%s, %s, %s, %s, %s, %s, %s)",
783  ['integer', 'integer', 'text', 'float', 'integer', 'text', 'integer'],
784  [
785  $next_id,
786  $this->getId(),
787  ilRTE::_replaceMediaObjectImageSrc($answer_obj->getAnswertext(), 0),
788  $answer_obj->getPoints(),
789  $answer_obj->getOrder(),
790  $answer_obj->getImage(),
791  time()
792  ]
793  );
794  }
795  }
796 
803  public function getQuestionType(): string
804  {
805  return "assSingleChoice";
806  }
807 
814  public function getAdditionalTableName(): string
815  {
816  return "qpl_qst_sc";
817  }
818 
825  public function getAnswerTableName(): string
826  {
827  return "qpl_a_sc";
828  }
829 
838  public function setImageFile($image_filename, $image_tempfilename = ""): int
839  {
840  if (empty($image_tempfilename)) {
841  return 0;
842  }
843 
844  $cleaned_image_filename = str_replace(" ", "_", $image_filename);
845  $imagepath = $this->getImagePath();
846  if (!file_exists($imagepath)) {
847  ilFileUtils::makeDirParents($imagepath);
848  }
849 
850  if (!ilFileUtils::moveUploadedFile($image_tempfilename, $cleaned_image_filename, $imagepath . $cleaned_image_filename)) {
851  return 2;
852  }
853 
854  $mimetype = ilObjMediaObject::getMimeType($imagepath . $cleaned_image_filename);
855  if (!preg_match("/^image/", $mimetype)) {
856  unlink($imagepath . $cleaned_image_filename);
857  return 1;
858  }
859 
860  if ($this->isSingleline && $this->getThumbSize()) {
861  $this->generateThumbForFile(
862  $cleaned_image_filename,
863  $this->getImagePath(),
864  $this->getThumbSize()
865  );
866  }
867 
868  return 0;
869  }
870 
877  public function deleteImage($image_filename): void
878  {
879  $imagepath = $this->getImagePath();
880  @unlink($imagepath . $image_filename);
881  $thumbpath = $imagepath . $this->getThumbPrefix() . $image_filename;
882  @unlink($thumbpath);
883  }
884 
885  public function duplicateImages($question_id, $objectId = null): void
886  {
887  global $DIC;
888  $ilLog = $DIC['ilLog'];
889  $imagepath = $this->getImagePath();
890  $imagepath_original = str_replace("/$this->id/images", "/$question_id/images", $imagepath);
891 
892  if ((int) $objectId > 0) {
893  $imagepath_original = str_replace("/$this->obj_id/", "/$objectId/", $imagepath_original);
894  }
895 
896  foreach ($this->answers as $answer) {
897  if ($answer->hasImage()) {
898  $filename = $answer->getImage();
899  if (!file_exists($imagepath)) {
900  ilFileUtils::makeDirParents($imagepath);
901  }
902  if (!@copy($imagepath_original . $filename, $imagepath . $filename)) {
903  $ilLog->write("image could not be duplicated!!!!", $ilLog->ERROR);
904  $ilLog->write("object: " . print_r($this, true), $ilLog->ERROR);
905  }
906  if (@file_exists($imagepath_original . $this->getThumbPrefix() . $filename)) {
907  if (!@copy($imagepath_original . $this->getThumbPrefix() . $filename, $imagepath . $this->getThumbPrefix() . $filename)) {
908  $ilLog->write("image thumbnail could not be duplicated!!!!", $ilLog->ERROR);
909  $ilLog->write("object: " . print_r($this, true), $ilLog->ERROR);
910  }
911  }
912  }
913  }
914  }
915 
916  public function copyImages($question_id, $source_questionpool): void
917  {
919  global $DIC;
920  $ilLog = $DIC['ilLog'];
921 
922  $imagepath = $this->getImagePath();
923  $imagepath_original = str_replace("/$this->id/images", "/$question_id/images", $imagepath);
924  $imagepath_original = str_replace("/$this->obj_id/", "/$source_questionpool/", $imagepath_original);
925  foreach ($this->answers as $answer) {
926  if ($answer->hasImage()) {
927  $filename = $answer->getImage();
928  if (!file_exists($imagepath)) {
929  ilFileUtils::makeDirParents($imagepath);
930  }
931 
932  if (file_exists($imagepath_original . $filename)) {
933  if (!copy($imagepath_original . $filename, $imagepath . $filename)) {
934  $ilLog->warning(sprintf(
935  "Could not clone source image '%s' to '%s' (srcQuestionId: %s|tgtQuestionId: %s|srcParentObjId: %s|tgtParentObjId: %s)",
936  $imagepath_original . $filename,
937  $imagepath . $filename,
938  $question_id,
939  $this->id,
940  $source_questionpool,
941  $this->obj_id
942  ));
943  }
944  }
945 
946  if (file_exists($imagepath_original . $this->getThumbPrefix() . $filename)) {
947  if (!copy($imagepath_original . $this->getThumbPrefix() . $filename, $imagepath . $this->getThumbPrefix() . $filename)) {
948  $ilLog->warning(sprintf(
949  "Could not clone thumbnail source image '%s' to '%s' (srcQuestionId: %s|tgtQuestionId: %s|srcParentObjId: %s|tgtParentObjId: %s)",
950  $imagepath_original . $this->getThumbPrefix() . $filename,
951  $imagepath . $this->getThumbPrefix() . $filename,
952  $question_id,
953  $this->id,
954  $source_questionpool,
955  $this->obj_id
956  ));
957  }
958  }
959  }
960  }
961  }
962 
966  protected function syncImages(): void
967  {
968  global $DIC;
969  $ilLog = $DIC['ilLog'];
970  $question_id = $this->questioninfo->getOriginalId();
971  $imagepath = $this->getImagePath();
972  $imagepath_original = str_replace("/$this->id/images", "/$question_id/images", $imagepath);
973  ilFileUtils::delDir($imagepath_original);
974  foreach ($this->answers as $answer) {
975  if ($answer->hasImage()) {
976  $filename = $answer->getImage();
977  if (@file_exists($imagepath . $filename)) {
978  if (!file_exists($imagepath)) {
979  ilFileUtils::makeDirParents($imagepath);
980  }
981  if (!file_exists($imagepath_original)) {
982  ilFileUtils::makeDirParents($imagepath_original);
983  }
984  if (!@copy($imagepath . $filename, $imagepath_original . $filename)) {
985  $ilLog->write("image could not be duplicated!!!!", $ilLog->ERROR);
986  $ilLog->write("object: " . print_r($this, true), $ilLog->ERROR);
987  }
988  }
989  if (@file_exists($imagepath . $this->getThumbPrefix() . $filename)) {
990  if (!@copy($imagepath . $this->getThumbPrefix() . $filename, $imagepath_original . $this->getThumbPrefix() . $filename)) {
991  $ilLog->write("image thumbnail could not be duplicated!!!!", $ilLog->ERROR);
992  $ilLog->write("object: " . print_r($this, true), $ilLog->ERROR);
993  }
994  }
995  }
996  }
997  }
998 
1003  public function getRTETextWithMediaObjects(): string
1004  {
1005  $text = parent::getRTETextWithMediaObjects();
1006  foreach ($this->answers as $index => $answer) {
1007  $text .= $this->feedbackOBJ->getSpecificAnswerFeedbackContent($this->getId(), 0, $index);
1008  $answer_obj = $this->answers[$index];
1009  $text .= $answer_obj->getAnswertext();
1010  }
1011  return $text;
1012  }
1013 
1017  public function &getAnswers(): array
1018  {
1019  return $this->answers;
1020  }
1021 
1022  public function setAnswers(array $answers): void
1023  {
1024  $this->answers = $answers;
1025  }
1026 
1030  public function setExportDetailsXLSX(ilAssExcelFormatHelper $worksheet, int $startrow, int $col, int $active_id, int $pass): int
1031  {
1032  parent::setExportDetailsXLSX($worksheet, $startrow, $col, $active_id, $pass);
1033 
1034  $solution = $this->getSolutionValues($active_id, $pass);
1035  $i = 1;
1036  foreach ($this->getAnswers() as $id => $answer) {
1037  $worksheet->setCell($startrow + $i, $col, $answer->getAnswertext());
1038  $worksheet->setBold($worksheet->getColumnCoord($col) . ($startrow + $i));
1039  if (
1040  count($solution) > 0 &&
1041  isset($solution[0]) &&
1042  is_array($solution[0]) &&
1043  strlen($solution[0]['value1']) > 0 && $id == $solution[0]['value1']
1044  ) {
1045  $worksheet->setCell($startrow + $i, $col + 2, 1);
1046  } else {
1047  $worksheet->setCell($startrow + $i, $col + 2, 0);
1048  }
1049  $i++;
1050  }
1051 
1052  return $startrow + $i + 1;
1053  }
1054 
1059  {
1060  foreach ($this->getAnswers() as $answer) {
1061  /* @var ASS_AnswerBinaryStateImage $answer */
1062  $answer->setAnswertext($migrator->migrateToLmContent($answer->getAnswertext()));
1063  }
1064  }
1065 
1069  public function toJSON(): string
1070  {
1071  $result = [];
1072  $result['id'] = $this->getId();
1073  $result['type'] = (string) $this->getQuestionType();
1074  $result['title'] = $this->getTitleForHTMLOutput();
1075  $result['question'] = $this->formatSAQuestion($this->getQuestion());
1076  $result['nr_of_tries'] = $this->getNrOfTries();
1077  $result['shuffle'] = $this->getShuffle();
1078 
1079  $result['feedback'] = [
1080  'onenotcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)),
1081  'allcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true))
1082  ];
1083 
1084  $answers = [];
1085  $has_image = false;
1086  foreach ($this->getAnswers() as $key => $answer_obj) {
1087  if ((string) $answer_obj->getImage()) {
1088  $has_image = true;
1089  }
1090  array_push($answers, [
1091  "answertext" => $this->formatSAQuestion($answer_obj->getAnswertext()),
1092  'html_id' => $this->getId() . '_' . $key,
1093  "points" => (float) $answer_obj->getPoints(),
1094  "order" => (int) $answer_obj->getOrder(),
1095  "image" => (string) $answer_obj->getImage(),
1096  "feedback" => $this->formatSAQuestion(
1097  $this->feedbackOBJ->getSpecificAnswerFeedbackExportPresentation($this->getId(), 0, $key)
1098  )
1099  ]);
1100  }
1101  $result['answers'] = $answers;
1102  if ($has_image) {
1103  $result['path'] = $this->getImagePathWeb();
1104  $result['thumb'] = $this->getThumbSize();
1105  }
1106 
1107  $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId());
1108  $result['mobs'] = $mobs;
1109 
1110  return json_encode($result);
1111  }
1112 
1113  public function removeAnswerImage($index): void
1114  {
1115  $answer = $this->answers[$index];
1116  if (is_object($answer)) {
1117  $this->deleteImage($answer->getImage());
1118  $answer->setImage(null);
1119  }
1120  }
1121 
1122  public function getMultilineAnswerSetting()
1123  {
1124  global $DIC;
1125  $ilUser = $DIC['ilUser'];
1126 
1127  $multilineAnswerSetting = $ilUser->getPref("tst_multiline_answers");
1128  if ($multilineAnswerSetting != 1) {
1129  $multilineAnswerSetting = 0;
1130  }
1131  return $multilineAnswerSetting;
1132  }
1133 
1134  public function setMultilineAnswerSetting($a_setting = 0): void
1135  {
1136  global $DIC;
1137  $ilUser = $DIC['ilUser'];
1138  $ilUser->writePref("tst_multiline_answers", (string) $a_setting);
1139  }
1140 
1150  public function setSpecificFeedbackSetting($a_feedback_setting): void
1151  {
1152  $this->feedback_setting = $a_feedback_setting;
1153  }
1154 
1164  public function getSpecificFeedbackSetting(): int
1165  {
1166  if ($this->feedback_setting) {
1167  return $this->feedback_setting;
1168  } else {
1169  return 1;
1170  }
1171  }
1172 
1174  {
1175  return 'feedback_correct_sc_mc';
1176  }
1177 
1188  public static function isObligationPossible(int $questionId): bool
1189  {
1190  return true;
1191  }
1192 
1201  public function getOperators($expression): array
1202  {
1204  }
1205 
1210  public function getExpressionTypes(): array
1211  {
1212  return [
1216  ];
1217  }
1218 
1227  public function getUserQuestionResult($active_id, $pass): ilUserQuestionResult
1228  {
1230  global $DIC;
1231  $ilDB = $DIC['ilDB'];
1232  $result = new ilUserQuestionResult($this, $active_id, $pass);
1233 
1234  $maxStep = $this->lookupMaxStep($active_id, $pass);
1235 
1236  if ($maxStep > 0) {
1237  $data = $ilDB->queryF(
1238  "SELECT * FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s AND step = %s",
1239  ["integer", "integer", "integer","integer"],
1240  [$active_id, $pass, $this->getId(), $maxStep]
1241  );
1242  } else {
1243  $data = $ilDB->queryF(
1244  "SELECT * FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s",
1245  ["integer", "integer", "integer"],
1246  [$active_id, $pass, $this->getId()]
1247  );
1248  }
1249 
1250  $row = $ilDB->fetchAssoc($data);
1251 
1252  if ($row != null) {
1253  ++$row["value1"];
1254  $result->addKeyValue($row["value1"], $row["value1"]);
1255  }
1256 
1257  $points = $this->calculateReachedPoints($active_id, $pass);
1258  $max_points = $this->getMaximumPoints();
1259 
1260  $result->setReachedPercentage(($points / $max_points) * 100);
1261 
1262  return $result;
1263  }
1264 
1271  public function getAvailableAnswerOptions($index = null)
1272  {
1273  if ($index !== null) {
1274  return $this->getAnswer($index);
1275  } else {
1276  return $this->getAnswers();
1277  }
1278  }
1279 
1283  protected function afterSyncWithOriginal($origQuestionId, $dupQuestionId, $origParentObjId, $dupParentObjId): void
1284  {
1285  parent::afterSyncWithOriginal($origQuestionId, $dupQuestionId, $origParentObjId, $dupParentObjId);
1286 
1287  $origImagePath = $this->questionFilesService->buildImagePath($origQuestionId, $origParentObjId);
1288  $dupImagePath = $this->questionFilesService->buildImagePath($dupQuestionId, $dupParentObjId);
1289 
1290  ilFileUtils::delDir($origImagePath);
1291  if (is_dir($dupImagePath)) {
1292  ilFileUtils::makeDirParents($origImagePath);
1293  ilFileUtils::rCopy($dupImagePath, $origImagePath);
1294  }
1295  }
1296 
1297  public function isSingleline(): bool
1298  {
1299  return $this->isSingleline;
1300  }
1301 
1302  public function setIsSingleline(bool $isSingleline): void
1303  {
1304  $this->isSingleline = $isSingleline;
1305  }
1306 
1307  public function getFeedbackSetting(): int
1308  {
1309  return $this->feedback_setting;
1310  }
1311 
1312  public function setFeedbackSetting(int $feedback_setting): void
1313  {
1314  $this->feedback_setting = $feedback_setting;
1315  }
1316 }
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...
getSolutionValues($active_id, $pass=null, bool $authorized=true)
Loads solutions of a given user from the database an returns it.
setNrOfTries(int $a_nr_of_tries)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
getMaximumPoints()
Returns the maximum points, a learner can reach answering the question.
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...
saveAdditionalQuestionDataToDb()
Saves a record to the question types additional data table.
Abstract basic class which is to be extended by the concrete assessment question type classes...
setAnswers(array $answers)
Class for answers with a binary state indicator.
setOwner(int $owner=-1)
setMultilineAnswerSetting($a_setting=0)
& getAnswers()
Returns a reference to the answers array.
afterSyncWithOriginal($origQuestionId, $dupQuestionId, $origParentObjId, $dupParentObjId)
{}
getAnswerCount()
Returns the number of answers.
getColumnCoord(int $a_col)
Get column "name" from number.
ensureNonNegativePoints($points)
bool $shuffle
Indicates whether the answers will be shuffled or not.
getAvailableAnswerOptions($index=null)
If index is null, the function returns an array with all anwser options Else it returns the specific ...
isComplete()
Returns true, if a single choice question is complete for use.
setExportDetailsXLSX(ilAssExcelFormatHelper $worksheet, int $startrow, int $col, int $active_id, int $pass)
{}
copyObject($target_questionpool_id, $title="")
Copies an assSingleChoice object.
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...
getAnswerTableName()
Returns the name of the answer table in the database.
getImagePathWeb()
Returns the web image path for web accessable images of a question.
setThumbSize(int $a_size)
static rCopy(string $a_sdir, string $a_tdir, bool $preserveTimeAttributes=false)
Copies content of a directory $a_sdir recursively to a directory $a_tdir.
loadFromDb($question_id)
Loads a assSingleChoice object from a database.
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.
setSpecificFeedbackSetting($a_feedback_setting)
Sets the feedback settings in effect for the question.
static makeDirParents(string $a_dir)
Create a new directory and all parent directories.
getAnswer($index=0)
Returns an answer with a given index.
setComment(string $comment="")
getOperators($expression)
Get all available operations for a specific question.
setIsSingleline(bool $isSingleline)
float $points
The maximum available points for the question.
Base Exception for all Exceptions relating to Modules/Test.
saveWorkingData($active_id, $pass=null, $authorized=true)
Saves the learners input of the question to the database.
setFeedbackSetting(int $feedback_setting)
getSpecificFeedbackSetting()
Gets the current feedback settings in effect for the question.
updateCurrentSolution(int $solutionId, $value1, $value2, bool $authorized=true)
createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle="")
global $DIC
Definition: feed.php:28
deleteImage($image_filename)
Deletes an image file.
saveCurrentSolution(int $active_id, int $pass, $value1, $value2, bool $authorized=true, $tstamp=0)
setBold(string $a_coords)
Set cell(s) to bold.
calculateReachedPointsFromPreviewSession(ilAssQuestionPreviewSession $previewSession)
static http()
Fetches the global http state from ILIAS.
__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...
static logAction(string $logtext, int $active_id, int $question_id)
removeSolutionRecordById(int $solutionId)
Class for single choice questions.
static delDir(string $a_dir, bool $a_clean_only=false)
removes a dir and all its content (subdirs and files) recursively
deleteAnswer($index=0)
Deletes an answer with a given index.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
toJSON()
Returns a JSON representation of the question.
string $key
Consumer key/client ID value.
Definition: System.php:193
__construct( $title="", $comment="", $author="", $owner=-1, $question="", $output_type=OUTPUT_ORDER)
assSingleChoice constructor
syncImages()
Sync images of a MC question on synchronisation with the original question.
flushAnswers()
Deletes all answers.
static moveUploadedFile(string $a_file, string $a_name, string $a_target, bool $a_raise_errors=true, string $a_mode="move_uploaded")
move uploaded file
setPoints(float $points)
setObjId(int $obj_id=0)
string $question
The question text.
getAdditionalTableName()
Returns the name of the additional question data table in the database.
lmMigrateQuestionTypeSpecificContent(ilAssSelfAssessmentMigrator $migrator)
static _getMobsOfObject(string $a_type, int $a_id, int $a_usage_hist_nr=0, string $a_lang="-")
duplicate(bool $for_test=true, string $title="", string $author="", int $owner=-1, $testObjId=null)
Duplicates an assSingleChoiceQuestion.
$filename
Definition: buildRTE.php:78
saveAnswerSpecificDataToDb()
Saves the answer specific records into a question types answer table.
deductHintPointsFromReachedPoints(ilAssQuestionPreviewSession $previewSession, $reachedPoints)
getRTETextWithMediaObjects()
Collects all text in the question which could contain media objects which were created with the Rich ...
calculateReachedPoints($active_id, $pass=null, $authorizedSolution=true, $returndetails=false)
Returns the points, a learner has reached answering the question.
saveQuestionDataToDb(int $original_id=-1)
getSolutionMaxPass(int $active_id)
setImageFile($image_filename, $image_tempfilename="")
Sets the image file and uploads the image to the object&#39;s image directory.
duplicateImages($question_id, $objectId=null)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setId(int $id=-1)
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="")
getExpressionTypes()
Get all available expression types for a specific question.
setLifecycle(ilAssQuestionLifecycle $lifecycle)
getCurrentSolutionResultSet(int $active_id, int $pass, bool $authorized=true)
const OUTPUT_ORDER
addAnswer( $answertext="", $points=0.0, $order=0, $answerimage=null, $answer_id=-1)
Adds a possible answer for a single choice question.
ILIAS DI LoggingServices $ilLog
lookupMaxStep(int $active_id, int $pass)
setAuthor(string $author="")
setShuffle(?bool $shuffle=true)
setAdditionalContentEditingMode(?string $additionalContentEditingMode)
static isObligationPossible(int $questionId)
returns boolean wether it is possible to set this question type as obligatory or not considering the ...
savePreviewData(ilAssQuestionPreviewSession $previewSession)
setQuestion(string $question="")