ILIAS  trunk Revision v11.0_alpha-2638-g80c1d007f79
class.assQuestion.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
40 use ILIAS\Notes\InternalDataService as NotesInternalDataService;
41 use ILIAS\Notes\NoteDBRepository as NotesRepo;
47 
52 abstract class assQuestion implements Question
53 {
54  protected const HAS_SPECIFIC_FEEDBACK = true;
55 
56  public const ADDITIONAL_CONTENT_EDITING_MODE_RTE = 'default';
57  public const ADDITIONAL_CONTENT_EDITING_MODE_IPE = 'pageobject';
58 
59  private const DEFAULT_THUMB_SIZE = 150;
60  private const MINIMUM_THUMB_SIZE = 20;
61  private const MAXIMUM_THUMB_SIZE = 8192;
62  public const TRIM_PATTERN = '/^[\p{C}\p{Z}]+|[\p{C}\p{Z}]+$/u';
63 
64  protected static $force_pass_results_update_enabled = false;
65 
69  protected \ilAssQuestionProcessLocker $processLocker;
72 
73  protected ILIAS $ilias;
75  protected ilLanguage $lng;
76  protected ilDBInterface $db;
79  protected HTTPServices $http;
80  protected Refinery $refinery;
82  protected LoggingServices $log;
83  protected Container $dic;
84 
86  protected \ilAssQuestionLifecycle $lifecycle;
87  public \ilAssQuestionFeedback $feedbackOBJ;
88  protected \ilAssQuestionPage $page;
90 
91  protected int $id;
92  protected string $title;
93  protected string $comment;
94  protected int $owner;
95  protected string $author;
96  protected int $thumb_size;
97  protected string $question;
98  protected float $points = 0.0;
99  protected bool $shuffle = true;
100  protected int $test_id;
101  protected int $obj_id = 0;
102  protected ?int $original_id = null;
103  private int $nr_of_tries;
104  protected ?int $lastChange = null;
105  private string $export_image_path;
106  protected ?string $external_id = null;
107  private string $additionalContentEditingMode = '';
108  public bool $prevent_rte_usage = false;
109  public bool $selfassessmenteditingmode = false;
110  public int $defaultnroftries = 0;
111  public string $questionActionCmd = 'handleQuestionAction';
112  protected ?int $step = null;
113 
117  protected array $suggested_solutions;
118 
119  public function __construct(
120  string $title = "",
121  string $comment = "",
122  string $author = "",
123  int $owner = -1,
124  string $question = ""
125  ) {
127  global $DIC;
128  $this->dic = $DIC;
129  $lng = $DIC['lng'];
130  $tpl = $DIC['tpl'];
131  $ilDB = $DIC['ilDB'];
132  $ilLog = $DIC->logger();
133  $local_dic = QuestionPoolDIC::dic();
134  $this->questionrepository = $local_dic['question.general_properties.repository'];
135  $this->questionpool_request = $local_dic['request_data_collector'];
136  $this->question_files = $local_dic['question_files'];
137  $this->suggestedsolution_repo = $local_dic['question.repo.suggestedsolutions'];
138  $this->current_user = $DIC['ilUser'];
139  $this->lng = $lng;
140  $this->tpl = $tpl;
141  $this->db = $ilDB;
142  $this->log = $ilLog;
143  $this->http = $DIC->http();
144  $this->refinery = $DIC->refinery();
145 
146  $this->thumb_size = self::DEFAULT_THUMB_SIZE;
147 
148  $this->title = $title;
149  $this->comment = $comment;
150  $this->setAuthor($author);
151  $this->setOwner($owner);
152 
153  $this->setQuestion($question);
154 
155  $this->id = -1;
156  $this->test_id = -1;
157  $this->suggested_solutions = [];
158  $this->shuffle = true;
159  $this->nr_of_tries = 0;
160  $this->setExternalId(null);
161 
162  $this->questionActionCmd = 'handleQuestionAction';
163  $this->export_image_path = '';
164  $this->shuffler = $DIC->refinery()->random()->dontShuffle();
165  $this->lifecycle = ilAssQuestionLifecycle::getDraftInstance();
166  $this->skillUsageService = $DIC->skills()->usage();
167 
168  $this->test_result_repository = TestDIC::dic()['results.data.repository'];
169  }
170 
171  abstract public function getQuestionType(): string;
172 
173  abstract public function isComplete(): bool;
174 
175  abstract public function saveWorkingData(int $active_id, ?int $pass = null, bool $authorized = true): bool;
176 
177  abstract public function calculateReachedPoints(
178  int $active_id,
179  ?int $pass = null,
180  bool $authorized_solution = true
181  ): float;
182 
183  abstract public function getAdditionalTableName(): string;
184  abstract public function getAnswerTableName(): string|array;
185 
194  abstract public function toLog(AdditionalInformationGenerator $additional_info): array;
195 
204  abstract protected function solutionValuesToLog(
205  AdditionalInformationGenerator $additional_info,
206  array $solution_values
207  ): array|string;
208 
213  abstract protected function solutionValuesToText(
214  array $solution_values
215  ): array|string;
216 
218  {
219  self::$force_pass_results_update_enabled = $force_pass_results_update_enabled;
220  }
221 
222  public static function isForcePassResultUpdateEnabled(): bool
223  {
224  return self::$force_pass_results_update_enabled;
225  }
226 
227  protected function getQuestionAction(): string
228  {
229  return $this->questionpool_request->getCmdIndex($this->questionActionCmd) ?? '';
230  }
231 
232  protected function isNonEmptyItemListPostSubmission(string $post_submission_field_name): bool
233  {
234  return !empty($this->questionpool_request->strArray($post_submission_field_name));
235  }
236 
237  public function getCurrentUser(): ilObjUser
238  {
239  return $this->current_user;
240  }
241 
242  public function getShuffler(): Transformation
243  {
244  return $this->shuffler;
245  }
246 
247  public function setShuffler(Transformation $shuffler): void
248  {
249  $this->shuffler = $shuffler;
250  }
251 
252  public function setProcessLocker(ilAssQuestionProcessLocker $processLocker): void
253  {
254  $this->processLocker = $processLocker;
255  }
256 
258  {
259  return $this->processLocker;
260  }
261 
262  final public function fromXML(
263  string $importdirectory,
264  int $user_id,
265  ilQTIItem $item,
266  int $questionpool_id,
267  ?int $tst_id,
268  ?ilObject &$tst_object,
269  int &$question_counter,
270  array $import_mapping
271  ): array {
272  $classname = $this->getQuestionType() . "Import";
273  $import = new $classname($this);
274  $new_import_mapping = $import->fromXML(
275  $importdirectory,
276  $user_id,
277  $item,
278  $questionpool_id,
279  $tst_id,
280  $tst_object,
281  $question_counter,
282  $import_mapping
283  );
284  return $new_import_mapping;
285  }
286 
292  final public function toXML(
293  bool $a_include_header = true,
294  bool $a_include_binary = true,
295  bool $a_shuffle = false,
296  bool $test_output = false,
297  bool $force_image_references = false
298  ): string {
299  $classname = $this->getQuestionType() . "Export";
300  $export = new $classname($this);
301  return $export->toXML($a_include_header, $a_include_binary, $a_shuffle, $test_output, $force_image_references);
302  }
303 
304  public function setTitle(string $title = ""): void
305  {
306  $this->title = $title;
307  }
308 
309  public function setId(int $id = -1): void
310  {
311  $this->id = $id;
312  }
313 
314  public function setTestId(int $id = -1): void
315  {
316  $this->test_id = $id;
317  }
318 
319  public function setComment(string $comment = ""): void
320  {
321  $this->comment = $comment;
322  }
323 
324  public function setShuffle(?bool $shuffle = true): void
325  {
326  $this->shuffle = $shuffle ?? false;
327  }
328 
329  public function setAuthor(string $author = ""): void
330  {
331  if ($author === '') {
332  $author = $this->current_user->getFullname();
333  }
334  $this->author = $author;
335  }
336 
337  public function setOwner(int $owner = -1): void
338  {
339  $this->owner = $owner;
340  }
341 
342  public function getTitle(): string
343  {
344  return $this->title;
345  }
346 
347  public function getTitleForHTMLOutput(): string
348  {
349  return $this->refinery->encode()->htmlSpecialCharsAsEntities()->transform($this->title);
350  }
351 
352  public function getTitleFilenameCompliant(): string
353  {
354  return ilFileUtils::getASCIIFilename($this->getTitle());
355  }
356 
357  public function getId(): int
358  {
359  return $this->id;
360  }
361 
362  public function getShuffle(): bool
363  {
364  return $this->shuffle;
365  }
366 
367  public function getTestId(): int
368  {
369  return $this->test_id;
370  }
371 
372  public function getComment(): string
373  {
374  return $this->comment;
375  }
376 
377  public function getDescriptionForHTMLOutput(): string
378  {
379  return $this->refinery->encode()->htmlSpecialCharsAsEntities()->transform($this->comment);
380  }
381 
382  public function getThumbSize(): int
383  {
384  return $this->thumb_size;
385  }
386 
387  public function setThumbSize(int $a_size): void
388  {
389  if ($a_size >= self::MINIMUM_THUMB_SIZE) {
390  $this->thumb_size = $a_size;
391  } else {
392  throw new ilException("Thumb size must be at least " . self::MINIMUM_THUMB_SIZE . "px");
393  }
394  }
395 
396  public function getMinimumThumbSize(): int
397  {
398  return self::MINIMUM_THUMB_SIZE;
399  }
400 
401  public function getMaximumThumbSize(): int
402  {
403  return self::MAXIMUM_THUMB_SIZE;
404  }
405 
406  public function getAuthor(): string
407  {
408  return $this->author;
409  }
410 
411  public function getAuthorForHTMLOutput(): string
412  {
413  return $this->refinery->string()->stripTags()->transform($this->author);
414  }
415 
416  public function getOwner(): int
417  {
418  return $this->owner;
419  }
420 
421  public function getObjId(): int
422  {
423  return $this->obj_id;
424  }
425 
426  public function setObjId(int $obj_id = 0): void
427  {
428  $this->obj_id = $obj_id;
429  }
430 
432  {
433  return $this->lifecycle;
434  }
435 
436  public function setLifecycle(ilAssQuestionLifecycle $lifecycle): void
437  {
438  $this->lifecycle = $lifecycle;
439  }
440 
441  public function setExternalId(?string $external_id): void
442  {
443  $this->external_id = $external_id;
444  }
445 
446  public function getExternalId(): string
447  {
448  if ($this->external_id === null || $this->external_id === '') {
449  if ($this->getId() > 0) {
450  return 'il_' . IL_INST_ID . '_qst_' . $this->getId();
451  }
452  return uniqid('', true);
453  }
454  return $this->external_id;
455  }
456 
457  public static function _getSuggestedSolutionOutput(int $question_id): string
458  {
459  $question = self::instantiateQuestion($question_id);
460  if (!is_object($question)) {
461  return "";
462  }
463  return $question->getSuggestedSolutionOutput();
464  }
465 
466  public function getSuggestedSolutionOutput(): string
467  {
468  $output = [];
469  foreach ($this->suggested_solutions as $solution) {
470  switch ($solution->getType()) {
471  case SuggestedSolution::TYPE_LM:
472  case SuggestedSolution::TYPE_LM_CHAPTER:
473  case SuggestedSolution::TYPE_LM_PAGE:
474  case SuggestedSolution::TYPE_GLOSARY_TERM:
475  $output[] = '<a href="'
476  . $this->getInternalLinkHref($solution->getInternalLink())
477  . '">'
478  . $this->lng->txt("solution_hint")
479  . '</a>';
480  break;
481 
482  case SuggestedSolution::TYPE_FILE:
483  $possible_texts = array_values(
484  array_filter(
485  [
486  ilLegacyFormElementsUtil::prepareFormOutput($solution->getTitle()),
487  ilLegacyFormElementsUtil::prepareFormOutput($solution->getFilename()),
488  $this->lng->txt('tst_show_solution_suggested')
489  ]
490  )
491  );
492 
494  $path_to_solution = $this->getSuggestedSolutionPathWeb() . $solution->getFilename();
495  if (!file_exists($path_to_solution)) {
496  break;
497  }
498  $output[] = '<a href="'
499  . ilWACSignedPath::signFile($path_to_solution)
500  . '">'
501  . $possible_texts[0]
502  . '</a>';
503  break;
504  }
505  }
506  return implode("<br />", $output);
507  }
508 
509  public function getSuggestedSolutions(): array
510  {
512  }
513 
514  public static function _getReachedPoints(int $active_id, int $question_id, int $pass): float
515  {
516  global $DIC;
517  $ilDB = $DIC['ilDB'];
518 
519  $points = 0.0;
520 
521  $result = $ilDB->queryF(
522  "SELECT * FROM tst_test_result WHERE active_fi = %s AND question_fi = %s AND pass = %s",
523  ['integer','integer','integer'],
524  [$active_id, $question_id, $pass]
525  );
526  if ($result->numRows() == 1) {
527  $row = $ilDB->fetchAssoc($result);
528  $points = (float) $row["points"];
529  }
530  return $points;
531  }
532 
533  public function getReachedPoints(int $active_id, int $pass): float
534  {
535  return round(self::_getReachedPoints($active_id, $this->getId(), $pass), 2);
536  }
537 
538  public function getMaximumPoints(): float
539  {
540  return $this->points;
541  }
542 
543  final public function getAdjustedReachedPoints(int $active_id, int $pass, bool $authorized_solution = true): float
544  {
545  // determine reached points for submitted solution
546  $reached_points = $this->calculateReachedPoints($active_id, $pass, $authorized_solution);
547  // adjust reached points regarding to tests scoring options
548  $reached_points = $this->adjustReachedPointsByScoringOptions($reached_points, $active_id);
549 
550  return $reached_points;
551  }
552 
556  final public function calculateResultsFromSolution(int $active_id, int $pass): void
557  {
558  // determine reached points for submitted solution
559  $reached_points = $this->calculateReachedPoints($active_id, $pass);
560 
561  // adjust reached points regarding to tests scoring options
562  $reached_points = $this->adjustReachedPointsByScoringOptions($reached_points, $active_id);
563 
564  if (is_null($reached_points)) {
565  $reached_points = 0.0;
566  }
567 
568  // fau: testNav - check for existing authorized solution to know if a result record should be written
569  $existing_solutions = $this->lookupForExistingSolutions($active_id, $pass);
570 
571  $this->getProcessLocker()->executeUserQuestionResultUpdateOperation(
572  function () use ($active_id, $pass, $reached_points, $existing_solutions) {
573  $query = "
574  DELETE FROM tst_test_result
575 
576  WHERE active_fi = %s
577  AND question_fi = %s
578  AND pass = %s
579  ";
580 
581  $types = ['integer', 'integer', 'integer'];
582  $values = [$active_id, $this->getId(), $pass];
583 
584  if ($this->getStep() !== null) {
585  $query .= "
586  AND step = %s
587  ";
588 
589  $types[] = 'integer';
590  $values[] = $this->getStep();
591  }
592  $this->db->manipulateF($query, $types, $values);
593 
594  if ($existing_solutions['authorized']) {
595  $next_id = $this->db->nextId("tst_test_result");
596  $fieldData = [
597  'test_result_id' => ['integer', $next_id],
598  'active_fi' => ['integer', $active_id],
599  'question_fi' => ['integer', $this->getId()],
600  'pass' => ['integer', $pass],
601  'points' => ['float', $reached_points],
602  'tstamp' => ['integer', time()],
603  'answered' => ['integer', true]
604  ];
605 
606  if ($this->getStep() !== null) {
607  $fieldData['step'] = ['integer', $this->getStep()];
608  }
609 
610  $this->db->insert('tst_test_result', $fieldData);
611  }
612  }
613  );
614 
615  $this->test_result_repository->updateTestAttemptResult($active_id, $pass, $this->getProcessLocker());
616  ilCourseObjectiveResult::_updateObjectiveResult($this->current_user->getId(), $active_id, $this->getId());
617  }
618 
623  final public function persistWorkingState(int $active_id, $pass, bool $authorized = true): bool
624  {
625  if (!$this instanceof QuestionPartiallySaveable && !$this->validateSolutionSubmit()) {
626  return false;
627  }
628 
629  $saveStatus = false;
630 
631  $this->getProcessLocker()->executePersistWorkingStateLockOperation(function () use ($active_id, $pass, $authorized, &$saveStatus) {
632  if ($pass === null) {
633  $pass = ilObjTest::_getPass($active_id);
634  }
635 
636  $saveStatus = $this->saveWorkingData($active_id, $pass, $authorized);
637 
638  if ($authorized) {
639  // fau: testNav - remove an intermediate solution if the authorized solution is saved
640  // the intermediate solution would set the displayed question status as "editing ..."
641  $this->removeIntermediateSolution($active_id, $pass);
642  // fau.
643  $this->calculateResultsFromSolution($active_id, $pass);
644  }
645  });
646 
647  return $saveStatus;
648  }
649 
653  final public function persistPreviewState(ilAssQuestionPreviewSession $preview_session): bool
654  {
655  $this->savePreviewData($preview_session);
656  return $this->validateSolutionSubmit();
657  }
658 
659  public function validateSolutionSubmit(): bool
660  {
661  return true;
662  }
663 
664  protected function savePreviewData(ilAssQuestionPreviewSession $preview_session): void
665  {
666  $preview_session->setParticipantsSolution($this->getSolutionSubmit());
667  }
668 
669  public function getSuggestedSolutionPath(): string
670  {
671  return CLIENT_WEB_DIR . "/assessment/$this->obj_id/$this->id/solution/";
672  }
673 
678  public function getImagePath($question_id = null, $object_id = null): string
679  {
680  if ($question_id === null) {
681  $question_id = $this->id;
682  }
683 
684  if ($object_id === null) {
685  $object_id = $this->obj_id;
686  }
687 
688  return $this->question_files->buildImagePath($question_id, $object_id);
689  }
690 
691  public function getSuggestedSolutionPathWeb(): string
692  {
694  . "/assessment/{$this->obj_id}/{$this->id}/solution/";
695  return str_replace(
696  ilFileUtils::removeTrailingPathSeparators(ILIAS_ABSOLUTE_PATH . '/public'),
698  $webdir
699  );
700  }
701 
707  public function getImagePathWeb(): string
708  {
709  if (!$this->export_image_path) {
711  . "/assessment/{$this->obj_id}/{$this->id}/images/";
712  return str_replace(
713  ilFileUtils::removeTrailingPathSeparators(ILIAS_ABSOLUTE_PATH . '/public'),
715  $webdir
716  );
717  }
719  }
720 
721  public function getTestOutputSolutions(int $activeId, int $pass): array
722  {
723  if ($this->getTestPresentationConfig()->isSolutionInitiallyPrefilled()) {
724  return $this->getSolutionValues($activeId, $pass, true);
725  }
726  return $this->getUserSolutionPreferingIntermediate($activeId, $pass);
727  }
728 
729 
731  int $active_id,
732  ?int $pass = null
733  ): array {
734  $solution = $this->getSolutionValues($active_id, $pass, false);
735 
736  if (!count($solution)) {
737  $solution = $this->getSolutionValues($active_id, $pass, true);
738  }
739 
740  return $solution;
741  }
742 
746  public function getSolutionValues(
747  int $active_id,
748  ?int $pass = null,
749  bool $authorized = true
750  ): array {
751  if ($pass === null && is_numeric($active_id)) {
752  $pass = $this->getSolutionMaxPass((int) $active_id);
753  }
754 
755  if ($this->getStep() !== null) {
756  $query = "
757  SELECT *
758  FROM tst_solutions
759  WHERE active_fi = %s
760  AND question_fi = %s
761  AND pass = %s
762  AND step = %s
763  AND authorized = %s
764  ORDER BY solution_id";
765 
766  $result = $this->db->queryF(
767  $query,
768  ['integer', 'integer', 'integer', 'integer', 'integer'],
769  [(int) $active_id, $this->getId(), $pass, $this->getStep(), (int) $authorized]
770  );
771  } else {
772  $query = "
773  SELECT *
774  FROM tst_solutions
775  WHERE active_fi = %s
776  AND question_fi = %s
777  AND pass = %s
778  AND authorized = %s
779  ORDER BY solution_id
780  ";
781 
782  $result = $this->db->queryF(
783  $query,
784  ['integer', 'integer', 'integer', 'integer'],
785  [(int) $active_id, $this->getId(), $pass, (int) $authorized]
786  );
787  }
788 
789  $values = [];
790 
791  while ($row = $this->db->fetchAssoc($result)) {
792  $values[] = $row;
793  }
794 
795  return $values;
796  }
797 
798  public function deleteAnswers(int $question_id): void
799  {
800  $answer_table_name = $this->getAnswerTableName();
801 
802  if (!is_array($answer_table_name)) {
803  $answer_table_name = [$answer_table_name];
804  }
805 
806  foreach ($answer_table_name as $table) {
807  if (strlen($table)) {
808  $this->db->manipulateF(
809  "DELETE FROM $table WHERE question_fi = %s",
810  ['integer'],
811  [$question_id]
812  );
813  }
814  }
815  }
816 
817  public function deleteAdditionalTableData(int $question_id): void
818  {
819  $additional_table_name = $this->getAdditionalTableName();
820 
821  if (!is_array($additional_table_name)) {
822  $additional_table_name = [$additional_table_name];
823  }
824 
825  foreach ($additional_table_name as $table) {
826  if (strlen($table)) {
827  $this->db->manipulateF(
828  "DELETE FROM $table WHERE question_fi = %s",
829  ['integer'],
830  [$question_id]
831  );
832  }
833  }
834  }
835 
836  protected function deletePageOfQuestion(int $question_id): void
837  {
838  if (ilAssQuestionPage::_exists('qpl', $question_id, "", true)) {
839  $page = new ilAssQuestionPage($question_id);
840  $page->delete();
841  }
842  }
843 
844  public function delete(int $question_id): void
845  {
846  if ($question_id < 1) {
847  return;
848  }
849 
850  $result = $this->db->queryF(
851  "SELECT obj_fi FROM qpl_questions WHERE question_id = %s",
852  ['integer'],
853  [$question_id]
854  );
855  if ($this->db->numRows($result) !== 1) {
856  return;
857  }
858 
859  $row = $this->db->fetchAssoc($result);
860  $obj_id = $row["obj_fi"];
861 
862  try {
863  $this->deletePageOfQuestion($question_id);
864  } catch (Exception $e) {
865  $this->log->root()->error("EXCEPTION: Could not delete page of question $question_id: $e");
866  return;
867  }
868 
869  $affectedRows = $this->db->manipulateF(
870  "DELETE FROM qpl_questions WHERE question_id = %s",
871  ['integer'],
872  [$question_id]
873  );
874  if ($affectedRows == 0) {
875  return;
876  }
877 
878  try {
879  $this->deleteAdditionalTableData($question_id);
880  $this->deleteAnswers($question_id);
881  $this->feedbackOBJ->deleteGenericFeedbacks($question_id, $this->isAdditionalContentEditingModePageObject());
882  $this->feedbackOBJ->deleteSpecificAnswerFeedbacks($question_id, $this->isAdditionalContentEditingModePageObject());
883  } catch (Exception $e) {
884  $this->log->root()->error("EXCEPTION: Could not delete additional table data of question {$question_id}: {$e}");
885  }
886 
887  try {
888  // delete the question in the tst_test_question table (list of test questions)
889  $affectedRows = $this->db->manipulateF(
890  "DELETE FROM tst_test_question WHERE question_fi = %s",
891  ['integer'],
892  [$question_id]
893  );
894  } catch (Exception $e) {
895  $this->log->root()->error("EXCEPTION: Could not delete delete question {$question_id} from a test: {$e}");
896  }
897 
898  try {
899  $this->getSuggestedSolutionsRepo()->deleteForQuestion($question_id);
900  } catch (Exception $e) {
901  $this->log->root()->error("EXCEPTION: Could not delete suggested solutions of question {$question_id}: {$e}");
902  }
903 
904  $directory = CLIENT_WEB_DIR . "/assessment/" . $obj_id . "/$question_id";
905  try {
906  if (is_dir($directory)) {
907  ilFileUtils::delDir($directory);
908  }
909  } catch (Exception $e) {
910  $this->log->root()->error("EXCEPTION: Could not delete question file directory {$directory} of question {$question_id}: {$e}");
911  }
912 
913  try {
914  $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $question_id);
915  // remaining usages are not in text anymore -> delete them
916  // and media objects (note: delete method of ilObjMediaObject
917  // checks whether object is used in another context; if yes,
918  // the object is not deleted!)
919  foreach ($mobs as $mob) {
920  ilObjMediaObject::_removeUsage($mob, "qpl:html", $question_id);
921  if (ilObjMediaObject::_exists($mob)) {
922  $mob_obj = new ilObjMediaObject($mob);
923  $mob_obj->delete();
924  }
925  }
926  } catch (Exception $e) {
927  $this->log->root()->error("EXCEPTION: Error deleting the media objects of question {$question_id}: {$e}");
928  }
929  $assignmentList = new ilAssQuestionSkillAssignmentList($this->db);
930  $assignmentList->setParentObjId($obj_id);
931  $assignmentList->setQuestionIdFilter($question_id);
932  $assignmentList->loadFromDb();
933  foreach ($assignmentList->getAssignmentsByQuestionId($question_id) as $assignment) {
934  /* @var ilAssQuestionSkillAssignment $assignment */
935  $assignment->deleteFromDb();
936 
937  // remove skill usage
938  if (!$assignment->isSkillUsed()) {
939  $this->skillUsageService->removeUsage(
940  $assignment->getParentObjId(),
941  $assignment->getSkillBaseId(),
942  $assignment->getSkillTrefId()
943  );
944  }
945  }
946 
947  $this->deleteTaxonomyAssignments();
948  $this->deleteComments();
949 
950  try {
952  } catch (Exception $e) {
953  $this->log->root()->error(
954  "EXCEPTION: Error updating the question pool question count of"
955  . " question pool {$this->getObjId()} when deleting question {$question_id}: {$e}"
956  );
957  }
958  }
959 
960  private function deleteTaxonomyAssignments(): void
961  {
962  $taxIds = ilObjTaxonomy::getUsageOfObject($this->getObjId());
963 
964  foreach ($taxIds as $taxId) {
965  $taxNodeAssignment = new ilTaxNodeAssignment('qpl', $this->getObjId(), 'quest', $taxId);
966  $taxNodeAssignment->deleteAssignmentsOfItem($this->getId());
967  }
968  }
969 
970  public function getTotalAnswers(): int
971  {
972  // get all question references to the question id
973  $result = $this->db->queryF(
974  "SELECT question_id FROM qpl_questions WHERE original_id = %s OR question_id = %s",
975  ['integer','integer'],
976  [$this->id, $this->id]
977  );
978  if ($this->db->numRows($result) == 0) {
979  return 0;
980  }
981  $found_id = [];
982  while ($row = $this->db->fetchAssoc($result)) {
983  $found_id[] = $row["question_id"];
984  }
985 
986  $result = $this->db->query("SELECT * FROM tst_test_result WHERE " . $this->db->in('question_fi', $found_id, false, 'integer'));
987 
988  return $this->db->numRows($result);
989  }
990 
991  public static function isFileAvailable(string $file): bool
992  {
993  if (!file_exists($file)) {
994  return false;
995  }
996 
997  if (!is_file($file)) {
998  return false;
999  }
1000 
1001  if (!is_readable($file)) {
1002  return false;
1003  }
1004 
1005  return true;
1006  }
1007 
1008  public function cloneXHTMLMediaObjectsOfQuestion(int $source_question_id): void
1009  {
1010  $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $source_question_id);
1011  foreach ($mobs as $mob) {
1012  ilObjMediaObject::_saveUsage($mob, "qpl:html", $this->getId());
1013  }
1014  }
1015 
1016  public function createPageObject(): void
1017  {
1018  $qpl_id = $this->getObjId();
1019  $this->page = new ilAssQuestionPage(0);
1020  $this->page->setId($this->getId());
1021  $this->page->setParentId($qpl_id);
1022  $this->page->setXMLContent("<PageObject><PageContent>" .
1023  "<Question QRef=\"il__qst_" . $this->getId() . "\"/>" .
1024  "</PageContent></PageObject>");
1025  $this->page->create(false);
1026  }
1027 
1028  public function clonePageOfQuestion(int $a_q_id): void
1029  {
1030  if ($a_q_id > 0) {
1031  $page = new ilAssQuestionPage($a_q_id);
1032  $page->buildDom();
1033  ilPCPlugged::handleCopiedPluggedContent($page, $page->getDomDoc());
1034  $xml = str_replace("il__qst_" . $a_q_id, "il__qst_" . $this->id, $page->getXMLFromDom());
1035  $this->page->setXMLContent($xml);
1036  $this->page->updateFromXML();
1037  }
1038  }
1039 
1040  public function getPageOfQuestion(): string
1041  {
1042  $page = new ilAssQuestionPage($this->id);
1043  return $page->getXMLContent();
1044  }
1045 
1046  public function setOriginalId(?int $original_id): void
1047  {
1048  $this->original_id = $original_id;
1049  }
1050 
1051  public function getOriginalId(): ?int
1052  {
1053  return $this->original_id;
1054  }
1055 
1056  protected static $imageSourceFixReplaceMap = [
1057  'ok.svg' => 'ok.png',
1058  'not_ok.svg' => 'not_ok.png',
1059  'object/checkbox_checked.svg' => 'checkbox_checked.png',
1060  'object/checkbox_unchecked.svg' => 'checkbox_unchecked.png',
1061  'object/radiobutton_checked.svg' => 'radiobutton_checked.png',
1062  'object/radiobutton_unchecked.svg' => 'radiobutton_unchecked.png'
1063  ];
1064 
1065  public function fixSvgToPng(string $imageFilenameContainingString): string
1066  {
1067  $needles = array_keys(self::$imageSourceFixReplaceMap);
1068  $replacements = array_values(self::$imageSourceFixReplaceMap);
1069  return str_replace($needles, $replacements, $imageFilenameContainingString);
1070  }
1071 
1072  public function fixUnavailableSkinImageSources(string $html): string
1073  {
1074  $matches = null;
1075  if (preg_match_all('/src="(.*?)"/m', $html, $matches)) {
1076  $sources = $matches[1];
1077 
1078  $needleReplacementMap = [];
1079 
1080  foreach ($sources as $src) {
1081  $file = ilFileUtils::removeTrailingPathSeparators(ILIAS_ABSOLUTE_PATH) . DIRECTORY_SEPARATOR . $src;
1082 
1083  if (file_exists($file)) {
1084  continue;
1085  }
1086 
1087  $levels = explode(DIRECTORY_SEPARATOR, $src);
1088  if (count($levels) < 5 || $levels[0] !== 'Customizing' || $levels[2] !== 'skin') {
1089  continue;
1090  }
1091 
1092  $component = '';
1093 
1094  if ($levels[4] === 'components/ILIAS' || $levels[4] === 'components/ILIAS') {
1095  $component = $levels[4] . DIRECTORY_SEPARATOR . $levels[5];
1096  }
1097 
1098  $needleReplacementMap[$src] = ilUtil::getImagePath(basename($src), $component);
1099  }
1100 
1101  if (count($needleReplacementMap)) {
1102  $html = str_replace(array_keys($needleReplacementMap), array_values($needleReplacementMap), $html);
1103  }
1104  }
1105 
1106  return $html;
1107  }
1108 
1109  public function loadFromDb(int $question_id): void
1110  {
1111  $result = $this->db->queryF(
1112  'SELECT external_id FROM qpl_questions WHERE question_id = %s',
1113  ['integer'],
1114  [$question_id]
1115  );
1116  if ($this->db->numRows($result) === 1) {
1117  $data = $this->db->fetchAssoc($result);
1118  $this->external_id = $data['external_id'];
1119  }
1120 
1121  $suggested_solutions = $this->loadSuggestedSolutions();
1122  $this->suggested_solutions = [];
1123  if ($suggested_solutions) {
1124  foreach ($suggested_solutions as $solution) {
1125  $this->suggested_solutions[$solution->getSubquestionIndex()] = $solution;
1126  }
1127  }
1128  }
1129 
1136  public function createNewQuestion(bool $a_create_page = true): int
1137  {
1138  $complete = '0';
1139  $obj_id = $this->getObjId();
1140  if ($obj_id <= 0
1141  && $this->questionpool_request->hasRefId()) {
1142  $obj_id = $this->questionpool_request->getRefId();
1143  }
1144 
1145  if ($obj_id <= 0) {
1146  $obj_id = $this->questionpool_request->int('sel_qpl');
1147  }
1148 
1149  if ($obj_id <= 0) {
1150  return $this->getId();
1151  }
1152 
1153  $tstamp = time();
1154  if ($a_create_page) {
1155  $tstamp = 0;
1156  }
1157 
1158  $next_id = $this->db->nextId('qpl_questions');
1159  $this->db->insert("qpl_questions", [
1160  "question_id" => ["integer", $next_id],
1161  "question_type_fi" => ["integer", $this->getQuestionTypeID()],
1162  "obj_fi" => ["integer", $obj_id],
1163  "title" => ["text", ''],
1164  "description" => ["text", ''],
1165  "author" => ["text", $this->getAuthor()],
1166  "owner" => ["integer", $this->current_user->getId()],
1167  "question_text" => ["clob", ''],
1168  "points" => ["float", "0.0"],
1169  "nr_of_tries" => ["integer", $this->getDefaultNrOfTries()], // #10771
1170  "complete" => ["text", $complete],
1171  "created" => ["integer", time()],
1172  "original_id" => ["integer", null],
1173  "tstamp" => ["integer", $tstamp],
1174  "external_id" => ["text", $this->getExternalId()],
1175  'add_cont_edit_mode' => ['text', $this->getAdditionalContentEditingMode()]
1176  ]);
1177  $this->setId($next_id);
1178 
1179  if ($a_create_page) {
1180  // create page object of question
1181  $this->createPageObject();
1182  }
1183 
1184  return $this->getId();
1185  }
1186 
1187  public function saveQuestionDataToDb(?int $original_id = null): void
1188  {
1189  if ($this->getId() === -1) {
1190  $next_id = $this->db->nextId('qpl_questions');
1191  $this->db->insert("qpl_questions", [
1192  "question_id" => ["integer", $next_id],
1193  "question_type_fi" => ["integer", $this->getQuestionTypeID()],
1194  "obj_fi" => ["integer", $this->getObjId()],
1195  "title" => ["text", mb_substr($this->getTitle(), 0, 124)],
1196  "description" => ["text", mb_substr($this->getComment(), 0, 1000)],
1197  "author" => ["text", mb_substr($this->getAuthor(), 0, 512)],
1198  "owner" => ["integer", $this->getOwner()],
1199  "question_text" => ["clob", ilRTE::_replaceMediaObjectImageSrc($this->getQuestion(), 0)],
1200  "points" => ["float", $this->getMaximumPoints()],
1201  "nr_of_tries" => ["integer", $this->getNrOfTries()],
1202  "created" => ["integer", time()],
1203  "original_id" => ["integer", $original_id],
1204  "tstamp" => ["integer", time()],
1205  "external_id" => ["text", $this->getExternalId()],
1206  'add_cont_edit_mode' => ['text', $this->getAdditionalContentEditingMode()]
1207  ]);
1208  $this->setId($next_id);
1209  // create page object of question
1210  $this->createPageObject();
1211  return;
1212  }
1213 
1214  // Vorhandenen Datensatz aktualisieren
1215  $this->db->update("qpl_questions", [
1216  "obj_fi" => ["integer", $this->getObjId()],
1217  "title" => ["text", mb_substr($this->getTitle(), 0, 124)],
1218  "description" => ["text", mb_substr($this->getComment(), 0, 1000)],
1219  "author" => ["text", mb_substr($this->getAuthor(), 0, 512)],
1220  "question_text" => ["clob", ilRTE::_replaceMediaObjectImageSrc($this->getQuestion(), 0)],
1221  "points" => ["float", $this->getMaximumPoints()],
1222  "nr_of_tries" => ["integer", $this->getNrOfTries()],
1223  "tstamp" => ["integer", time()],
1224  'complete' => ['integer', $this->isComplete()],
1225  "external_id" => ["text", $this->getExternalId()]
1226  ], [
1227  "question_id" => ["integer", $this->getId()]
1228  ]);
1229  }
1230 
1231  public function duplicate(
1232  bool $for_test = true,
1233  string $title = '',
1234  string $author = '',
1235  int $owner = -1,
1236  $test_obj_id = null
1237  ): int {
1238  if ($this->id <= 0) {
1239  // The question has not been saved. It cannot be duplicated
1240  return -1;
1241  }
1242 
1243  $clone = clone $this;
1244  $clone->id = -1;
1245 
1246  if ((int) $test_obj_id > 0) {
1247  $clone->setObjId($test_obj_id);
1248  }
1249 
1250  if ($title) {
1251  $clone->setTitle($title);
1252  }
1253  if ($author) {
1254  $clone->setAuthor($author);
1255  }
1256  if ($owner) {
1257  $clone->setOwner($owner);
1258  }
1259  if ($for_test) {
1260  $clone->saveToDb($this->id);
1261  } else {
1262  $clone->saveToDb();
1263  }
1264 
1265  $clone->clonePageOfQuestion($this->getId());
1266  $clone->cloneXHTMLMediaObjectsOfQuestion($this->getId());
1267 
1268  $clone = $this->cloneQuestionTypeSpecificProperties($clone);
1269 
1270  $clone->onDuplicate($this->getObjId(), $this->getId(), $clone->getObjId(), $clone->getId());
1271 
1272  return $clone->id;
1273  }
1274 
1275  final public function copyObject(
1276  int $target_parent_id,
1277  string $title = ''
1278  ): int {
1279  if ($this->getId() <= 0) {
1280  throw new RuntimeException('The question has not been saved. It cannot be duplicated');
1281  }
1282  // duplicate the question in database
1283  $clone = clone $this;
1284  $clone->id = -1;
1285  $source_parent_id = $this->getObjId();
1286  $clone->setObjId($target_parent_id);
1287  if ($title) {
1288  $clone->setTitle($title);
1289  }
1290  $clone->saveToDb();
1291  $clone->clonePageOfQuestion($this->id);
1292  $clone->cloneXHTMLMediaObjectsOfQuestion($this->id);
1293  $clone = $this->cloneQuestionTypeSpecificProperties($clone);
1294 
1295  $clone->onCopy($source_parent_id, $this->id, $clone->getObjId(), $clone->getId());
1296 
1297  return $clone->id;
1298  }
1299 
1300  final public function createNewOriginalFromThisDuplicate(
1301  int $target_parent_id,
1302  string $target_question_title = ''
1303  ): int {
1304  if ($this->getId() <= 0) {
1305  throw new RuntimeException('The question has not been saved. It cannot be duplicated');
1306  }
1307 
1308  $source_question_id = $this->id;
1309  $source_parent_id = $this->getObjId();
1310 
1311  // duplicate the question in database
1312  $clone = clone $this;
1313  $clone->id = -1;
1314 
1315  $clone->setObjId($target_parent_id);
1316 
1317  if ($target_question_title) {
1318  $clone->setTitle($target_question_title);
1319  }
1320 
1321  $clone->saveToDb();
1322  $clone->clonePageOfQuestion($source_question_id);
1323  $clone->cloneXHTMLMediaObjectsOfQuestion($source_question_id);
1324 
1325  $clone = $this->cloneQuestionTypeSpecificProperties($clone);
1326 
1327  $clone->onCopy($source_parent_id, $source_question_id, $clone->getObjId(), $clone->getId());
1328 
1329  return $clone->id;
1330  }
1331 
1333  self $target
1334  ): self {
1335  return $target;
1336  }
1337 
1338  public function saveToDb(?int $original_id = null): void
1339  {
1340  // remove unused media objects from ILIAS
1341  $this->cleanupMediaObjectUsage();
1342 
1343  $complete = "0";
1344  if ($this->isComplete()) {
1345  $complete = "1";
1346  }
1347 
1348  $this->db->update(
1349  'qpl_questions',
1350  [
1351  'tstamp' => ['integer', time()],
1352  'owner' => ['integer', $this->getOwner()],
1353  'complete' => ['integer', $complete],
1354  'lifecycle' => ['text', $this->getLifecycle()->getIdentifier()],
1355  ],
1356  [
1357  'question_id' => ['integer', $this->getId()]
1358  ]
1359  );
1360 
1362  }
1363 
1364  protected function removeAllImageFiles(string $image_target_path): void
1365  {
1366  $target = opendir($image_target_path);
1367  while ($target_file = readdir($target)) {
1368  if ($target_file === '.' || $target_file === '..') {
1369  continue;
1370  }
1371  copy(
1372  $image_target_path . DIRECTORY_SEPARATOR . $target_file,
1373  $image_target_path . DIRECTORY_SEPARATOR . $target_file
1374  );
1375  }
1376  }
1377 
1378  public static function saveOriginalId(int $questionId, int $originalId): void
1379  {
1380  global $DIC;
1381  $ilDB = $DIC->database();
1382  $query = "UPDATE qpl_questions SET tstamp = %s, original_id = %s WHERE question_id = %s";
1383 
1384  $ilDB->manipulateF(
1385  $query,
1386  ['integer','integer', 'text'],
1387  [time(), $originalId, $questionId]
1388  );
1389  }
1390 
1391  public static function resetOriginalId(int $questionId): void
1392  {
1393  global $DIC;
1394  $ilDB = $DIC->database();
1395 
1396  $query = "UPDATE qpl_questions SET tstamp = %s, original_id = NULL WHERE question_id = %s";
1397 
1398  $ilDB->manipulateF(
1399  $query,
1400  ['integer', 'text'],
1401  [time(), $questionId]
1402  );
1403  }
1404 
1405  protected function onDuplicate(
1406  int $original_parent_id,
1407  int $original_question_id,
1408  int $duplicate_parent_id,
1409  int $duplicate_question_id
1410  ): void {
1411  $this->copySuggestedSolutions($duplicate_question_id);
1412  $this->cloneSuggestedSolutionFiles($original_parent_id, $original_question_id);
1413  $this->feedbackOBJ->duplicateFeedback($original_question_id, $duplicate_question_id);
1414  $this->duplicateSkillAssignments($original_parent_id, $original_question_id, $duplicate_parent_id, $duplicate_question_id);
1415  $this->duplicateComments($original_parent_id, $original_question_id, $duplicate_parent_id, $duplicate_question_id);
1416  }
1417 
1418  protected function afterSyncWithOriginal(
1419  int $original_question_id,
1420  int $clone_question_id,
1421  int $original_parent_id,
1422  int $clone_parent_id
1423  ): void {
1424  $this->feedbackOBJ->cloneFeedback($original_question_id, $clone_question_id);
1425  }
1426 
1427  protected function onCopy(int $sourceParentId, int $sourceQuestionId, int $targetParentId, int $targetQuestionId): void
1428  {
1429  $this->copySuggestedSolutions($targetQuestionId);
1430  $this->duplicateSuggestedSolutionFiles($sourceParentId, $sourceQuestionId);
1431  $this->feedbackOBJ->duplicateFeedback($sourceQuestionId, $targetQuestionId);
1432  $this->duplicateSkillAssignments($sourceParentId, $sourceQuestionId, $targetParentId, $targetQuestionId);
1433  $this->duplicateComments($sourceParentId, $sourceQuestionId, $targetParentId, $targetQuestionId);
1434  }
1435 
1436  protected function duplicateComments(
1437  int $parent_source_id,
1438  int $source_id,
1439  int $parent_target_id,
1440  int $target_id
1441  ): void {
1442  $manager = $this->getNotesManager();
1443  $data_service = $this->getNotesDataService();
1444  $notes = $manager->getNotesForRepositoryObjIds([$parent_source_id], Note::PUBLIC);
1445  $notes = array_filter(
1446  $notes,
1447  fn($n) => $n->getContext()->getSubObjId() === $source_id
1448  );
1449 
1450  foreach ($notes as $note) {
1451  $new_context = $data_service->context(
1452  $parent_target_id,
1453  $target_id,
1454  $note->getContext()->getType()
1455  );
1456  $new_note = $data_service->note(
1457  -1,
1458  $new_context,
1459  $note->getText(),
1460  $note->getAuthor(),
1461  $note->getType(),
1462  $note->getCreationDate(),
1463  $note->getUpdateDate(),
1464  $note->getRecipient()
1465  );
1466  $manager->createNote($new_note, [], true);
1467  }
1468  }
1469 
1470  protected function deleteComments(): void
1471  {
1472  $repo = $this->getNotesRepo();
1473  $manager = $this->getNotesManager();
1474  $source_id = $this->getId();
1475  $notes = $manager->getNotesForRepositoryObjIds([$this->getObjId()], Note::PUBLIC);
1476  $notes = array_filter(
1477  $notes,
1478  fn($n) => $n->getContext()->getSubObjId() === $source_id
1479  );
1480  foreach ($notes as $note) {
1481  $repo->deleteNote($note->getId());
1482  }
1483  }
1484 
1485  protected function getNotesManager(): NotesManager
1486  {
1487  $service = new NotesService($this->dic);
1488  return $service->internal()->domain()->notes();
1489  }
1490 
1491  protected function getNotesDataService(): NotesInternalDataService
1492  {
1493  $service = new NotesService($this->dic);
1494  return $service->internal()->data();
1495  }
1496 
1497  protected function getNotesRepo(): NotesRepo
1498  {
1499  $service = new NotesService($this->dic);
1500  return $service->internal()->repo()->note();
1501  }
1502 
1503  public function deleteSuggestedSolutions(): void
1504  {
1505  $this->getSuggestedSolutionsRepo()->deleteForQuestion($this->getId());
1508  $this->suggested_solutions = [];
1509  }
1510 
1511 
1512  public function getSuggestedSolution(int $subquestion_index = 0): ?SuggestedSolution
1513  {
1514  if (array_key_exists($subquestion_index, $this->suggested_solutions)) {
1515  return $this->suggested_solutions[$subquestion_index];
1516  }
1517  return null;
1518  }
1519 
1520  protected function cloneSuggestedSolutions(
1521  int $source_question_id,
1522  int $target_question_id
1523  ): void {
1524  $this->getSuggestedSolutionsRepo()->clone($source_question_id, $target_question_id);
1525  $this->cloneSuggestedSolutionFiles($source_question_id, $target_question_id);
1526  }
1527 
1531  protected function duplicateSuggestedSolutionFiles(int $parent_id, int $question_id): void
1532  {
1533  foreach ($this->suggested_solutions as $solution) {
1534  if (!$solution->isOfTypeFile()
1535  || $solution->getFilename() === '') {
1536  continue;
1537  }
1538 
1539  $filepath = $this->getSuggestedSolutionPath();
1540  $filepath_original = str_replace(
1541  "/{$this->obj_id}/{$this->id}/solution",
1542  "/{$parent_id}/{$question_id}/solution",
1543  $filepath
1544  );
1545  if (!file_exists($filepath)) {
1546  ilFileUtils::makeDirParents($filepath);
1547  }
1548  if (!is_file($filepath_original . $solution->getFilename())
1549  || !copy($filepath_original . $solution->getFilename(), $filepath . $solution->getFilename())) {
1550  $this->log->root()->error('File for suggested solutions could not be duplicated:');
1551  $this->log->root()->error("Question-Id: {$this->id}; Question-Title: {$this->title}; File: {$filepath_original}{$solution->getFilename()}");
1552  }
1553  }
1554  }
1555 
1556  protected function cloneSuggestedSolutionFiles(
1557  int $source_question_id,
1558  int $target_question_id
1559  ): void {
1560  $filepath_target = $this->getSuggestedSolutionPath();
1561  $filepath_original = str_replace("/$target_question_id/solution", "/$source_question_id/solution", $filepath_target);
1562  ilFileUtils::delDir($filepath_original);
1563  foreach ($this->suggested_solutions as $solution) {
1564  if (!$solution->isOfTypeFile()
1565  || $solution->getFilename() === '') {
1566  continue;
1567  }
1568 
1569  if (!file_exists($filepath_original)) {
1570  ilFileUtils::makeDirParents($filepath_original);
1571  }
1572 
1573  if (!is_file($filepath_original . $solution->getFilename())
1574  || copy($filepath_target . $solution->getFilename(), $filepath_target . $solution->getFilename())) {
1575  $this->log->root()->error('File for suggested solutions could not be cloned:');
1576  $this->log->root()->error("Question-Id: {$this->id}; Question-Title: {$this->title}; File: {$filepath_original}{$solution->getFilename()}");
1577  }
1578  }
1579  }
1580 
1581  protected function copySuggestedSolutions(int $target_question_id): void
1582  {
1583  $update = [];
1584  foreach ($this->getSuggestedSolutions() as $index => $solution) {
1585  $solution = $solution->withQuestionId($target_question_id);
1586  $update[] = $solution;
1587  }
1588  $this->getSuggestedSolutionsRepo()->update($update);
1589  }
1590 
1591  public function resolveInternalLink(string $internal_link): string
1592  {
1593  if (preg_match("/il_(\d+)_(\w+)_(\d+)/", $internal_link, $matches) === false) {
1594  return $internal_link;
1595  }
1596  switch ($matches[2]) {
1597  case "lm":
1598  $resolved_link = ilLMObject::_getIdForImportId($internal_link);
1599  break;
1600  case "pg":
1601  $resolved_link = ilInternalLink::_getIdForImportId("PageObject", $internal_link);
1602  break;
1603  case "st":
1604  $resolved_link = ilInternalLink::_getIdForImportId("StructureObject", $internal_link);
1605  break;
1606  case "git":
1607  $resolved_link = ilInternalLink::_getIdForImportId("GlossaryItem", $internal_link);
1608  break;
1609  case "mob":
1610  $resolved_link = ilInternalLink::_getIdForImportId("MediaObject", $internal_link);
1611  break;
1612  }
1613  if ($resolved_link === null) {
1614  return "il__{$matches[2]}_{$matches[3]}";
1615  }
1616  return $internal_link;
1617  }
1618 
1619 
1620  //TODO: move this to import or suggested solutions repo.
1621  //use in LearningModule and Survey as well ;(
1622  public function resolveSuggestedSolutionLinks(): void
1623  {
1624  $resolvedlinks = 0;
1625  $result_pre = $this->db->queryF(
1626  "SELECT internal_link, suggested_solution_id FROM qpl_sol_sug WHERE question_fi = %s",
1627  ['integer'],
1628  [$this->getId()]
1629  );
1630  if ($this->db->numRows($result_pre) < 1) {
1631  return;
1632  }
1633 
1634  while ($row = $this->db->fetchAssoc($result_pre)) {
1635  $internal_link = $row["internal_link"];
1636  $resolved_link = $this->resolveInternalLink($internal_link);
1637  if ($internal_link === $resolved_link) {
1638  continue;
1639  }
1640  // internal link was resolved successfully
1641  $this->db->manipulateF(
1642  "UPDATE qpl_sol_sug SET internal_link = %s WHERE suggested_solution_id = %s",
1643  ['text','integer'],
1644  [$resolved_link, $row["suggested_solution_id"]]
1645  );
1646  $resolvedlinks++;
1647  }
1648  if ($resolvedlinks === 0) {
1649  return;
1650  }
1651 
1653 
1654  $result_post = $this->db->queryF(
1655  "SELECT internal_link FROM qpl_sol_sug WHERE question_fi = %s",
1656  ['integer'],
1657  [$this->getId()]
1658  );
1659  if ($this->db->numRows($result_post) < 1) {
1660  return;
1661  }
1662 
1663  while ($row = $this->db->fetchAssoc($result_post)) {
1664  if (preg_match("/il_(\d*?)_(\w+)_(\d+)/", $row["internal_link"], $matches)) {
1665  ilInternalLink::_saveLink("qst", $this->getId(), $matches[2], $matches[3], $matches[1]);
1666  }
1667  }
1668  }
1669 
1670  public function getInternalLinkHref(string $target): string
1671  {
1672  $linktypes = [
1673  "lm" => "LearningModule",
1674  "pg" => "PageObject",
1675  "st" => "StructureObject",
1676  "git" => "GlossaryItem",
1677  "mob" => "MediaObject"
1678  ];
1679  $href = "";
1680  if (preg_match("/il__(\w+)_(\d+)/", $target, $matches)) {
1681  $type = $matches[1];
1682  $target_id = $matches[2];
1683  switch ($linktypes[$matches[1]]) {
1684  case "MediaObject":
1685  $href = "./ilias.php?baseClass=ilLMPresentationGUI&obj_type=" . $linktypes[$type]
1686  . "&cmd=media&ref_id=" . $this->questionpool_request->getRefId()
1687  . "&mob_id=" . $target_id;
1688  break;
1689  case "StructureObject":
1690  case "GlossaryItem":
1691  case "PageObject":
1692  case "LearningModule":
1693  default:
1694  $href = "./goto.php?target=" . $type . "_" . $target_id;
1695  break;
1696  }
1697  }
1698  return $href;
1699  }
1700 
1701  public function syncWithOriginal(): void
1702  {
1703  if ($this->getOriginalId() === null) {
1704  return;
1705  }
1706 
1707  $original_parent_id = self::lookupParentObjId($this->getOriginalId());
1708 
1709  if ($original_parent_id === null) {
1710  return;
1711  }
1712 
1713  $this->cloneSuggestedSolutions($this->getId(), $this->getOriginalId());
1714  $original = clone $this;
1715  // Now we become the original
1716  $original->setId($this->getOriginalId());
1717  $original->setOriginalId(null);
1718  $original->setObjId($original_parent_id);
1719 
1720  $original->saveToDb();
1721 
1722  $original->deletePageOfQuestion($this->getOriginalId());
1723  $original->createPageObject();
1724  $original->clonePageOfQuestion($this->getId());
1725  $original = $this->cloneQuestionTypeSpecificProperties($original);
1726  $this->cloneXHTMLMediaObjectsOfQuestion($original->getId());
1727  $this->afterSyncWithOriginal($this->getOriginalId(), $this->getId(), $this->getObjId(), $original_parent_id);
1728  $this->afterSyncWithOriginal($this->getOriginalId(), $this->getId(), $original_parent_id, $this->getObjId());
1729  }
1730 
1736  public static function instantiateQuestion(int $question_id): assQuestion
1737  {
1738  global $DIC;
1739  $ilCtrl = $DIC['ilCtrl'];
1740  $ilDB = $DIC['ilDB'];
1741  $lng = $DIC['lng'];
1742  $questionrepository = QuestionPoolDIC::dic()['question.general_properties.repository'];
1743  $question_type = $questionrepository->getForQuestionId($question_id)?->getClassName() ?? '';
1744  if ($question_type === '') {
1745  throw new InvalidArgumentException('No question with ID ' . $question_id . ' exists');
1746  }
1747 
1748  $question = new $question_type();
1749  $question->loadFromDb($question_id);
1750 
1751  $feedbackObjectClassname = self::getFeedbackClassNameByQuestionType($question_type);
1752  $question->feedbackOBJ = new $feedbackObjectClassname($question, $ilCtrl, $ilDB, $lng);
1753 
1754  return $question;
1755  }
1756 
1757  public function getPoints(): float
1758  {
1759  return $this->points;
1760  }
1761 
1762  public function setPoints(float $points): void
1763  {
1764  $this->points = $points;
1765  }
1766 
1767  public function getSolutionMaxPass(int $active_id): ?int
1768  {
1769  return self::_getSolutionMaxPass($this->getId(), $active_id);
1770  }
1771 
1775  public static function _getSolutionMaxPass(int $question_id, int $active_id): ?int
1776  {
1777  // the following code was the old solution which added the non answered
1778  // questions of a pass from the answered questions of the previous pass
1779  // with the above solution, only the answered questions of the last pass are counted
1780  global $DIC;
1781  $ilDB = $DIC['ilDB'];
1782 
1783  $result = $ilDB->queryF(
1784  "SELECT MAX(pass) maxpass FROM tst_test_result WHERE active_fi = %s AND question_fi = %s",
1785  ['integer','integer'],
1786  [$active_id, $question_id]
1787  );
1788  if ($result->numRows() === 1) {
1789  $row = $ilDB->fetchAssoc($result);
1790  return $row["maxpass"];
1791  }
1792 
1793  return null;
1794  }
1795 
1796  public function isWriteable(): bool
1797  {
1798  return ilObjQuestionPool::_isWriteable($this->getObjId(), $this->getCurrentUser()->getId());
1799  }
1800 
1802  {
1803  $reached_points = $this->calculateReachedPointsForSolution($preview_session->getParticipantsSolution());
1804  return $this->ensureNonNegativePoints($reached_points);
1805  }
1806 
1807  protected function ensureNonNegativePoints(float $points): float
1808  {
1809  return $points > 0.0 ? $points : 0.0;
1810  }
1811 
1812  public function isPreviewSolutionCorrect(ilAssQuestionPreviewSession $preview_session): bool
1813  {
1814  $reached_points = $this->calculateReachedPointsFromPreviewSession($preview_session);
1815 
1816  return !($reached_points < $this->getMaximumPoints());
1817  }
1818 
1819 
1830  final public function adjustReachedPointsByScoringOptions(float $points, int $active_id): float
1831  {
1832  $count_system = ilObjTest::_getCountSystem($active_id);
1833  if ($count_system == 1) {
1834  if (abs($this->getMaximumPoints() - $points) > 0.0000000001) {
1835  $points = 0;
1836  }
1837  }
1838  $score_cutting = ilObjTest::_getScoreCutting($active_id);
1839  if ($score_cutting == 0) {
1840  if ($points < 0) {
1841  $points = 0;
1842  }
1843  }
1844  return $points;
1845  }
1846  public function buildHashedImageFilename(string $plain_image_filename, bool $unique = false): string
1847  {
1848  $extension = "";
1849 
1850  if (preg_match("/.*\.(png|jpg|gif|jpeg)$/i", $plain_image_filename, $matches)) {
1851  $extension = "." . $matches[1];
1852  }
1853 
1854  if ($unique) {
1855  $plain_image_filename = uniqid($plain_image_filename . microtime(true), true);
1856  }
1857 
1858  return md5($plain_image_filename) . $extension;
1859  }
1860 
1871  public static function _setReachedPoints(
1872  int $active_id,
1873  int $question_id,
1874  float $points,
1875  float $maxpoints,
1876  int $pass,
1877  bool $manualscoring
1878  ): void {
1879  global $DIC;
1880  $ilDB = $DIC['ilDB'];
1882  $question_properties_repository = QuestionPoolDIC::dic()['question.general_properties.repository'];
1883 
1884  if ($points > $maxpoints) {
1885  return;
1886  }
1887 
1888  if ($pass === null) {
1889  $pass = assQuestion::_getSolutionMaxPass($question_id, $active_id);
1890  }
1891 
1892  $old_points = 0;
1893  $result = $ilDB->queryF(
1894  "SELECT points FROM tst_test_result WHERE active_fi = %s AND question_fi = %s AND pass = %s",
1895  ['integer','integer','integer'],
1896  [$active_id, $question_id, $pass]
1897  );
1898  $manual = ($manualscoring) ? 1 : 0;
1899  $rowsnum = $result->numRows();
1900  if ($rowsnum > 0) {
1901  $row = $ilDB->fetchAssoc($result);
1902  $old_points = $row['points'];
1903  if ($old_points !== $points) {
1904  $affectedRows = $ilDB->manipulateF(
1905  "UPDATE tst_test_result SET points = %s, manual = %s, tstamp = %s WHERE active_fi = %s AND question_fi = %s AND pass = %s",
1906  ['float', 'integer', 'integer', 'integer', 'integer', 'integer'],
1907  [$points, $manual, time(), $active_id, $question_id, $pass]
1908  );
1909  }
1910  } else {
1911  $next_id = $ilDB->nextId('tst_test_result');
1912  $affectedRows = $ilDB->manipulateF(
1913  "INSERT INTO tst_test_result (test_result_id, active_fi, question_fi, points, pass, manual, tstamp) VALUES (%s, %s, %s, %s, %s, %s, %s)",
1914  ['integer', 'integer','integer', 'float', 'integer', 'integer','integer'],
1915  [$next_id, $active_id, $question_id, $points, $pass, $manual, time()]
1916  );
1917  }
1918 
1919  if (!self::isForcePassResultUpdateEnabled() && $old_points === $points && $rowsnum !== 0) {
1920  return;
1921  }
1922 
1924  $test_result_repository = TestDIC::dic()['results.data.repository'];
1925  $test_result_repository->updateTestAttemptResult($active_id, $pass);
1927  }
1928 
1929  public function getQuestion(): string
1930  {
1931  return $this->question;
1932  }
1933 
1934  public function getQuestionForHTMLOutput(): string
1935  {
1936  return $this->purifyAndPrepareTextAreaOutput($this->question);
1937  }
1938 
1939  protected function purifyAndPrepareTextAreaOutput(string $content): string
1940  {
1941  $purified_content = $this->getHtmlQuestionContentPurifier()->purify($content);
1943  || !(new ilSetting('advanced_editing'))->get('advanced_editing_javascript_editor') === 'tinymce') {
1944  $purified_content = nl2br($purified_content);
1945  }
1947  $purified_content,
1948  true,
1949  true
1950  );
1951  }
1952 
1953  public function setQuestion(string $question = ""): void
1954  {
1955  $this->question = $question;
1956  }
1957 
1958  public function getQuestionTypeID(): int
1959  {
1960  $result = $this->db->queryF(
1961  "SELECT question_type_id FROM qpl_qst_type WHERE type_tag = %s",
1962  ['text'],
1963  [$this->getQuestionType()]
1964  );
1965  if ($this->db->numRows($result) == 1) {
1966  $row = $this->db->fetchAssoc($result);
1967  return (int) $row["question_type_id"];
1968  }
1969  return 0;
1970  }
1971 
1972  protected function getRTETextWithMediaObjects(): string
1973  {
1974  // must be called in parent classes. add additional RTE text in the parent
1975  // classes and call this method to add the standard RTE text
1976  $collected = $this->getQuestion();
1977  $collected .= $this->feedbackOBJ->getGenericFeedbackContent($this->getId(), false);
1978  $collected .= $this->feedbackOBJ->getGenericFeedbackContent($this->getId(), true);
1979  $collected .= $this->feedbackOBJ->getAllSpecificAnswerFeedbackContents($this->getId());
1980  return $collected;
1981  }
1982 
1983  public function cleanupMediaObjectUsage(): void
1984  {
1985  $combinedtext = $this->getRTETextWithMediaObjects();
1986  ilRTE::_cleanupMediaObjectUsage($combinedtext, "qpl:html", $this->getId());
1987  }
1988 
1989  public function getInstances(): array
1990  {
1991  $result = $this->db->queryF(
1992  "SELECT question_id FROM qpl_questions WHERE original_id = %s",
1993  ["integer"],
1994  [$this->getId()]
1995  );
1996  $instances = [];
1997  $ids = [];
1998  while ($row = $this->db->fetchAssoc($result)) {
1999  $ids[] = $row["question_id"];
2000  }
2001  foreach ($ids as $question_id) {
2002  // check non random tests
2003  $result = $this->db->queryF(
2004  "SELECT tst_tests.obj_fi FROM tst_tests, tst_test_question WHERE tst_test_question.question_fi = %s AND tst_test_question.test_fi = tst_tests.test_id",
2005  ["integer"],
2006  [$question_id]
2007  );
2008  while ($row = $this->db->fetchAssoc($result)) {
2009  $instances[$row['obj_fi']] = ilObject::_lookupTitle($row['obj_fi']);
2010  }
2011  // check random tests
2012  $result = $this->db->queryF(
2013  "SELECT tst_tests.obj_fi FROM tst_tests, tst_test_rnd_qst, tst_active WHERE tst_test_rnd_qst.active_fi = tst_active.active_id AND tst_test_rnd_qst.question_fi = %s AND tst_tests.test_id = tst_active.test_fi",
2014  ["integer"],
2015  [$question_id]
2016  );
2017  while ($row = $this->db->fetchAssoc($result)) {
2018  $instances[$row['obj_fi']] = ilObject::_lookupTitle($row['obj_fi']);
2019  }
2020  }
2021  foreach ($instances as $key => $value) {
2022  $instances[$key] = ["obj_id" => $key, "title" => $value, "author" => ilObjTest::_lookupAuthor($key), "refs" => ilObject::_getAllReferences($key)];
2023  }
2024  return $instances;
2025  }
2026 
2033  public function getActiveUserData(int $active_id): array
2034  {
2035  $result = $this->db->queryF(
2036  "SELECT * FROM tst_active WHERE active_id = %s",
2037  ['integer'],
2038  [$active_id]
2039  );
2040  if ($this->db->numRows($result)) {
2041  $row = $this->db->fetchAssoc($result);
2042  return ["user_id" => $row["user_fi"], "test_id" => $row["test_fi"]];
2043  }
2044 
2045  return [];
2046  }
2047 
2048  public function hasSpecificFeedback(): bool
2049  {
2050  return static::HAS_SPECIFIC_FEEDBACK;
2051  }
2052 
2053  public static function getFeedbackClassNameByQuestionType(string $questionType): string
2054  {
2055  return str_replace('ass', 'ilAss', $questionType) . 'Feedback';
2056  }
2057 
2058 
2059 
2060  public static function instantiateQuestionGUI(int $question_id): ?assQuestionGUI
2061  {
2063  global $DIC;
2064  $ilCtrl = $DIC['ilCtrl'];
2065  $ilDB = $DIC['ilDB'];
2066  $lng = $DIC['lng'];
2067  $ilUser = $DIC['ilUser'];
2068  $ilLog = $DIC['ilLog'];
2069 
2070  if ($question_id <= 0) {
2071  $ilLog->warning('Instantiate question called without question id. (instantiateQuestionGUI@assQuestion)');
2072  throw new InvalidArgumentException('Instantiate question called without question id. (instantiateQuestionGUI@assQuestion)');
2073  }
2074 
2075  $questionrepository = QuestionPoolDIC::dic()['question.general_properties.repository'];
2076  $question_type = $questionrepository->getForQuestionId($question_id)?->getClassName();
2077 
2078  if ($question_type === null) {
2079  return null;
2080  }
2081 
2082  $question_type_gui = $question_type . 'GUI';
2083  $question_gui = new $question_type_gui($question_id);
2084 
2085  $feedback_object_classname = self::getFeedbackClassNameByQuestionType($question_type);
2086  $question = $question_gui->getObject();
2087  $question->feedbackOBJ = new $feedback_object_classname($question, $ilCtrl, $ilDB, $lng);
2088 
2089  $assSettings = new ilSetting('assessment');
2090  $processLockerFactory = new ilAssQuestionProcessLockerFactory($assSettings, $ilDB);
2091  $processLockerFactory->setQuestionId($question_gui->getObject()->getId());
2092  $processLockerFactory->setUserId($ilUser->getId());
2093  $question->setProcessLocker($processLockerFactory->getLocker());
2094  $question_gui->setObject($question);
2095 
2096  return $question_gui;
2097  }
2098 
2099  public function getNrOfTries(): int
2100  {
2101  return $this->nr_of_tries;
2102  }
2103 
2104  public function setNrOfTries(int $a_nr_of_tries): void
2105  {
2106  $this->nr_of_tries = $a_nr_of_tries;
2107  }
2108 
2109  public function setExportImagePath(string $path): void
2110  {
2111  $this->export_image_path = $path;
2112  }
2113 
2114  public static function _questionExistsInTest(int $question_id, int $test_id): bool
2115  {
2116  global $DIC;
2117  $ilDB = $DIC['ilDB'];
2118 
2119  if ($question_id < 1) {
2120  return false;
2121  }
2122 
2123  $result = $ilDB->queryF(
2124  "SELECT question_fi FROM tst_test_question WHERE question_fi = %s AND test_fi = %s",
2125  ['integer', 'integer'],
2126  [$question_id, $test_id]
2127  );
2128  return $ilDB->numRows($result) == 1;
2129  }
2130 
2131  public function formatSAQuestion($a_q): string
2132  {
2133  return $this->getSelfAssessmentFormatter()->format($a_q);
2134  }
2135 
2137  {
2138  return new \ilAssSelfAssessmentQuestionFormatter();
2139  }
2140 
2141  // scorm2004-start ???
2142 
2143  public function setPreventRteUsage(bool $prevent_rte_usage): void
2144  {
2145  $this->prevent_rte_usage = $prevent_rte_usage;
2146  }
2147 
2148  public function getPreventRteUsage(): bool
2149  {
2150  return $this->prevent_rte_usage;
2151  }
2152 
2154  {
2155  $this->lmMigrateQuestionTypeGenericContent($migrator);
2156  $this->lmMigrateQuestionTypeSpecificContent($migrator);
2157  $this->saveToDb();
2158 
2159  $this->feedbackOBJ->migrateContentForLearningModule($migrator, $this->getId());
2160  }
2161 
2163  {
2164  $this->setQuestion($migrator->migrateToLmContent($this->getQuestion()));
2165  }
2166 
2168  {
2169  // overwrite if any question type specific content except feedback needs to be migrated
2170  }
2171 
2172  public function setSelfAssessmentEditingMode(bool $selfassessmenteditingmode): void
2173  {
2174  $this->selfassessmenteditingmode = $selfassessmenteditingmode;
2175  }
2176 
2177  public function getSelfAssessmentEditingMode(): bool
2178  {
2180  }
2181 
2182  public function setDefaultNrOfTries(int $defaultnroftries): void
2183  {
2184  $this->defaultnroftries = $defaultnroftries;
2185  }
2186 
2187  public function getDefaultNrOfTries(): int
2188  {
2189  return $this->defaultnroftries;
2190  }
2191 
2192  // scorm2004-end ???
2193 
2194  public static function lookupParentObjId(int $question_id): ?int
2195  {
2196  global $DIC;
2197  $ilDB = $DIC['ilDB'];
2198 
2199  $query = "SELECT obj_fi FROM qpl_questions WHERE question_id = %s";
2200 
2201  $res = $ilDB->queryF($query, ['integer'], [$question_id]);
2202  $row = $ilDB->fetchAssoc($res);
2203 
2204  return $row['obj_fi'] ?? null;
2205  }
2206 
2207  protected function duplicateSkillAssignments(int $srcParentId, int $srcQuestionId, int $trgParentId, int $trgQuestionId): void
2208  {
2209  $assignmentList = new ilAssQuestionSkillAssignmentList($this->db);
2210  $assignmentList->setParentObjId($srcParentId);
2211  $assignmentList->setQuestionIdFilter($srcQuestionId);
2212  $assignmentList->loadFromDb();
2213 
2214  foreach ($assignmentList->getAssignmentsByQuestionId($srcQuestionId) as $assignment) {
2215  $assignment->setParentObjId($trgParentId);
2216  $assignment->setQuestionId($trgQuestionId);
2217  $assignment->saveToDb();
2218 
2219  // add skill usage
2220  $this->skillUsageService->addUsage(
2221  $trgParentId,
2222  $assignment->getSkillBaseId(),
2223  $assignment->getSkillTrefId()
2224  );
2225  }
2226  }
2227 
2228  public function syncSkillAssignments(int $srcParentId, int $srcQuestionId, int $trgParentId, int $trgQuestionId): void
2229  {
2230  $assignmentList = new ilAssQuestionSkillAssignmentList($this->db);
2231  $assignmentList->setParentObjId($trgParentId);
2232  $assignmentList->setQuestionIdFilter($trgQuestionId);
2233  $assignmentList->loadFromDb();
2234 
2235  foreach ($assignmentList->getAssignmentsByQuestionId($trgQuestionId) as $assignment) {
2236  $assignment->deleteFromDb();
2237 
2238  // remove skill usage
2239  if (!$assignment->isSkillUsed()) {
2240  $this->skillUsageService->removeUsage(
2241  $assignment->getParentObjId(),
2242  $assignment->getSkillBaseId(),
2243  $assignment->getSkillTrefId()
2244  );
2245  }
2246  }
2247 
2248  $this->duplicateSkillAssignments($srcParentId, $srcQuestionId, $trgParentId, $trgQuestionId);
2249  }
2250 
2251  public function isAnswered(int $active_id, int $pass): bool
2252  {
2253  $numExistingSolutionRecords = assQuestion::getNumExistingSolutionRecords($active_id, $pass, $this->getId());
2254  return $numExistingSolutionRecords > 0;
2255  }
2256 
2257  protected static function getNumExistingSolutionRecords(int $activeId, int $pass, int $questionId): int
2258  {
2259  global $DIC;
2260  $ilDB = $DIC['ilDB'];
2261 
2262  $query = "
2263  SELECT count(active_fi) cnt
2264 
2265  FROM tst_solutions
2266 
2267  WHERE active_fi = %s
2268  AND question_fi = %s
2269  AND pass = %s
2270  ";
2271 
2272  $res = $ilDB->queryF(
2273  $query,
2274  ['integer','integer','integer'],
2275  [$activeId, $questionId, $pass]
2276  );
2277 
2278  $row = $ilDB->fetchAssoc($res);
2279 
2280  return (int) $row['cnt'];
2281  }
2282 
2283  public function getAdditionalContentEditingMode(): string
2284  {
2286  }
2287 
2288  public function setAdditionalContentEditingMode(?string $additionalContentEditingMode): void
2289  {
2290  if (!in_array($additionalContentEditingMode, $this->getValidAdditionalContentEditingModes())) {
2291  throw new ilTestQuestionPoolException('invalid additional content editing mode given: ' . $additionalContentEditingMode);
2292  }
2293 
2294  $this->additionalContentEditingMode = $additionalContentEditingMode;
2295  }
2296 
2298  {
2300  }
2301 
2302  public function isValidAdditionalContentEditingMode(string $additionalContentEditingMode): bool
2303  {
2304  if (in_array($additionalContentEditingMode, $this->getValidAdditionalContentEditingModes())) {
2305  return true;
2306  }
2307 
2308  return false;
2309  }
2310 
2311  public function getValidAdditionalContentEditingModes(): array
2312  {
2313  return [
2314  self::ADDITIONAL_CONTENT_EDITING_MODE_RTE,
2315  self::ADDITIONAL_CONTENT_EDITING_MODE_IPE
2316  ];
2317  }
2318 
2323  {
2324  return ilHtmlPurifierFactory::getInstanceByType('qpl_usersolution');
2325  }
2326 
2331  {
2332  return ilHtmlPurifierFactory::getInstanceByType('qpl_usersolution');
2333  }
2334 
2335  protected function buildQuestionDataQuery(): string
2336  {
2337  return "
2338  SELECT qpl_questions.*,
2339  {$this->getAdditionalTableName()}.*
2340  FROM qpl_questions
2341  LEFT JOIN {$this->getAdditionalTableName()}
2342  ON {$this->getAdditionalTableName()}.question_fi = qpl_questions.question_id
2343  WHERE qpl_questions.question_id = %s
2344  ";
2345  }
2346 
2347  public function setLastChange(int $lastChange): void
2348  {
2349  $this->lastChange = $lastChange;
2350  }
2351 
2352  public function getLastChange(): ?int
2353  {
2354  return $this->lastChange;
2355  }
2356 
2357  protected function getCurrentSolutionResultSet(int $active_id, int $pass, bool $authorized = true): \ilDBStatement
2358  {
2359  if ($this->getStep() !== null) {
2360  $query = "
2361  SELECT *
2362  FROM tst_solutions
2363  WHERE active_fi = %s
2364  AND question_fi = %s
2365  AND pass = %s
2366  AND step = %s
2367  AND authorized = %s
2368  ";
2369 
2370  return $this->db->queryF(
2371  $query,
2372  ['integer', 'integer', 'integer', 'integer', 'integer'],
2373  [$active_id, $this->getId(), $pass, $this->getStep(), (int) $authorized]
2374  );
2375  }
2376 
2377  $query = "
2378  SELECT *
2379  FROM tst_solutions
2380  WHERE active_fi = %s
2381  AND question_fi = %s
2382  AND pass = %s
2383  AND authorized = %s
2384  ";
2385 
2386  return $this->db->queryF(
2387  $query,
2388  ['integer', 'integer', 'integer', 'integer'],
2389  [$active_id, $this->getId(), $pass, (int) $authorized]
2390  );
2391  }
2392 
2393  protected function removeSolutionRecordById(int $solutionId): int
2394  {
2395  return $this->db->manipulateF(
2396  "DELETE FROM tst_solutions WHERE solution_id = %s",
2397  ['integer'],
2398  [$solutionId]
2399  );
2400  }
2401 
2402  // hey: prevPassSolutions - selected file reuse, copy solution records
2406  protected function getSolutionRecordById(int $solutionId): array
2407  {
2408  $result = $this->db->queryF(
2409  "SELECT * FROM tst_solutions WHERE solution_id = %s",
2410  ['integer'],
2411  [$solutionId]
2412  );
2413 
2414  if ($this->db->numRows($result) > 0) {
2415  return $this->db->fetchAssoc($result);
2416  }
2417  return [];
2418  }
2419  // hey.
2420 
2421  public function removeIntermediateSolution(int $active_id, int $pass): void
2422  {
2423  $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(function () use ($active_id, $pass) {
2424  $this->removeCurrentSolution($active_id, $pass, false);
2425  });
2426  }
2427 
2431  public function removeCurrentSolution(int $active_id, int $pass, bool $authorized = true): int
2432  {
2433  if ($this->getStep() !== null) {
2434  $query = '
2435  DELETE FROM tst_solutions
2436  WHERE active_fi = %s
2437  AND question_fi = %s
2438  AND pass = %s
2439  AND step = %s
2440  AND authorized = %s
2441  ';
2442 
2443  return $this->db->manipulateF(
2444  $query,
2445  ['integer', 'integer', 'integer', 'integer', 'integer'],
2446  [$active_id, $this->getId(), $pass, $this->getStep(), (int) $authorized]
2447  );
2448  }
2449 
2450  $query = "
2451  DELETE FROM tst_solutions
2452  WHERE active_fi = %s
2453  AND question_fi = %s
2454  AND pass = %s
2455  AND authorized = %s
2456  ";
2457 
2458  return $this->db->manipulateF(
2459  $query,
2460  ['integer', 'integer', 'integer', 'integer'],
2461  [$active_id, $this->getId(), $pass, (int) $authorized]
2462  );
2463  }
2464 
2465  // fau: testNav - add timestamp as parameter to saveCurrentSolution
2466  public function saveCurrentSolution(int $active_id, int $pass, $value1, $value2, bool $authorized = true, $tstamp = 0): int
2467  {
2468  $next_id = $this->db->nextId("tst_solutions");
2469 
2470  $fieldData = [
2471  "solution_id" => ["integer", $next_id],
2472  "active_fi" => ["integer", $active_id],
2473  "question_fi" => ["integer", $this->getId()],
2474  "value1" => ["clob", $value1],
2475  "value2" => ["clob", $value2],
2476  "pass" => ["integer", $pass],
2477  "tstamp" => ["integer", ((int) $tstamp > 0) ? (int) $tstamp : time()],
2478  'authorized' => ['integer', (int) $authorized]
2479  ];
2480 
2481  if ($this->getStep() !== null) {
2482  $fieldData['step'] = ["integer", $this->getStep()];
2483  }
2484 
2485  return $this->db->insert("tst_solutions", $fieldData);
2486  }
2487  // fau.
2488 
2489  public function updateCurrentSolution(int $solutionId, $value1, $value2, bool $authorized = true): int
2490  {
2491  $fieldData = [
2492  "value1" => ["clob", $value1],
2493  "value2" => ["clob", $value2],
2494  "tstamp" => ["integer", time()],
2495  'authorized' => ['integer', (int) $authorized]
2496  ];
2497 
2498  if ($this->getStep() !== null) {
2499  $fieldData['step'] = ["integer", $this->getStep()];
2500  }
2501 
2502  return $this->db->update("tst_solutions", $fieldData, [
2503  'solution_id' => ['integer', $solutionId]
2504  ]);
2505  }
2506 
2507  // fau: testNav - added parameter to keep the timestamp (default: false)
2508  public function updateCurrentSolutionsAuthorization(int $activeId, int $pass, bool $authorized, bool $keepTime = false): int
2509  {
2510  $fieldData = [
2511  'authorized' => ['integer', (int) $authorized]
2512  ];
2513 
2514  if (!$keepTime) {
2515  $fieldData['tstamp'] = ['integer', time()];
2516  }
2517 
2518  $whereData = [
2519  'question_fi' => ['integer', $this->getId()],
2520  'active_fi' => ['integer', $activeId],
2521  'pass' => ['integer', $pass]
2522  ];
2523 
2524  if ($this->getStep() !== null) {
2525  $whereData['step'] = ["integer", $this->getStep()];
2526  }
2527 
2528  return $this->db->update('tst_solutions', $fieldData, $whereData);
2529  }
2530  // fau.
2531 
2532  // hey: prevPassSolutions - motivation slowly decreases on imagemap
2534 
2535  public static function implodeKeyValues(array $keyValues): string
2536  {
2537  return implode(assQuestion::KEY_VALUES_IMPLOSION_SEPARATOR, $keyValues);
2538  }
2539 
2540  public static function explodeKeyValues(string $keyValues): array
2541  {
2542  return explode(assQuestion::KEY_VALUES_IMPLOSION_SEPARATOR, $keyValues);
2543  }
2544 
2545  protected function deleteDummySolutionRecord(int $activeId, int $passIndex): void
2546  {
2547  foreach ($this->getSolutionValues($activeId, $passIndex, false) as $solutionRec) {
2548  if ($solutionRec['value1'] == '' && $solutionRec['value2'] == '') {
2549  $this->removeSolutionRecordById($solutionRec['solution_id']);
2550  }
2551  }
2552  }
2553 
2554  protected function isDummySolutionRecord(array $solutionRecord): bool
2555  {
2556  return !strlen($solutionRecord['value1']) && !strlen($solutionRecord['value2']);
2557  }
2558 
2559  protected function deleteSolutionRecordByValues(int $activeId, int $passIndex, bool $authorized, array $matchValues): void
2560  {
2561  $types = ["integer", "integer", "integer", "integer"];
2562  $values = [$activeId, $this->getId(), $passIndex, (int) $authorized];
2563  $valuesCondition = [];
2564 
2565  foreach ($matchValues as $valueField => $value) {
2566  switch ($valueField) {
2567  case 'value1':
2568  case 'value2':
2569  $valuesCondition[] = "{$valueField} = %s";
2570  $types[] = 'text';
2571  $values[] = $value;
2572  break;
2573 
2574  default:
2575  throw new ilTestQuestionPoolException('invalid value field given: ' . $valueField);
2576  }
2577  }
2578 
2579  $valuesCondition = implode(' AND ', $valuesCondition);
2580 
2581  $query = "
2582  DELETE FROM tst_solutions
2583  WHERE active_fi = %s
2584  AND question_fi = %s
2585  AND pass = %s
2586  AND authorized = %s
2587  AND $valuesCondition
2588  ";
2589 
2590  if ($this->getStep() !== null) {
2591  $query .= " AND step = %s ";
2592  $types[] = 'integer';
2593  $values[] = $this->getStep();
2594  }
2595 
2596  $this->db->manipulateF($query, $types, $values);
2597  }
2598 
2599  protected function duplicateIntermediateSolutionAuthorized(int $activeId, int $passIndex): void
2600  {
2601  foreach ($this->getSolutionValues($activeId, $passIndex, false) as $rec) {
2602  $this->saveCurrentSolution($activeId, $passIndex, $rec['value1'], $rec['value2'], true, $rec['tstamp']);
2603  }
2604  }
2605 
2606  protected function forceExistingIntermediateSolution(int $activeId, int $passIndex, bool $considerDummyRecordCreation): void
2607  {
2608  $intermediateSolution = $this->getSolutionValues($activeId, $passIndex, false);
2609 
2610  if (!count($intermediateSolution)) {
2611  $this->updateCurrentSolutionsAuthorization($activeId, $passIndex, false, true);
2612 
2613  // create a backup as authorized solution again (keeping timestamps)
2614  $this->duplicateIntermediateSolutionAuthorized($activeId, $passIndex);
2615 
2616  if ($considerDummyRecordCreation) {
2617  // create an additional dummy record to indicate the existence of an intermediate solution
2618  // even if all entries are deleted from the intermediate solution later
2619  $this->saveCurrentSolution($activeId, $passIndex, null, null, false);
2620  }
2621  }
2622  }
2623  // hey.
2624 
2628  public function setStep($step): void
2629  {
2630  $this->step = $step;
2631  }
2632 
2636  public function getStep(): ?int
2637  {
2638  return $this->step;
2639  }
2640 
2641  public static function convertISO8601FormatH_i_s_ExtendedToSeconds(string $time): int
2642  {
2643  $sec = 0;
2644  $time_array = explode(':', $time);
2645  if (count($time_array) == 3) {
2646  $sec += (int) $time_array[0] * 3600;
2647  $sec += (int) $time_array[1] * 60;
2648  $sec += (int) $time_array[2];
2649  }
2650  return $sec;
2651  }
2652 
2653  public function toJSON(): string
2654  {
2655  return json_encode([]);
2656  }
2657 
2658  // hey: prevPassSolutions - check for authorized solution
2659  public function intermediateSolutionExists(int $active_id, int $pass): bool
2660  {
2661  $solutionAvailability = $this->lookupForExistingSolutions($active_id, $pass);
2662  return (bool) $solutionAvailability['intermediate'];
2663  }
2664 
2665  public function authorizedSolutionExists(int $active_id, ?int $pass): bool
2666  {
2667  if ($pass === null) {
2668  return false;
2669  }
2670  $solutionAvailability = $this->lookupForExistingSolutions($active_id, $pass);
2671  return (bool) $solutionAvailability['authorized'];
2672  }
2673 
2674  public function authorizedOrIntermediateSolutionExists(int $active_id, int $pass): bool
2675  {
2676  $solutionAvailability = $this->lookupForExistingSolutions($active_id, $pass);
2677  return $solutionAvailability['authorized'] || $solutionAvailability['intermediate'];
2678  }
2679  // hey.
2680 
2681  protected function lookupMaxStep(int $active_id, int $pass): int
2682  {
2683  $result = $this->db->queryF(
2684  "SELECT MAX(step) max_step FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s",
2685  ["integer", "integer", "integer"],
2686  [$active_id, $pass, $this->getId()]
2687  );
2688 
2689  $row = $this->db->fetchAssoc($result);
2690 
2691  return (int) $row['max_step'];
2692  }
2693 
2694  // fau: testNav - new function lookupForExistingSolutions
2699  public function lookupForExistingSolutions(int $activeId, int $pass): array
2700  {
2701  $return = [
2702  'authorized' => false,
2703  'intermediate' => false
2704  ];
2705 
2706  $query = "
2707  SELECT authorized, COUNT(*) cnt
2708  FROM tst_solutions
2709  WHERE active_fi = %s
2710  AND question_fi = %s
2711  AND pass = %s
2712  ";
2713 
2714  if ($this->getStep() !== null) {
2715  $query .= " AND step = " . $this->db->quote((int) $this->getStep(), 'integer') . " ";
2716  }
2717 
2718  $query .= "
2719  GROUP BY authorized
2720  ";
2721 
2722  $result = $this->db->queryF($query, ['integer', 'integer', 'integer'], [$activeId, $this->getId(), $pass]);
2723 
2724  while ($row = $this->db->fetchAssoc($result)) {
2725  if ($row['authorized']) {
2726  $return['authorized'] = $row['cnt'] > 0;
2727  } else {
2728  $return['intermediate'] = $row['cnt'] > 0;
2729  }
2730  }
2731  return $return;
2732  }
2733  // fau.
2734 
2735  public function isAddableAnswerOptionValue(int $qIndex, string $answerOptionValue): bool
2736  {
2737  return false;
2738  }
2739 
2740  public function addAnswerOptionValue(int $qIndex, string $answerOptionValue, float $points): void
2741  {
2742  }
2743 
2744  public function removeAllExistingSolutions(): void
2745  {
2746  $query = "DELETE FROM tst_solutions WHERE question_fi = %s";
2747  $this->db->manipulateF($query, ['integer'], [$this->getId()]);
2748  }
2749 
2750  public function removeExistingSolutions(int $activeId, int $pass): int
2751  {
2752  $query = "
2753  DELETE FROM tst_solutions
2754  WHERE active_fi = %s
2755  AND question_fi = %s
2756  AND pass = %s
2757  ";
2758 
2759  if ($this->getStep() !== null) {
2760  $query .= " AND step = " . $this->db->quote((int) $this->getStep(), 'integer') . " ";
2761  }
2762 
2763  return $this->db->manipulateF(
2764  $query,
2765  ['integer', 'integer', 'integer'],
2766  [$activeId, $this->getId(), $pass]
2767  );
2768  }
2769 
2770  public function resetUsersAnswer(int $activeId, int $pass): void
2771  {
2772  $this->removeExistingSolutions($activeId, $pass);
2773  $this->removeResultRecord($activeId, $pass);
2774  $this->test_result_repository->updateTestAttemptResult($activeId, $pass, $this->getProcessLocker(), $this->getTestId());
2775  }
2776 
2777  public function removeResultRecord(int $activeId, int $pass): int
2778  {
2779  $query = "
2780  DELETE FROM tst_test_result
2781  WHERE active_fi = %s
2782  AND question_fi = %s
2783  AND pass = %s
2784  ";
2785 
2786  if ($this->getStep() !== null) {
2787  $query .= " AND step = " . $this->db->quote((int) $this->getStep(), 'integer') . " ";
2788  }
2789 
2790  return $this->db->manipulateF(
2791  $query,
2792  ['integer', 'integer', 'integer'],
2793  [$activeId, $this->getId(), $pass]
2794  );
2795  }
2796 
2797  public function fetchValuePairsFromIndexedValues(array $indexedValues): array
2798  {
2799  $valuePairs = [];
2800 
2801  foreach ($indexedValues as $value1 => $value2) {
2802  $valuePairs[] = ['value1' => $value1, 'value2' => $value2];
2803  }
2804 
2805  return $valuePairs;
2806  }
2807 
2808  public function fetchIndexedValuesFromValuePairs(array $value_pairs): array
2809  {
2810  $indexed_values = [];
2811 
2812  foreach ($value_pairs as $valuePair) {
2813  $indexed_values[$valuePair['value1']] = $valuePair['value2'];
2814  }
2815 
2816  return $indexed_values;
2817  }
2818 
2819  public function updateTimestamp(): void
2820  {
2821  $this->db->manipulateF(
2822  "UPDATE qpl_questions SET tstamp = %s WHERE question_id = %s",
2823  ['integer', 'integer'],
2824  [time(), $this->getId()]
2825  );
2826  }
2827 
2829  {
2830  if ($this->test_question_config === null) {
2831  $this->test_question_config = $this->buildTestPresentationConfig();
2832  }
2833 
2835  }
2836 
2838  {
2839  return new ilTestQuestionConfig();
2840  }
2841 
2843  {
2845  }
2846 
2847  protected function loadSuggestedSolutions(): array
2848  {
2849  $question_id = $this->getId();
2850  return $this->getSuggestedSolutionsRepo()->selectFor($question_id);
2851  }
2852 
2867  public static function extendedTrim(string $value): string
2868  {
2869  return preg_replace(self::TRIM_PATTERN, '', $value);
2870  }
2871 
2872  public function hasWritableOriginalInQuestionPool(): bool
2873  {
2874  return !is_null($this->original_id)
2875  && $this->questionrepository->questionExistsInPool($this->original_id)
2876  && assQuestion::instantiateQuestion($this->original_id)->isWriteable();
2877  }
2878 
2880  AdditionalInformationGenerator $additional_info,
2881  int $test_ref_id,
2882  int $active_id,
2883  int $pass,
2884  string $source_ip,
2885  TestParticipantInteractionTypes $interaction_type
2887  return new TestParticipantInteraction(
2888  $test_ref_id,
2889  $this->id,
2890  $this->current_user->getId(),
2891  $source_ip,
2892  $interaction_type,
2893  time(),
2894  $this->answerToLog($additional_info, $active_id, $pass)
2895  );
2896  }
2897 
2899  AdditionalInformationGenerator $additional_info,
2900  int $test_ref_id,
2904  $test_ref_id,
2905  $this->id,
2906  $this->current_user->getId(),
2907  $interaction_type,
2908  time(),
2909  $this->toLog($additional_info)
2910  );
2911  }
2912 
2913  protected function answerToLog(
2914  AdditionalInformationGenerator $additional_info,
2915  int $active_id,
2916  int $pass
2917  ): array {
2918  return [
2919  AdditionalInformationGenerator::KEY_PASS => $pass,
2920  AdditionalInformationGenerator::KEY_REACHED_POINTS => $this->getReachedPoints($active_id, $pass),
2921  AdditionalInformationGenerator::KEY_PAX_ANSWER => $this->solutionValuesToLog(
2922  $additional_info,
2923  $this->getSolutionValues($active_id, $pass)
2924  )
2925  ];
2926  }
2927 
2928  public function getSolutionForTextOutput(
2929  int $active_id,
2930  int $pass
2931  ): array|string {
2932  return $this->solutionValuesToText(
2933  $this->getSolutionValues($active_id, $pass)
2934  );
2935  }
2936 
2938  int $active_id,
2939  int $pass
2940  ): array|string {
2941  return $this->solutionValuesToText(
2942  $this->getSolutionValues($active_id, $pass)
2943  );
2944  }
2945 
2946  public function getVariablesAsTextArray(
2947  int $active_id,
2948  int $pass
2949  ): array {
2950  return [];
2951  }
2952 }
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...
loadFromDb(int $question_id)
const ADDITIONAL_CONTENT_EDITING_MODE_IPE
ilLanguage $lng
cloneXHTMLMediaObjectsOfQuestion(int $source_question_id)
setNrOfTries(int $a_nr_of_tries)
ilTestQuestionConfig $testQuestionConfig
ilAssQuestionFeedback $feedbackOBJ
syncSkillAssignments(int $srcParentId, int $srcQuestionId, int $trgParentId, int $trgQuestionId)
ilGlobalPageTemplate $tpl
deletePageOfQuestion(int $question_id)
getSolutionValues(int $active_id, ?int $pass=null, bool $authorized=true)
Loads solutions of a given user from the database an returns it.
$res
Definition: ltiservices.php:66
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
SuggestedSolutionsDatabaseRepository $suggestedsolution_repo
authorizedOrIntermediateSolutionExists(int $active_id, int $pass)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
const MAXIMUM_THUMB_SIZE
getActiveUserData(int $active_id)
Returns the user id and the test id for a given active id.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
migrateContentForLearningModule(ilAssSelfAssessmentMigrator $migrator)
toXML(bool $a_include_header=true, bool $a_include_binary=true, bool $a_shuffle=false, bool $test_output=false, bool $force_image_references=false)
Returns a QTI xml representation of the question.
static _isWriteable($object_id, $user_id)
Returns true, if the question pool is writeable by a given user.
calculateResultsFromSolution(int $active_id, int $pass)
Calculates the question results from a previously saved question solution.
static _getPass($active_id)
Retrieves the actual pass of a given user for a given test.
const IL_INST_ID
Definition: constants.php:40
ilTestQuestionConfig $test_question_config
isPreviewSolutionCorrect(ilAssQuestionPreviewSession $preview_session)
answerToLog(AdditionalInformationGenerator $additional_info, int $active_id, int $pass)
removeResultRecord(int $activeId, int $pass)
deleteAdditionalTableData(int $question_id)
deleteAnswers(int $question_id)
duplicate(bool $for_test=true, string $title='', string $author='', int $owner=-1, $test_obj_id=null)
static _getSuggestedSolutionOutput(int $question_id)
updateCurrentSolutionsAuthorization(int $activeId, int $pass, bool $authorized, bool $keepTime=false)
onDuplicate(int $original_parent_id, int $original_question_id, int $duplicate_parent_id, int $duplicate_question_id)
TestResultRepository $test_result_repository
resetUsersAnswer(int $activeId, int $pass)
static isFileAvailable(string $file)
removeAllImageFiles(string $image_target_path)
setProcessLocker(ilAssQuestionProcessLocker $processLocker)
Provides fluid interface to LoggingServices.
Interface Observer Contains several chained tasks and infos about them.
setOwner(int $owner=-1)
cloneSuggestedSolutionFiles(int $source_question_id, int $target_question_id)
ensureNonNegativePoints(float $points)
static _getAllReferences(int $id)
get all reference ids for object ID
ilAssQuestionProcessLocker $processLocker
Refinery $refinery
setSelfAssessmentEditingMode(bool $selfassessmenteditingmode)
SkillUsageService $skillUsageService
isDummySolutionRecord(array $solutionRecord)
solutionValuesToText(array $solution_values)
MUST convert the given solution values into text.
toLog(AdditionalInformationGenerator $additional_info)
MUST return an array of the question settings that can be stored in the log.
adjustReachedPointsByScoringOptions(float $points, int $active_id)
Adjust the given reached points by checks for all special scoring options in the test container...
createNewOriginalFromThisDuplicate(int $target_parent_id, string $target_question_title='')
static _saveUsage(int $a_mob_id, string $a_type, int $a_id, int $a_usage_hist_nr=0, string $a_lang="-")
Save usage of mob within another container (e.g.
getAdditionalTableName()
static isForcePassResultUpdateEnabled()
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.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setThumbSize(int $a_size)
static getUsageOfObject(int $a_obj_id, bool $a_include_titles=false)
cloneQuestionTypeSpecificProperties(self $target)
lmMigrateQuestionTypeSpecificContent(ilAssSelfAssessmentMigrator $migrator)
static getNumExistingSolutionRecords(int $activeId, int $pass, int $questionId)
duplicateSkillAssignments(int $srcParentId, int $srcQuestionId, int $trgParentId, int $trgQuestionId)
cloneSuggestedSolutions(int $source_question_id, int $target_question_id)
RequestDataCollector $questionpool_request
static _cleanupMediaObjectUsage(string $a_text, string $a_usage_type, int $a_usage_id)
Synchronises appearances of media objects in $a_text with media object usage table.
persistWorkingState(int $active_id, $pass, bool $authorized=true)
persists the working state for current testactive and testpass
static prepareFormOutput($a_str, bool $a_strip=false)
QuestionFiles $question_files
static setTokenMaxLifetimeInSeconds(int $token_max_lifetime_in_seconds)
persistPreviewState(ilAssQuestionPreviewSession $preview_session)
persists the preview state for current user and question
setPreventRteUsage(bool $prevent_rte_usage)
static makeDirParents(string $a_dir)
Create a new directory and all parent directories.
getSuggestedSolution(int $subquestion_index=0)
setComment(string $comment="")
static _questionExistsInTest(int $question_id, int $test_id)
string $questionActionCmd
getValidAdditionalContentEditingModes()
HTTPServices $http
Customizing of pimple-DIC for ILIAS.
Definition: Container.php:35
getUserSolutionPreferingIntermediate(int $active_id, ?int $pass=null)
afterSyncWithOriginal(int $original_question_id, int $clone_question_id, int $original_parent_id, int $clone_parent_id)
bool $selfassessmenteditingmode
$path
Definition: ltiservices.php:29
static implodeKeyValues(array $keyValues)
solutionValuesToLog(AdditionalInformationGenerator $additional_info, array $solution_values)
MUST convert the given solution values into an array or a string that can be stored in the log...
setExportImagePath(string $path)
static removeTrailingPathSeparators(string $path)
Repository internal data service.
calculateReachedPoints(int $active_id, ?int $pass=null, bool $authorized_solution=true)
fixUnavailableSkinImageSources(string $html)
isAddableAnswerOptionValue(int $qIndex, string $answerOptionValue)
updateCurrentSolution(int $solutionId, $value1, $value2, bool $authorized=true)
static getASCIIFilename(string $a_filename)
deleteDummySolutionRecord(int $activeId, int $passIndex)
getVariablesAsTextArray(int $active_id, int $pass)
while($session_entry=$r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) return null
removeIntermediateSolution(int $active_id, int $pass)
static _getUserIdFromActiveId(int $active_id)
LoggingServices $log
static resetOriginalId(int $questionId)
resolveInternalLink(string $internal_link)
static setForcePassResultUpdateEnabled(bool $force_pass_results_update_enabled)
ilAssQuestionPage $page
static instantiateQuestion(int $question_id)
Interface for html sanitizing functionality.
saveCurrentSolution(int $active_id, int $pass, $value1, $value2, bool $authorized=true, $tstamp=0)
purifyAndPrepareTextAreaOutput(string $content)
setTestId(int $id=-1)
static http()
Fetches the global http state from ILIAS.
clonePageOfQuestion(int $a_q_id)
getImagePath($question_id=null, $object_id=null)
Returns the image path for web accessable images of a question.
duplicateComments(int $parent_source_id, int $source_id, int $parent_target_id, int $target_id)
buildHashedImageFilename(string $plain_image_filename, bool $unique=false)
static _lookupTitle(int $obj_id)
lookupForExistingSolutions(int $activeId, int $pass)
Lookup if an authorized or intermediate solution exists.
Transformation $shuffler
copyObject(int $target_parent_id, string $title='')
getAdjustedReachedPoints(int $active_id, int $pass, bool $authorized_solution=true)
static _exists(string $a_parent_type, int $a_id, string $a_lang="", bool $a_no_cache=false)
Checks whether page exists.
answerToParticipantInteraction(AdditionalInformationGenerator $additional_info, int $test_ref_id, int $active_id, int $pass, string $source_ip, TestParticipantInteractionTypes $interaction_type)
removeSolutionRecordById(int $solutionId)
static _getReachedPoints(int $active_id, int $question_id, int $pass)
saveWorkingData(int $active_id, ?int $pass=null, bool $authorized=true)
getCorrectSolutionForTextOutput(int $active_id, int $pass)
static delDir(string $a_dir, bool $a_clean_only=false)
removes a dir and all its content (subdirs and files) recursively
setLastChange(int $lastChange)
const MINIMUM_THUMB_SIZE
getSolutionForTextOutput(int $active_id, int $pass)
static _getIdForImportId(string $a_import_id)
get current object id for import id (static)
static _getSolutionMaxPass(int $question_id, int $active_id)
Returns the maximum pass a users question solution.
intermediateSolutionExists(int $active_id, int $pass)
static _getCountSystem($active_id)
global $DIC
Definition: shib_login.php:26
static getImagePath(string $image_name, string $module_path="", string $mode="output", bool $offline=false)
get image path (for images located in a template directory)
static getInstanceByType(string $type)
const CLIENT_WEB_DIR
Definition: constants.php:47
static _lookupAuthor($obj_id)
Gets the authors name of the ilObjTest object.
setPoints(float $points)
setObjId(int $obj_id=0)
saveQuestionDataToDb(?int $original_id=null)
static _exists(int $id, bool $reference=false, ?string $type=null)
removeExistingSolutions(int $activeId, int $pass)
setDefaultNrOfTries(int $defaultnroftries)
saveToDb(?int $original_id=null)
static _getMobsOfObject(string $a_type, int $a_id, int $a_usage_hist_nr=0, string $a_lang="-")
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
fetchIndexedValuesFromValuePairs(array $value_pairs)
static saveOriginalId(int $questionId, int $originalId)
static _removeUsage(int $a_mob_id, string $a_type, int $a_id, int $a_usage_hist_nr=0, string $a_lang="-")
Remove usage of mob in another container.
const DEFAULT_THUMB_SIZE
static _updateQuestionCount(int $object_id)
savePreviewData(ilAssQuestionPreviewSession $preview_session)
ilAssQuestionLifecycle $lifecycle
fetchValuePairsFromIndexedValues(array $indexedValues)
createNewQuestion(bool $a_create_page=true)
Creates a new question without an owner when a new question is created This assures that an ID is giv...
isAdditionalContentEditingModePageObject()
duplicateIntermediateSolutionAuthorized(int $activeId, int $passIndex)
const KEY_VALUES_IMPLOSION_SEPARATOR
toQuestionAdministrationInteraction(AdditionalInformationGenerator $additional_info, int $test_ref_id, TestQuestionAdministrationInteractionTypes $interaction_type)
static $imageSourceFixReplaceMap
static _getScoreCutting(int $active_id)
Determines if the score of a question should be cut at 0 points or the score of the whole test...
getSolutionMaxPass(int $active_id)
isValidAdditionalContentEditingMode(string $additionalContentEditingMode)
static extendedTrim(string $value)
Trim non-printable characters from the beginning and end of a string.
removeCurrentSolution(int $active_id, int $pass, bool $authorized=true)
array $suggested_solutions
lmMigrateQuestionTypeGenericContent(ilAssSelfAssessmentMigrator $migrator)
static lookupParentObjId(int $question_id)
setId(int $id=-1)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
ilDBInterface $db
__construct(Container $dic, ilPlugin $plugin)
onCopy(int $sourceParentId, int $sourceQuestionId, int $targetParentId, int $targetQuestionId)
ilObjUser $current_user
const ADDITIONAL_CONTENT_EDITING_MODE_RTE
setOriginalId(?int $original_id)
setExternalId(?string $external_id)
getTestOutputSolutions(int $activeId, int $pass)
setShuffler(Transformation $shuffler)
static convertISO8601FormatH_i_s_ExtendedToSeconds(string $time)
setTitle(string $title="")
deleteSolutionRecordByValues(int $activeId, int $passIndex, bool $authorized, array $matchValues)
A transformation is a function from one datatype to another.
getAnswerTableName()
static signFile(string $path_to_file)
authorizedSolutionExists(int $active_id, ?int $pass)
setLifecycle(ilAssQuestionLifecycle $lifecycle)
fromXML(string $importdirectory, int $user_id, ilQTIItem $item, int $questionpool_id, ?int $tst_id, ?ilObject &$tst_object, int &$question_counter, array $import_mapping)
comment()
description: > Example for rendring a comment glyph.
Definition: comment.php:41
getCurrentSolutionResultSet(int $active_id, int $pass, bool $authorized=true)
getReachedPoints(int $active_id, int $pass)
isAnswered(int $active_id, int $pass)
addAnswerOptionValue(int $qIndex, string $answerOptionValue, float $points)
forceExistingIntermediateSolution(int $activeId, int $passIndex, bool $considerDummyRecordCreation)
static $force_pass_results_update_enabled
static _updateObjectiveResult(int $a_user_id, int $a_active_id, int $a_question_id)
GeneralQuestionPropertiesRepository $questionrepository
static fixSvgToPng(string $imageFilenameContainingString)
const HAS_SPECIFIC_FEEDBACK
lookupMaxStep(int $active_id, int $pass)
calculateReachedPointsFromPreviewSession(ilAssQuestionPreviewSession $preview_session)
setAuthor(string $author="")
isNonEmptyItemListPostSubmission(string $post_submission_field_name)
static prepareTextareaOutput(string $txt_output, bool $prepare_for_latex_output=false, bool $omitNl2BrWhenTextArea=false)
Prepares a string for a text area output where latex code may be in it If the text is HTML-free...
duplicateSuggestedSolutionFiles(int $parent_id, int $question_id)
Duplicates the files of a suggested solution if the question is duplicated.
string $additionalContentEditingMode
$service
Definition: ltiservices.php:40
copySuggestedSolutions(int $target_question_id)
getSolutionRecordById(int $solutionId)
setShuffle(?bool $shuffle=true)
setAdditionalContentEditingMode(?string $additionalContentEditingMode)
static explodeKeyValues(string $keyValues)
getInternalLinkHref(string $target)
static getFeedbackClassNameByQuestionType(string $questionType)
setQuestion(string $question="")
string $export_image_path