ILIAS  release_8 Revision v8.19
All Data Structures Namespaces Files Functions Variables Modules Pages
class.assKprimChoice.php
Go to the documentation of this file.
1 <?php
2 
26 {
27  public const NUM_REQUIRED_ANSWERS = 4;
28 
30 
31  public const ANSWER_TYPE_SINGLE_LINE = 'singleLine';
32  public const ANSWER_TYPE_MULTI_LINE = 'multiLine';
33 
34  public const OPTION_LABEL_RIGHT_WRONG = 'right_wrong';
35  public const OPTION_LABEL_PLUS_MINUS = 'plus_minus';
36  public const OPTION_LABEL_APPLICABLE_OR_NOT = 'applicable_or_not';
37  public const OPTION_LABEL_ADEQUATE_OR_NOT = 'adequate_or_not';
38  public const OPTION_LABEL_CUSTOM = 'customlabel';
39 
40  public const DEFAULT_THUMB_SIZE = 150;
41  public const THUMB_PREFIX = 'thumb.';
42 
44 
45  private $answerType;
46 
47  private $thumbSize;
48 
50 
51  private $optionLabel;
52 
54 
56 
58 
59  private $answers;
60 
61  public function __construct($title = '', $comment = '', $author = '', $owner = -1, $question = '')
62  {
64 
65  $this->shuffleAnswersEnabled = true;
66  $this->answerType = self::ANSWER_TYPE_SINGLE_LINE;
67  $this->thumbSize = self::DEFAULT_THUMB_SIZE;
68  $this->scorePartialSolutionEnabled = true;
69  $this->optionLabel = self::OPTION_LABEL_RIGHT_WRONG;
70  $this->customTrueOptionLabel = '';
71  $this->customFalseOptionLabel = '';
72 
74 
75  $this->answers = array();
76  }
77 
78  public function getQuestionType(): string
79  {
80  return 'assKprimChoice';
81  }
82 
83  public function getAdditionalTableName(): string
84  {
85  return "qpl_qst_kprim";
86  }
87 
88  public function getAnswerTableName(): string
89  {
90  return "qpl_a_kprim";
91  }
92 
94  {
95  $this->shuffleAnswersEnabled = $shuffleAnswersEnabled;
96  }
97 
98  public function isShuffleAnswersEnabled(): bool
99  {
101  }
102 
103  public function setAnswerType($answerType): void
104  {
105  $this->answerType = $answerType;
106  }
107 
108  public function getAnswerType(): string
109  {
110  return $this->answerType;
111  }
112 
113  public function setThumbSize(int $thumbSize): void
114  {
115  $this->thumbSize = $thumbSize;
116  }
117 
118  public function getThumbSize(): int
119  {
120  return $this->thumbSize;
121  }
122 
124  {
125  $this->scorePartialSolutionEnabled = $scorePartialSolutionEnabled;
126  }
127 
128  public function isScorePartialSolutionEnabled(): bool
129  {
131  }
132 
133  public function setOptionLabel($optionLabel): void
134  {
135  $this->optionLabel = $optionLabel;
136  }
137 
138  public function getOptionLabel(): string
139  {
140  return $this->optionLabel;
141  }
142 
144  {
145  $this->customTrueOptionLabel = $customTrueOptionLabel;
146  }
147 
148  public function getCustomTrueOptionLabel()
149  {
151  }
152 
154  {
155  $this->customFalseOptionLabel = $customFalseOptionLabel;
156  }
157 
158  public function getCustomFalseOptionLabel()
159  {
161  }
162 
164  {
165  $this->specificFeedbackSetting = $specificFeedbackSetting;
166  }
167 
168  public function getSpecificFeedbackSetting(): int
169  {
171  }
172 
173  public function setAnswers($answers): void
174  {
175  if (is_null($answers)) {
176  return;
177  }
178  $clean_answer_text = function (ilAssKprimChoiceAnswer $answer) {
179  $answer->setAnswertext(
180  $this->getHtmlQuestionContentPurifier()->purify($answer->getAnswertext())
181  );
182  return $answer;
183  };
184  $this->answers = array_map($clean_answer_text, $answers);
185  }
186 
187  public function getAnswers(): array
188  {
189  return $this->answers;
190  }
191 
192  public function getAnswer($position)
193  {
194  foreach ($this->getAnswers() as $answer) {
195  if ($answer->getPosition() == $position) {
196  return $answer;
197  }
198  }
199 
200  return null;
201  }
202 
203  public function addAnswer(ilAssKprimChoiceAnswer $answer): void
204  {
205  $answer->setAnswertext(
206  $this->getHtmlQuestionContentPurifier()->purify($answer->getAnswertext())
207  );
208  $this->answers[] = $answer;
209  }
210 
211  public function loadFromDb($questionId): void
212  {
213  $res = $this->db->queryF($this->buildQuestionDataQuery(), array('integer'), array($questionId));
214 
215  while ($data = $this->db->fetchAssoc($res)) {
216  $this->setId($questionId);
217 
218  $this->setOriginalId($data['original_id']);
219 
220  $this->setObjId($data['obj_fi']);
221 
222  $this->setTitle($data['title'] ?? '');
223  $this->setNrOfTries($data['nr_of_tries']);
224  $this->setComment($data['description'] ?? '');
225  $this->setAuthor($data['author']);
226  $this->setPoints($data['points']);
227  $this->setOwner($data['owner']);
228  $this->setLastChange($data['tstamp']);
229  require_once 'Services/RTE/classes/class.ilRTE.php';
230  $this->setQuestion(ilRTE::_replaceMediaObjectImageSrc($data['question_text'] ?? '', 1));
231 
232  $this->setShuffleAnswersEnabled((bool) $data['shuffle_answers']);
233 
234  if ($this->isValidAnswerType($data['answer_type'])) {
235  $this->setAnswerType($data['answer_type']);
236  }
237 
238  if (is_numeric($data['thumb_size'])) {
239  $this->setThumbSize((int) $data['thumb_size']);
240  }
241 
242  if ($this->isValidOptionLabel($data['opt_label'])) {
243  $this->setOptionLabel($data['opt_label']);
244  }
245 
246  if ($data['custom_true'] !== null) {
247  $this->setCustomTrueOptionLabel($data['custom_true']);
248  }
249 
250  if ($data['custom_false'] !== null) {
251  $this->setCustomFalseOptionLabel($data['custom_false']);
252  }
253 
254  if ($data['score_partsol'] !== null) {
255  $this->setScorePartialSolutionEnabled((bool) $data['score_partsol']);
256  }
257 
258  if (isset($data['feedback_setting'])) {
259  $this->setSpecificFeedbackSetting((int) $data['feedback_setting']);
260  }
261 
262  try {
263  $this->setLifecycle(ilAssQuestionLifecycle::getInstance($data['lifecycle']));
266  }
267 
268  try {
269  $this->setAdditionalContentEditingMode($data['add_cont_edit_mode']);
270  } catch (ilTestQuestionPoolException $e) {
271  }
272  }
273 
274  $this->loadAnswerData($questionId);
275 
276  parent::loadFromDb($questionId);
277  }
278 
279  private function loadAnswerData($questionId): void
280  {
281  global $DIC;
282  $ilDB = $DIC['ilDB'];
283 
284  $res = $this->db->queryF(
285  "SELECT * FROM {$this->getAnswerTableName()} WHERE question_fi = %s ORDER BY position ASC",
286  array('integer'),
287  array($questionId)
288  );
289 
290  require_once 'Modules/TestQuestionPool/classes/class.ilAssKprimChoiceAnswer.php';
291  require_once 'Services/RTE/classes/class.ilRTE.php';
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  array(
338  'question_fi' => array('integer', $this->getId())
339  ),
340  array(
341  'shuffle_answers' => array('integer', (int) $this->isShuffleAnswersEnabled()),
342  'answer_type' => array('text', $this->getAnswerType()),
343  'thumb_size' => array('integer', $this->getThumbSize()),
344  'opt_label' => array('text', $this->getOptionLabel()),
345  'custom_true' => array('text', $this->getCustomTrueOptionLabel()),
346  'custom_false' => array('text', $this->getCustomFalseOptionLabel()),
347  'score_partsol' => array('integer', (int) $this->isScorePartialSolutionEnabled()),
348  'feedback_setting' => array('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  array(
359  'question_fi' => array('integer', $this->getId()),
360  'position' => array('integer', (int) $answer->getPosition())
361  ),
362  array(
363  'answertext' => array('text', $answer->getAnswertext()),
364  'imagefile' => array('text', $answer->getImageFile()),
365  'correctness' => array('integer', (int) $answer->getCorrectness())
366  )
367  );
368  }
369 
370  $this->rebuildThumbnails();
371  }
372 
373  public function isComplete(): bool
374  {
375  foreach (array($this->title, $this->author, $this->question) as $text) {
376  if (!strlen($text)) {
377  return false;
378  }
379  }
380 
381  if (!isset($this->points)) {
382  return false;
383  }
384 
385  foreach ($this->getAnswers() as $answer) {
386  /* @var ilAssKprimChoiceAnswer $answer */
387 
388  if (is_null($answer->getCorrectness())) {
389  return false;
390  }
391 
392  if (
393  (!is_string($answer->getAnswertext()) || $answer->getAnswertext() === '') &&
394  (!is_string($answer->getImageFile()) || $answer->getImageFile() === '')
395  ) {
396  return false;
397  }
398  }
399 
400  return true;
401  }
402 
411  public function saveWorkingData($active_id, $pass = null, $authorized = true): bool
412  {
414  $ilDB = $GLOBALS['DIC']['ilDB'];
415 
416  if (is_null($pass)) {
417  include_once "./Modules/Test/classes/class.ilObjTest.php";
418  $pass = ilObjTest::_getPass($active_id);
419  }
420 
421  $entered_values = 0;
422 
423  $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(function () use (&$entered_values, $active_id, $pass, $authorized) {
424  $this->removeCurrentSolution($active_id, $pass, $authorized);
425 
426  $solutionSubmit = $this->getSolutionSubmit();
427 
428  foreach ($solutionSubmit as $answerIndex => $answerValue) {
429  $this->saveCurrentSolution($active_id, $pass, (int) $answerIndex, (int) $answerValue, $authorized);
430  $entered_values++;
431  }
432  });
433 
434  if ($entered_values) {
435  include_once("./Modules/Test/classes/class.ilObjAssessmentFolder.php");
437  assQuestion::logAction($this->lng->txtlng(
438  "assessment",
439  "log_user_entered_values",
441  ), $active_id, $this->getId());
442  }
443  } else {
444  include_once("./Modules/Test/classes/class.ilObjAssessmentFolder.php");
446  assQuestion::logAction($this->lng->txtlng(
447  "assessment",
448  "log_user_not_entered_values",
450  ), $active_id, $this->getId());
451  }
452  }
453 
454  return true;
455  }
456 
467  public function calculateReachedPoints($active_id, $pass = null, $authorizedSolution = true, $returndetails = false)
468  {
469  if ($returndetails) {
470  throw new ilTestException('return details not implemented for ' . __METHOD__);
471  }
472 
473  global $DIC;
474  $ilDB = $DIC['ilDB'];
475 
476  $found_values = array();
477  if (is_null($pass)) {
478  $pass = $this->getSolutionMaxPass($active_id);
479  }
480 
481  $result = $this->getCurrentSolutionResultSet($active_id, $pass, $authorizedSolution);
482 
483  while ($data = $ilDB->fetchAssoc($result)) {
484  $found_values[(int) $data['value1']] = (int) $data['value2'];
485  }
486 
487  $points = $this->calculateReachedPointsForSolution($found_values, $active_id);
488 
489  return $points;
490  }
491 
492  public function getValidAnswerTypes(): array
493  {
494  return array(self::ANSWER_TYPE_SINGLE_LINE, self::ANSWER_TYPE_MULTI_LINE);
495  }
496 
497  public function isValidAnswerType($answerType): bool
498  {
499  $validTypes = $this->getValidAnswerTypes();
500  return in_array($answerType, $validTypes);
501  }
502 
503  public function isSingleLineAnswerType($answerType): bool
504  {
506  }
507 
513  {
514  return array(
515  self::ANSWER_TYPE_SINGLE_LINE => $lng->txt('answers_singleline'),
516  self::ANSWER_TYPE_MULTI_LINE => $lng->txt('answers_multiline')
517  );
518  }
519 
520  public function getValidOptionLabels(): array
521  {
522  return array(
523  self::OPTION_LABEL_RIGHT_WRONG,
524  self::OPTION_LABEL_PLUS_MINUS,
525  self::OPTION_LABEL_APPLICABLE_OR_NOT,
526  self::OPTION_LABEL_ADEQUATE_OR_NOT,
527  self::OPTION_LABEL_CUSTOM
528  );
529  }
530 
532  {
533  return array(
534  self::OPTION_LABEL_RIGHT_WRONG => $lng->txt('option_label_right_wrong'),
535  self::OPTION_LABEL_PLUS_MINUS => $lng->txt('option_label_plus_minus'),
536  self::OPTION_LABEL_APPLICABLE_OR_NOT => $lng->txt('option_label_applicable_or_not'),
537  self::OPTION_LABEL_ADEQUATE_OR_NOT => $lng->txt('option_label_adequate_or_not'),
538  self::OPTION_LABEL_CUSTOM => $lng->txt('option_label_custom')
539  );
540  }
541 
542  public function isValidOptionLabel($optionLabel): bool
543  {
544  $validLabels = $this->getValidOptionLabels();
545  return in_array($optionLabel, $validLabels);
546  }
547 
549  {
550  switch ($optionLabel) {
551  case self::OPTION_LABEL_RIGHT_WRONG:
552  return $lng->txt('option_label_right');
553 
554  case self::OPTION_LABEL_PLUS_MINUS:
555  return $lng->txt('option_label_plus');
556 
557  case self::OPTION_LABEL_APPLICABLE_OR_NOT:
558  return $lng->txt('option_label_applicable');
559 
560  case self::OPTION_LABEL_ADEQUATE_OR_NOT:
561  return $lng->txt('option_label_adequate');
562 
563  case self::OPTION_LABEL_CUSTOM:
564  default:
565  return $this->getCustomTrueOptionLabel();
566  }
567  }
568 
570  {
571  switch ($optionLabel) {
572  case self::OPTION_LABEL_RIGHT_WRONG:
573  return $lng->txt('option_label_wrong');
574 
575  case self::OPTION_LABEL_PLUS_MINUS:
576  return $lng->txt('option_label_minus');
577 
578  case self::OPTION_LABEL_APPLICABLE_OR_NOT:
579  return $lng->txt('option_label_not_applicable');
580 
581  case self::OPTION_LABEL_ADEQUATE_OR_NOT:
582  return $lng->txt('option_label_not_adequate');
583 
584  case self::OPTION_LABEL_CUSTOM:
585  default:
586  return $this->getCustomFalseOptionLabel();
587  }
588  }
589 
591  {
592  return sprintf(
593  $lng->txt('kprim_instruction_text'),
596  );
597  }
598 
599  public function isCustomOptionLabel($labelValue): bool
600  {
601  return $labelValue == self::OPTION_LABEL_CUSTOM;
602  }
603 
604  public function getThumbPrefix(): string
605  {
606  return self::THUMB_PREFIX;
607  }
608 
609  public function rebuildThumbnails(): void
610  {
611  if ($this->isSingleLineAnswerType($this->getAnswerType()) && $this->getThumbSize()) {
612  foreach ($this->getAnswers() as $answer) {
613  if (strlen($answer->getImageFile())) {
614  $this->generateThumbForFile($answer->getImageFsDir(), $answer->getImageFile());
615  }
616  }
617  }
618  }
619 
620  protected function generateThumbForFile($path, $file): void
621  {
622  $filename = $path . $file;
623  if (@file_exists($filename)) {
624  $thumbpath = $path . $this->getThumbPrefix() . $file;
625  $path_info = @pathinfo($filename);
626  $ext = "";
627  switch (strtoupper($path_info['extension'])) {
628  case 'PNG':
629  $ext = 'PNG';
630  break;
631  case 'GIF':
632  $ext = 'GIF';
633  break;
634  default:
635  $ext = 'JPEG';
636  break;
637  }
638  ilShellUtil::convertImage($filename, $thumbpath, $ext, (string) $this->getThumbSize());
639  }
640  }
641 
642  public function handleFileUploads($answers, $files): void
643  {
644  foreach ($answers as $answer) {
645  /* @var ilAssKprimChoiceAnswer $answer */
646 
647  if (!isset($files[$answer->getPosition()])) {
648  continue;
649  }
650 
651  $this->handleFileUpload($answer, $files[$answer->getPosition()]);
652  }
653  }
654 
655  private function handleFileUpload(ilAssKprimChoiceAnswer $answer, $fileData): int
656  {
657  $imagePath = $this->getImagePath();
658 
659  if (!file_exists($imagePath)) {
660  ilFileUtils::makeDirParents($imagePath);
661  }
662 
663  $filename = $this->buildHashedImageFilename($fileData['name'], true);
664 
665  $answer->setImageFsDir($imagePath);
666  $answer->setImageFile($filename);
667 
668  if (!ilFileUtils::moveUploadedFile($fileData['tmp_name'], $fileData['name'], $answer->getImageFsPath())) {
669  return 2;
670  }
671 
672  return 0;
673  }
674 
675  public function removeAnswerImage($position): void
676  {
677  $answer = $this->getAnswer($position);
678 
679  if (file_exists($answer->getImageFsPath())) {
680  ilFileUtils::delDir($answer->getImageFsPath());
681  }
682 
683  if (file_exists($answer->getThumbFsPath())) {
684  ilFileUtils::delDir($answer->getThumbFsPath());
685  }
686 
687  $answer->setImageFile(null);
688  }
689 
690  protected function getSolutionSubmit(): array
691  {
692  $solutionSubmit = [];
693  $post = $this->dic->http()->wrapper()->post();
694 
695  foreach ($this->getAnswers() as $index => $a) {
696  if ($post->has("kprim_choice_result_$index")) {
697  $value = $post->retrieve(
698  "kprim_choice_result_$index",
699  $this->dic->refinery()->kindlyTo()->string()
700  );
701  if (is_numeric($value)) {
702  $solutionSubmit[] = $value;
703  }
704  }
705  }
706  return $solutionSubmit;
707  }
708 
709  protected function calculateReachedPointsForSolution($found_values, $active_id = 0)
710  {
711  $numCorrect = 0;
712  if ($found_values == null) {
713  $found_values = [];
714  }
715  foreach ($this->getAnswers() as $key => $answer) {
716  if (!isset($found_values[$answer->getPosition()])) {
717  continue;
718  }
719 
720  if ($found_values[$answer->getPosition()] == $answer->getCorrectness()) {
721  $numCorrect++;
722  }
723  }
724 
725  if ($numCorrect >= self::NUM_REQUIRED_ANSWERS) {
726  $points = $this->getPoints();
727  } elseif ($this->isScorePartialSolutionEnabled() && $numCorrect >= self::PARTIAL_SCORING_NUM_CORRECT_ANSWERS) {
728  $points = $this->getPoints() / 2;
729  } else {
730  $points = 0;
731  }
732 
733  if ($active_id) {
734  if (count($found_values) == 0) {
735  $points = 0;
736  }
737  }
738  return $points;
739  }
740 
741  public function duplicate(bool $for_test = true, string $title = "", string $author = "", string $owner = "", $testObjId = null): int
742  {
743  if ($this->id <= 0) {
744  // The question has not been saved. It cannot be duplicated
745  return -1;
746  }
747  // duplicate the question in database
748  $this_id = $this->getId();
749  $thisObjId = $this->getObjId();
750 
751  $clone = $this;
752  include_once("./Modules/TestQuestionPool/classes/class.assQuestion.php");
754  $clone->id = -1;
755 
756  if ((int) $testObjId > 0) {
757  $clone->setObjId($testObjId);
758  }
759 
760  if ($title) {
761  $clone->setTitle($title);
762  }
763 
764  if ($author) {
765  $clone->setAuthor($author);
766  }
767  if ($owner) {
768  $clone->setOwner($owner);
769  }
770 
771  if ($for_test) {
772  $clone->saveToDb($original_id);
773  } else {
774  $clone->saveToDb();
775  }
776 
777  // copy question page content
778  $clone->copyPageOfQuestion($this_id);
779  // copy XHTML media objects
780  $clone->copyXHTMLMediaObjectsOfQuestion($this_id);
781  // duplicate the images
782  $clone->cloneAnswerImages($this_id, $thisObjId, $clone->getId(), $clone->getObjId());
783 
784  $clone->onDuplicate($thisObjId, $this_id, $clone->getObjId(), $clone->getId());
785 
786  return $clone->id;
787  }
788 
789  public function createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle = ""): int
790  {
791  if ($this->getId() <= 0) {
792  throw new RuntimeException('The question has not been saved. It cannot be duplicated');
793  }
794 
795  include_once("./Modules/TestQuestionPool/classes/class.assQuestion.php");
796 
797  $sourceQuestionId = $this->id;
798  $sourceParentId = $this->getObjId();
799 
800  // duplicate the question in database
801  $clone = $this;
802  $clone->id = -1;
803 
804  $clone->setObjId($targetParentId);
805 
806  if ($targetQuestionTitle) {
807  $clone->setTitle($targetQuestionTitle);
808  }
809 
810  $clone->saveToDb();
811  // copy question page content
812  $clone->copyPageOfQuestion($sourceQuestionId);
813  // copy XHTML media objects
814  $clone->copyXHTMLMediaObjectsOfQuestion($sourceQuestionId);
815  // duplicate the image
816  $clone->cloneAnswerImages($sourceQuestionId, $sourceParentId, $clone->getId(), $clone->getObjId());
817 
818  $clone->onCopy($sourceParentId, $sourceQuestionId, $targetParentId, $clone->getId());
819 
820  return $clone->id;
821  }
822 
826  public function copyObject($target_questionpool_id, $title = ""): int
827  {
828  if ($this->getId() <= 0) {
829  throw new RuntimeException('The question has not been saved. It cannot be duplicated');
830  }
831  // duplicate the question in database
832  $clone = $this;
833  include_once("./Modules/TestQuestionPool/classes/class.assQuestion.php");
835  $clone->id = -1;
836  $source_questionpool_id = $this->getObjId();
837  $clone->setObjId($target_questionpool_id);
838  if ($title) {
839  $clone->setTitle($title);
840  }
841  $clone->saveToDb();
842  // copy question page content
843  $clone->copyPageOfQuestion($original_id);
844  // copy XHTML media objects
845  $clone->copyXHTMLMediaObjectsOfQuestion($original_id);
846  // duplicate the image
847  $clone->cloneAnswerImages($original_id, $source_questionpool_id, $clone->getId(), $clone->getObjId());
848 
849  $clone->onCopy($source_questionpool_id, $original_id, $clone->getObjId(), $clone->getId());
850 
851  return $clone->id;
852  }
853 
854  protected function beforeSyncWithOriginal($origQuestionId, $dupQuestionId, $origParentObjId, $dupParentObjId): void
855  {
856  parent::beforeSyncWithOriginal($origQuestionId, $dupQuestionId, $origParentObjId, $dupParentObjId);
857 
858  $question = self::instantiateQuestion($origQuestionId);
859 
860  foreach ($question->getAnswers() as $answer) {
861  $question->removeAnswerImage($answer->getPosition());
862  }
863  }
864 
865  protected function afterSyncWithOriginal($origQuestionId, $dupQuestionId, $origParentObjId, $dupParentObjId): void
866  {
867  parent::afterSyncWithOriginal($origQuestionId, $dupQuestionId, $origParentObjId, $dupParentObjId);
868 
869  $this->cloneAnswerImages($dupQuestionId, $dupParentObjId, $origQuestionId, $origParentObjId);
870  }
871 
872  protected function cloneAnswerImages($sourceQuestionId, $sourceParentId, $targetQuestionId, $targetParentId): void
873  {
875  global $DIC;
876  $ilLog = $DIC['ilLog'];
877 
878  $sourcePath = $this->buildImagePath($sourceQuestionId, $sourceParentId);
879  $targetPath = $this->buildImagePath($targetQuestionId, $targetParentId);
880 
881  foreach ($this->getAnswers() as $answer) {
882  $filename = $answer->getImageFile();
883 
884  if (strlen($filename)) {
885  if (!file_exists($targetPath)) {
886  ilFileUtils::makeDirParents($targetPath);
887  }
888 
889  if (file_exists($sourcePath . $filename)) {
890  if (!copy($sourcePath . $filename, $targetPath . $filename)) {
891  $ilLog->warning(sprintf(
892  "Could not clone source image '%s' to '%s' (srcQuestionId: %s|tgtQuestionId: %s|srcParentObjId: %s|tgtParentObjId: %s)",
893  $sourcePath . $filename,
894  $targetPath . $filename,
895  $sourceQuestionId,
896  $targetQuestionId,
897  $sourceParentId,
898  $targetParentId
899  ));
900  }
901  }
902 
903  if (file_exists($sourcePath . $this->getThumbPrefix() . $filename)) {
904  if (!copy($sourcePath . $this->getThumbPrefix() . $filename, $targetPath . $this->getThumbPrefix() . $filename)) {
905  $ilLog->warning(sprintf(
906  "Could not clone thumbnail source image '%s' to '%s' (srcQuestionId: %s|tgtQuestionId: %s|srcParentObjId: %s|tgtParentObjId: %s)",
907  $sourcePath . $this->getThumbPrefix() . $filename,
908  $targetPath . $this->getThumbPrefix() . $filename,
909  $sourceQuestionId,
910  $targetQuestionId,
911  $sourceParentId,
912  $targetParentId
913  ));
914  }
915  }
916  }
917  }
918  }
919 
920  protected function getRTETextWithMediaObjects(): string
921  {
922  $combinedText = parent::getRTETextWithMediaObjects();
923 
924  foreach ($this->getAnswers() as $answer) {
925  $combinedText .= $answer->getAnswertext();
926  }
927 
928  return $combinedText;
929  }
930 
935  {
936  foreach ($this->getAnswers() as $answer) {
937  /* @var ilAssKprimChoiceAnswer $answer */
938  $answer->setAnswertext($migrator->migrateToLmContent($answer->getAnswertext()));
939  }
940  }
941 
945  public function toJSON(): string
946  {
947  $this->lng->loadLanguageModule('assessment');
948 
949  require_once './Services/RTE/classes/class.ilRTE.php';
950  $result = array();
951  $result['id'] = $this->getId();
952  $result['type'] = $this->getQuestionType();
953  $result['title'] = $this->getTitleForHTMLOutput();
954  $result['question'] = $this->formatSAQuestion($this->getQuestion());
955  $result['instruction'] = $this->getInstructionTextTranslation(
956  $this->lng,
957  $this->getOptionLabel()
958  );
959  $result['nr_of_tries'] = $this->getNrOfTries();
960  $result['shuffle'] = $this->isShuffleAnswersEnabled();
961  $result['feedback'] = array(
962  'onenotcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), false)),
963  'allcorrect' => $this->formatSAQuestion($this->feedbackOBJ->getGenericFeedbackTestPresentation($this->getId(), true))
964  );
965 
966  $result['trueOptionLabel'] = $this->getTrueOptionLabelTranslation($this->lng, $this->getOptionLabel());
967  $result['falseOptionLabel'] = $this->getFalseOptionLabelTranslation($this->lng, $this->getOptionLabel());
968 
969  $result['num_allowed_failures'] = $this->getNumAllowedFailures();
970 
971  $answers = array();
972  $has_image = false;
973 
974  foreach ($this->getAnswers() as $key => $answer) {
975  if (strlen((string) $answer->getImageFile())) {
976  $has_image = true;
977  }
978 
979  $answers[] = array(
980  'answertext' => $this->formatSAQuestion($answer->getAnswertext() ?? ''),
981  'correctness' => (bool) $answer->getCorrectness(),
982  'order' => (int) $answer->getPosition(),
983  'image' => (string) $answer->getImageFile(),
984  'feedback' => $this->formatSAQuestion(
985  $this->feedbackOBJ->getSpecificAnswerFeedbackExportPresentation($this->getId(), 0, $key)
986  )
987  );
988  }
989 
990  $result['answers'] = $answers;
991 
992  if ($has_image) {
993  $result['path'] = $this->getImagePathWeb();
994  $result['thumb'] = $this->getThumbSize();
995  }
996 
997  $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId());
998  $result['mobs'] = $mobs;
999 
1000  return json_encode($result);
1001  }
1002 
1003  private function getNumAllowedFailures(): int
1004  {
1005  if ($this->isScorePartialSolutionEnabled()) {
1006  return self::NUM_REQUIRED_ANSWERS - self::PARTIAL_SCORING_NUM_CORRECT_ANSWERS;
1007  }
1008 
1009  return 0;
1010  }
1011 
1013  {
1014  return 'feedback_correct_kprim';
1015  }
1016 
1017  public static function isObligationPossible(int $questionId): bool
1018  {
1019  return true;
1020  }
1021 
1022  public function isAnswered(int $active_id, int $pass): bool
1023  {
1024  $numExistingSolutionRecords = assQuestion::getNumExistingSolutionRecords($active_id, $pass, $this->getId());
1025 
1026  return $numExistingSolutionRecords >= 4;
1027  }
1028 
1032  public function setExportDetailsXLS(ilAssExcelFormatHelper $worksheet, int $startrow, int $active_id, int $pass): int
1033  {
1034  parent::setExportDetailsXLS($worksheet, $startrow, $active_id, $pass);
1035 
1036  $solution = $this->getSolutionValues($active_id, $pass);
1037 
1038  $i = 1;
1039  foreach ($this->getAnswers() as $id => $answer) {
1040  $worksheet->setCell($startrow + $i, 0, $answer->getAnswertext());
1041  $worksheet->setBold($worksheet->getColumnCoord(0) . ($startrow + $i));
1042  $correctness = false;
1043  foreach ($solution as $solutionvalue) {
1044  if ($id == $solutionvalue['value1']) {
1045  $correctness = $solutionvalue['value2'];
1046  break;
1047  }
1048  }
1049  $worksheet->setCell($startrow + $i, 2, $correctness);
1050  $i++;
1051  }
1052 
1053  return $startrow + $i + 1;
1054  }
1055 
1056  public function moveAnswerDown($position): bool
1057  {
1058  if ($position < 0 || $position >= (self::NUM_REQUIRED_ANSWERS - 1)) {
1059  return false;
1060  }
1061 
1062  for ($i = 0, $max = count($this->answers); $i < $max; $i++) {
1063  if ($i == $position) {
1064  $movingAnswer = $this->answers[$i];
1065  $targetAnswer = $this->answers[ $i + 1 ];
1066 
1067  $movingAnswer->setPosition($position + 1);
1068  $targetAnswer->setPosition($position);
1069 
1070  $this->answers[ $i + 1 ] = $movingAnswer;
1071  $this->answers[$i] = $targetAnswer;
1072  }
1073  }
1074  return true;
1075  }
1076 
1077  public function moveAnswerUp($position): bool
1078  {
1079  if ($position <= 0 || $position > (self::NUM_REQUIRED_ANSWERS - 1)) {
1080  return false;
1081  }
1082 
1083  for ($i = 0, $max = count($this->answers); $i < $max; $i++) {
1084  if ($i == $position) {
1085  $movingAnswer = $this->answers[$i];
1086  $targetAnswer = $this->answers[ $i - 1 ];
1087 
1088  $movingAnswer->setPosition($position - 1);
1089  $targetAnswer->setPosition($position);
1090 
1091  $this->answers[ $i - 1 ] = $movingAnswer;
1092  $this->answers[$i] = $targetAnswer;
1093  }
1094  }
1095 
1096  return true;
1097  }
1098 }
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.
saveAnswerSpecificDataToDb()
Saves the answer specific records into a question types answer table.
$mobs
Definition: imgupload.php:70
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)
static _getOriginalId(int $question_id)
static getNumExistingSolutionRecords(int $activeId, int $pass, int $questionId)
copyObject($target_questionpool_id, $title="")
Copies an assMultipleChoice object.
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)
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
setOptionLabel($optionLabel)
duplicate(bool $for_test=true, string $title="", string $author="", string $owner="", $testObjId=null)
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.
buildHashedImageFilename(string $plain_image_filename, bool $unique=false)
isSingleLineAnswerType($answerType)
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
createNewOriginalFromThisDuplicate($targetParentId, $targetQuestionTitle="")
setExportDetailsXLS(ilAssExcelFormatHelper $worksheet, int $startrow, int $active_id, int $pass)
{}
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
if(!defined('PATH_SEPARATOR')) $GLOBALS['_PEAR_default_error_mode']
Definition: PEAR.php:64
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 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="-")
$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)
__construct(Container $dic, ilPlugin $plugin)
setSpecificFeedbackSetting($specificFeedbackSetting)
setOriginalId(?int $original_id)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
buildImagePath($questionId, $parentObjectId)
setTitle(string $title="")
generateThumbForFile($path, $file)
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)
getInstructionTextTranslation(ilLanguage $lng, $optionLabel)
saveToDb($originalId='')
isAnswered(int $active_id, int $pass)
$i
Definition: metadata.php:41
toJSON()
Returns a JSON representation of the question.
afterSyncWithOriginal($origQuestionId, $dupQuestionId, $origParentObjId, $dupParentObjId)
setQuestion(string $question="")