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