ILIAS  release_8 Revision v8.19
All Data Structures Namespaces Files Functions Variables Modules Pages
class.assMultipleChoice.php
Go to the documentation of this file.
1 <?php
2 
19 require_once './Modules/Test/classes/inc.AssessmentConstants.php';
20 
37 {
45  public $answers;
46 
55  public $output_type;
56 
57  public $isSingleline;
58  public $lastChange;
60 
64  protected $selectionLimit;
65 
69  public function setIsSingleline($isSingleline): void
70  {
71  $this->isSingleline = $isSingleline;
72  }
73 
77  public function getIsSingleline()
78  {
79  return $this->isSingleline;
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  {
117  return $this->selectionLimit;
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 {
253  }
254 
255  try {
256  $this->setAdditionalContentEditingMode($data['add_cont_edit_mode']);
257  } catch (ilTestQuestionPoolException $e) {
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 
276  $answer = new ASS_AnswerMultipleResponseImage(
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) {
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 
1190  public function getMultilineAnswerSetting(): int
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 
1232  public function getSpecificFeedbackSetting(): int
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 }
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)
$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...
isAnswered(int $active_id, int $pass)
returns boolean wether the question is answered during test pass or not
generateThumbForFile($path, $file)
static _getPass($active_id)
Retrieves the actual pass of a given user for a given test.
isComplete()
Returns true, if a multiple choice question is complete for use.
copyObject($target_questionpool_id, $title="")
Copies an assMultipleChoice object.
$mobs
Definition: imgupload.php:70
createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle="")
duplicate(bool $for_test=true, string $title="", string $author="", string $owner="", $testObjId=null)
Duplicates an assMultipleChoiceQuestion.
saveAdditionalQuestionDataToDb()
Saves a record to the question types additional data table.
rebuildThumbnails()
Rebuild the thumbnail images with a new thumbnail size.
addAnswer( $answertext="", $points=0.0, $points_unchecked=0.0, $order=0, $answerimage="", $answer_id=-1)
Adds a possible answer for a multiple choice question.
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...
setExportDetailsXLS(ilAssExcelFormatHelper $worksheet, int $startrow, int $active_id, int $pass)
{}
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.
if(!array_key_exists('PATH_INFO', $_SERVER)) $config
Definition: metadata.php:85
bool $shuffle
Indicates whether the answers will be shuffled or not.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
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)
static _getOriginalId(int $question_id)
static getNumExistingSolutionRecords(int $activeId, int $pass, int $questionId)
setOutputType($output_type=OUTPUT_ORDER)
Sets the output type of the assMultipleChoice object.
getQuestionType()
Returns the question type of the question.
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="")
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
float $points
The maximum available points for the question.
lmMigrateQuestionTypeSpecificContent(ilAssSelfAssessmentMigrator $migrator)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
$index
Definition: metadata.php:145
$path
Definition: ltiservices.php:32
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
saveCurrentSolution(int $active_id, int $pass, $value1, $value2, bool $authorized=true, $tstamp=0)
setBold(string $a_coords)
Set cell(s) to bold.
getImagePath($question_id=null, $object_id=null)
Returns the image path for web accessable images of a question.
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
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)
$query
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 convertImage(string $a_from, string $a_to, string $a_target_format="", string $a_geometry="", string $a_background_color="")
convert image
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)
__construct(Container $dic, ilPlugin $plugin)
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...
$ilUser
Definition: imgupload.php:34
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
getOutputType()
Gets the multiple choice output type which is either OUTPUT_ORDER (=0) or OUTPUT_RANDOM (=1)...
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)
$i
Definition: metadata.php:41
setQuestion(string $question="")