ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
class.assQuestion.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
21use ILIAS\Test\Results\Data\Repository as TestResultRepository;
39use ILIAS\Notes\Service as NotesService;
40use ILIAS\Notes\InternalDataService as NotesInternalDataService;
41use ILIAS\Notes\NoteDBRepository as NotesRepo;
45use ILIAS\Refinery\Factory as Refinery;
46use ILIAS\HTTP\Services as HTTPServices;
47
52abstract 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;
83 protected Container $dic;
84
86 protected \ilAssQuestionLifecycle $lifecycle;
87 public \ilAssQuestionFeedback $feedbackOBJ;
88 protected \ilAssQuestionPage $page;
89 protected TestResultRepository $test_result_repository;
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 {
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
253 {
254 $this->processLocker = $processLocker;
255 }
256
258 {
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 {
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
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 {
511 return $this->suggested_solutions;
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 }
718 return $this->export_image_path;
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
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
1361 ilObjQuestionPool::_updateQuestionCount($this->getObjId());
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());
1507 ilFileUtils::delDir($this->getSuggestedSolutionPath());
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
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);
1942 if ($this->isAdditionalContentEditingModePageObject()
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 {
2179 return $this->selfassessmenteditingmode;
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 {
2285 return $this->additionalContentEditingMode;
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 {
2299 return $this->getAdditionalContentEditingMode() == assQuestion::ADDITIONAL_CONTENT_EDITING_MODE_IPE;
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
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
2533 public const KEY_VALUES_IMPLOSION_SEPARATOR = ':';
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
2834 return $this->test_question_config;
2835 }
2836
2838 {
2839 return new ilTestQuestionConfig();
2840 }
2841
2843 {
2844 return $this->suggestedsolution_repo;
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
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
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
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
2947 int $active_id,
2948 int $pass
2949 ): array {
2950 return [];
2951 }
2952}
$id
plugin.php for ilComponentBuildPluginInfoObjectiveTest::testAddPlugins
Definition: plugin.php:23
$lifecycle
$comment
Definition: buildRTE.php:72
Customizing of pimple-DIC for ILIAS.
Definition: Container.php:36
Provides fluid interface to LoggingServices.
Builds data types.
Definition: Factory.php:36
Class Services.
Definition: Services.php:38
array $suggested_solutions
calculateReachedPoints(int $active_id, ?int $pass=null, bool $authorized_solution=true)
fixSvgToPng(string $imageFilenameContainingString)
isAddableAnswerOptionValue(int $qIndex, string $answerOptionValue)
const KEY_VALUES_IMPLOSION_SEPARATOR
loadFromDb(int $question_id)
string $export_image_path
getSolutionValues(int $active_id, ?int $pass=null, bool $authorized=true)
Loads solutions of a given user from the database an returns it.
const HAS_SPECIFIC_FEEDBACK
const ADDITIONAL_CONTENT_EDITING_MODE_RTE
deletePageOfQuestion(int $question_id)
persistPreviewState(ilAssQuestionPreviewSession $preview_session)
persists the preview state for current user and question
ilAssQuestionLifecycle $lifecycle
setOriginalId(?int $original_id)
static _getReachedPoints(int $active_id, int $question_id, int $pass)
ilDBInterface $db
ensureNonNegativePoints(float $points)
const ADDITIONAL_CONTENT_EDITING_MODE_IPE
copySuggestedSolutions(int $target_question_id)
setProcessLocker(ilAssQuestionProcessLocker $processLocker)
ilObjUser $current_user
static lookupParentObjId(int $question_id)
setId(int $id=-1)
const MAXIMUM_THUMB_SIZE
LoggingServices $log
calculateReachedPointsFromPreviewSession(ilAssQuestionPreviewSession $preview_session)
updateCurrentSolutionsAuthorization(int $activeId, int $pass, bool $authorized, bool $keepTime=false)
RequestDataCollector $questionpool_request
duplicateComments(int $parent_source_id, int $source_id, int $parent_target_id, int $target_id)
lmMigrateQuestionTypeGenericContent(ilAssSelfAssessmentMigrator $migrator)
const MINIMUM_THUMB_SIZE
duplicateSuggestedSolutionFiles(int $parent_id, int $question_id)
Duplicates the files of a suggested solution if the question is duplicated.
answerToParticipantInteraction(AdditionalInformationGenerator $additional_info, int $test_ref_id, int $active_id, int $pass, string $source_ip, TestParticipantInteractionTypes $interaction_type)
Refinery $refinery
onDuplicate(int $original_parent_id, int $original_question_id, int $duplicate_parent_id, int $duplicate_question_id)
ilGlobalPageTemplate $tpl
fromXML(string $importdirectory, int $user_id, ilQTIItem $item, int $questionpool_id, ?int $tst_id, ?ilObject &$tst_object, int &$question_counter, array $import_mapping)
authorizedOrIntermediateSolutionExists(int $active_id, int $pass)
setAdditionalContentEditingMode(?string $additionalContentEditingMode)
deleteSolutionRecordByValues(int $activeId, int $passIndex, bool $authorized, array $matchValues)
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.
TestResultRepository $test_result_repository
buildHashedImageFilename(string $plain_image_filename, bool $unique=false)
authorizedSolutionExists(int $active_id, ?int $pass)
fetchValuePairsFromIndexedValues(array $indexedValues)
lmMigrateQuestionTypeSpecificContent(ilAssSelfAssessmentMigrator $migrator)
isPreviewSolutionCorrect(ilAssQuestionPreviewSession $preview_session)
static isFileAvailable(string $file)
removeAllImageFiles(string $image_target_path)
saveToDb(?int $original_id=null)
ilTestQuestionConfig $test_question_config
onCopy(int $sourceParentId, int $sourceQuestionId, int $targetParentId, int $targetQuestionId)
saveCurrentSolution(int $active_id, int $pass, $value1, $value2, bool $authorized=true, $tstamp=0)
getSolutionForTextOutput(int $active_id, int $pass)
removeResultRecord(int $activeId, int $pass)
deleteAdditionalTableData(int $question_id)
syncSkillAssignments(int $srcParentId, int $srcQuestionId, int $trgParentId, int $trgQuestionId)
getInternalLinkHref(string $target)
clonePageOfQuestion(int $a_q_id)
setShuffle(?bool $shuffle=true)
setTestId(int $id=-1)
GeneralQuestionPropertiesRepository $questionrepository
static $force_pass_results_update_enabled
ilTestQuestionConfig $testQuestionConfig
cloneXHTMLMediaObjectsOfQuestion(int $source_question_id)
solutionValuesToText(array $solution_values)
MUST convert the given solution values into text.
static _questionExistsInTest(int $question_id, int $test_id)
resetUsersAnswer(int $activeId, int $pass)
static extendedTrim(string $value)
Trim non-printable characters from the beginning and end of a string.
afterSyncWithOriginal(int $original_question_id, int $clone_question_id, int $original_parent_id, int $clone_parent_id)
migrateContentForLearningModule(ilAssSelfAssessmentMigrator $migrator)
savePreviewData(ilAssQuestionPreviewSession $preview_session)
getAdditionalTableName()
updateCurrentSolution(int $solutionId, $value1, $value2, bool $authorized=true)
ilLanguage $lng
deleteAnswers(int $question_id)
getSolutionRecordById(int $solutionId)
setQuestion(string $question="")
ilAssQuestionProcessLocker $processLocker
isValidAdditionalContentEditingMode(string $additionalContentEditingMode)
isAnswered(int $active_id, int $pass)
SkillUsageService $skillUsageService
static _getSolutionMaxPass(int $question_id, int $active_id)
Returns the maximum pass a users question solution.
isNonEmptyItemListPostSubmission(string $post_submission_field_name)
getImagePath($question_id=null, $object_id=null)
Returns the image path for web accessable images of a question.
getVariablesAsTextArray(int $active_id, int $pass)
getCurrentSolutionResultSet(int $active_id, int $pass, bool $authorized=true)
setPreventRteUsage(bool $prevent_rte_usage)
setSelfAssessmentEditingMode(bool $selfassessmenteditingmode)
calculateResultsFromSolution(int $active_id, int $pass)
Calculates the question results from a previously saved question solution.
setAuthor(string $author="")
getSuggestedSolution(int $subquestion_index=0)
getCorrectSolutionForTextOutput(int $active_id, int $pass)
getTestOutputSolutions(int $activeId, int $pass)
setThumbSize(int $a_size)
copyObject(int $target_parent_id, string $title='')
removeExistingSolutions(int $activeId, int $pass)
getValidAdditionalContentEditingModes()
static implodeKeyValues(array $keyValues)
static explodeKeyValues(string $keyValues)
toQuestionAdministrationInteraction(AdditionalInformationGenerator $additional_info, int $test_ref_id, TestQuestionAdministrationInteractionTypes $interaction_type)
setExportImagePath(string $path)
deleteDummySolutionRecord(int $activeId, int $passIndex)
static convertISO8601FormatH_i_s_ExtendedToSeconds(string $time)
getActiveUserData(int $active_id)
Returns the user id and the test id for a given active id.
static saveOriginalId(int $questionId, int $originalId)
static setForcePassResultUpdateEnabled(bool $force_pass_results_update_enabled)
static isForcePassResultUpdateEnabled()
ilAssQuestionPage $page
QuestionFiles $question_files
static _getSuggestedSolutionOutput(int $question_id)
toLog(AdditionalInformationGenerator $additional_info)
MUST return an array of the question settings that can be stored in the log.
bool $selfassessmenteditingmode
persistWorkingState(int $active_id, $pass, bool $authorized=true)
persists the working state for current testactive and testpass
getUserSolutionPreferingIntermediate(int $active_id, ?int $pass=null)
addAnswerOptionValue(int $qIndex, string $answerOptionValue, float $points)
cloneQuestionTypeSpecificProperties(self $target)
purifyAndPrepareTextAreaOutput(string $content)
duplicate(bool $for_test=true, string $title='', string $author='', int $owner=-1, $test_obj_id=null)
removeIntermediateSolution(int $active_id, int $pass)
setComment(string $comment="")
Transformation $shuffler
duplicateSkillAssignments(int $srcParentId, int $srcQuestionId, int $trgParentId, int $trgQuestionId)
setObjId(int $obj_id=0)
SuggestedSolutionsDatabaseRepository $suggestedsolution_repo
removeSolutionRecordById(int $solutionId)
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...
forceExistingIntermediateSolution(int $activeId, int $passIndex, bool $considerDummyRecordCreation)
ilAssQuestionFeedback $feedbackOBJ
static getFeedbackClassNameByQuestionType(string $questionType)
setLastChange(int $lastChange)
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.
getSolutionMaxPass(int $active_id)
HTTPServices $http
getAnswerTableName()
setOwner(int $owner=-1)
setNrOfTries(int $a_nr_of_tries)
setLifecycle(ilAssQuestionLifecycle $lifecycle)
getAdjustedReachedPoints(int $active_id, int $pass, bool $authorized_solution=true)
static getNumExistingSolutionRecords(int $activeId, int $pass, int $questionId)
fixUnavailableSkinImageSources(string $html)
resolveInternalLink(string $internal_link)
static instantiateQuestion(int $question_id)
isAdditionalContentEditingModePageObject()
setExternalId(?string $external_id)
lookupForExistingSolutions(int $activeId, int $pass)
Lookup if an authorized or intermediate solution exists.
adjustReachedPointsByScoringOptions(float $points, int $active_id)
Adjust the given reached points by checks for all special scoring options in the test container.
static resetOriginalId(int $questionId)
setTitle(string $title="")
removeCurrentSolution(int $active_id, int $pass, bool $authorized=true)
isDummySolutionRecord(array $solutionRecord)
fetchIndexedValuesFromValuePairs(array $value_pairs)
saveQuestionDataToDb(?int $original_id=null)
setDefaultNrOfTries(int $defaultnroftries)
cloneSuggestedSolutions(int $source_question_id, int $target_question_id)
intermediateSolutionExists(int $active_id, int $pass)
answerToLog(AdditionalInformationGenerator $additional_info, int $active_id, int $pass)
duplicateIntermediateSolutionAuthorized(int $activeId, int $passIndex)
getImagePathWeb()
Returns the web image path for web accessable images of a question.
string $additionalContentEditingMode
getReachedPoints(int $active_id, int $pass)
setShuffler(Transformation $shuffler)
createNewOriginalFromThisDuplicate(int $target_parent_id, string $target_question_title='')
lookupMaxStep(int $active_id, int $pass)
const DEFAULT_THUMB_SIZE
setPoints(float $points)
cloneSuggestedSolutionFiles(int $source_question_id, int $target_question_id)
saveWorkingData(int $active_id, ?int $pass=null, bool $authorized=true)
string $questionActionCmd
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static _updateObjectiveResult(int $a_user_id, int $a_active_id, int $a_question_id)
Base class for ILIAS Exception handling.
static makeDirParents(string $a_dir)
Create a new directory and all parent directories.
static getASCIIFilename(string $a_filename)
static delDir(string $a_dir, bool $a_clean_only=false)
removes a dir and all its content (subdirs and files) recursively
static removeTrailingPathSeparators(string $path)
static getInstanceByType(string $type)
static _getIdForImportId(string $a_import_id)
get current object id for import id (static)
language handling
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,...
static prepareFormOutput($a_str, bool $a_strip=false)
static _exists(int $id, bool $reference=false, ?string $type=null)
checks if an object exists in object_data
static _getMobsOfObject(string $a_type, int $a_id, int $a_usage_hist_nr=0, string $a_lang="-")
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.
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.
static _isWriteable($object_id, $user_id)
Returns true, if the question pool is writeable by a given user.
static _updateQuestionCount(int $object_id)
static getUsageOfObject(int $a_obj_id, bool $a_include_titles=false)
static _getCountSystem($active_id)
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.
static _lookupAuthor($obj_id)
Gets the authors name of the ilObjTest object.
static _getPass($active_id)
Retrieves the actual pass of a given user for a given test.
static _getUserIdFromActiveId(int $active_id)
User class.
Class ilObject Basic functions for all objects.
static _getAllReferences(int $id)
get all reference ids for object ID
static _lookupTitle(int $obj_id)
static _exists(string $a_parent_type, int $a_id, string $a_lang="", bool $a_no_cache=false)
Checks whether page exists.
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.
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...
ILIAS Setting Class.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
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 setTokenMaxLifetimeInSeconds(int $token_max_lifetime_in_seconds)
static signFile(string $path_to_file)
const CLIENT_WEB_DIR
Definition: constants.php:47
const IL_INST_ID
Definition: constants.php:40
return['delivery_method'=> 'php',]
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
A transformation is a function from one datatype to another.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Interface ilDBInterface.
Interface ilDBStatement.
Interface for html sanitizing functionality.
$service
Definition: ltiresult.php:36
$path
Definition: ltiservices.php:30
$res
Definition: ltiservices.php:69
static http()
Fetches the global http state from ILIAS.
__construct(Container $dic, ilPlugin $plugin)
@inheritDoc
Interface Observer \BackgroundTasks Contains several chained tasks and infos about them.
global $lng
Definition: privfeed.php:31
if(!file_exists('../ilias.ini.php'))
global $DIC
Definition: shib_login.php:26