ILIAS  release_8 Revision v8.24
class.assQuestion.php
Go to the documentation of this file.
1<?php
2
20use ILIAS\TA\Questions\assQuestionSuggestedSolution;
21use ILIAS\TA\Questions\assQuestionSuggestedSolutionsDatabaseRepository;
23use Psr\Http\Message\ServerRequestInterface;
24
25require_once './Modules/Test/classes/inc.AssessmentConstants.php';
26
41abstract class assQuestion
42{
43 public const IMG_MIME_TYPE_JPG = 'image/jpeg';
44 public const IMG_MIME_TYPE_PNG = 'image/png';
45 public const IMG_MIME_TYPE_GIF = 'image/gif';
46
47 protected const HAS_SPECIFIC_FEEDBACK = true;
48
49 protected static $allowedFileExtensionsByMimeType = array(
50 self::IMG_MIME_TYPE_JPG => ['jpg', 'jpeg'],
51 self::IMG_MIME_TYPE_PNG => ['png'],
52 self::IMG_MIME_TYPE_GIF => ['gif']
53 );
54
55 protected static $allowedCharsetsByMimeType = array(
56 self::IMG_MIME_TYPE_JPG => ['binary'],
57 self::IMG_MIME_TYPE_PNG => ['binary'],
58 self::IMG_MIME_TYPE_GIF => ['binary']
59 );
60
61 private const DEFAULT_THUMB_SIZE = 150;
62 private const MINIMUM_THUMB_SIZE = 20;
63 private const MAXIMUM_THUMB_SIZE = 8192;
64
65 public const TRIM_PATTERN = '/^[\p{C}\p{Z}]+|[\p{C}\p{Z}]+$/u';
66
69
71
72 protected int $id;
73 protected string $title;
74 protected string $comment;
75 protected string $owner;
76 protected string $author;
77 protected int $thumb_size;
78
82 protected string $question;
83
87 protected float $points;
88
92 protected bool $shuffle;
93
97 protected int $test_id;
98
102 protected int $obj_id = 0;
103
109 protected $ilias;
110
112
113 protected ilLanguage $lng;
114
116
117 protected Container $dic;
118
123
129 protected array $suggested_solutions;
130
131 protected ?int $original_id = null;
132
138 protected $page;
139
140 private int $nr_of_tries;
141
145 private string $export_image_path;
146
147 protected ?string $external_id = null;
148
149 public const ADDITIONAL_CONTENT_EDITING_MODE_RTE = 'default';
150 public const ADDITIONAL_CONTENT_EDITING_MODE_IPE = 'pageobject';
151
152 private string $additionalContentEditingMode = '';
153
154 public \ilAssQuestionFeedback $feedbackOBJ;
155
156 public bool $prevent_rte_usage = false;
157
158 public bool $selfassessmenteditingmode = false;
159
160 public int $defaultnroftries = 0;
161
162 protected \ilAssQuestionProcessLocker $processLocker;
163
164 public string $questionActionCmd = 'handleQuestionAction';
165
169 protected $step;
170
171 protected $lastChange;
172
174
175 private bool $obligationsToBeConsidered = false;
176
178
180
182 'image/jpeg' => ['jpg', 'jpeg'],
183 'image/png' => ['png'],
184 'image/gif' => ['gif']
185 );
186
188
192 public function __construct(
193 string $title = "",
194 string $comment = "",
195 string $author = "",
196 int $owner = -1,
197 string $question = ""
198 ) {
199 global $DIC;
200 $this->dic = $DIC;
201 $lng = $DIC['lng'];
202 $tpl = $DIC['tpl'];
203 $ilDB = $DIC['ilDB'];
204 $ilLog = $DIC->logger();
205
206 $this->current_user = $DIC['ilUser'];
207 $this->lng = $lng;
208 $this->tpl = $tpl;
209 $this->db = $ilDB;
210 $this->ilLog = $ilLog;
211 $this->http = $DIC->http();
212 $this->refinery = $DIC->refinery();
213
214 $this->thumb_size = self::DEFAULT_THUMB_SIZE;
215
216 $this->title = $title;
217 $this->comment = $comment;
218 $this->setAuthor($author);
219 $this->setOwner($owner);
220
221 $this->setQuestion($question);
222
223 $this->id = -1;
224 $this->test_id = -1;
225 $this->suggested_solutions = [];
226 $this->shuffle = 1;
227 $this->nr_of_tries = 0;
228 $this->setExternalId(null);
229
230 $this->questionActionCmd = 'handleQuestionAction';
231 $this->export_image_path = '';
232 $this->shuffler = $DIC->refinery()->random()->dontShuffle();
233 $this->lifecycle = ilAssQuestionLifecycle::getDraftInstance();
234 }
235
236 protected static $forcePassResultsUpdateEnabled = false;
237
239 {
240 self::$forcePassResultsUpdateEnabled = $forcePassResultsUpdateEnabled;
241 }
242
243 public static function isForcePassResultUpdateEnabled(): bool
244 {
246 }
247
248 public static function isAllowedImageMimeType($mimeType): bool
249 {
250 return (bool) count(self::getAllowedFileExtensionsForMimeType($mimeType));
251 }
252
253 public static function fetchMimeTypeIdentifier(string $contentType): string
254 {
255 return current(explode(';', $contentType));
256 }
257
258 public static function getAllowedFileExtensionsForMimeType(string $mimeType): array
259 {
260 foreach (self::$allowedFileExtensionsByMimeType as $allowedMimeType => $extensions) {
261 $rexCharsets = implode('|', self::$allowedCharsetsByMimeType[$allowedMimeType]);
262 $rexMimeType = preg_quote($allowedMimeType, '/');
263
264 $rex = '/^' . $rexMimeType . '(;(\s)*charset=(' . $rexCharsets . '))*$/';
265
266 if (!preg_match($rex, $mimeType)) {
267 continue;
268 }
269
270 return $extensions;
271 }
272
273 return [];
274 }
275
276 public static function isAllowedImageFileExtension(string $mimeType, string $fileExtension): bool
277 {
278 return in_array(strtolower($fileExtension), self::getAllowedFileExtensionsForMimeType($mimeType), true);
279 }
280
281 // hey: prevPassSolutions - question action actracted (heavy use in fileupload refactoring)
282 private function generateExternalId(int $question_id): string
283 {
284 if ($question_id > 0) {
285 return 'il_' . IL_INST_ID . '_qst_' . $question_id;
286 }
287
288 return uniqid('', true);
289 }
290
291 protected function getQuestionAction(): string
292 {
293 if (!isset($_POST['cmd']) || !isset($_POST['cmd'][$this->questionActionCmd])) {
294 return '';
295 }
296
297 if (!is_array($_POST['cmd'][$this->questionActionCmd]) || !count($_POST['cmd'][$this->questionActionCmd])) {
298 return '';
299 }
300
301 return key($_POST['cmd'][$this->questionActionCmd]);
302 }
303
304 protected function isNonEmptyItemListPostSubmission(string $postSubmissionFieldname): bool
305 {
306 if (!isset($_POST[$postSubmissionFieldname])) {
307 return false;
308 }
309
310 if (!is_array($_POST[$postSubmissionFieldname])) {
311 return false;
312 }
313
314 if (!count($_POST[$postSubmissionFieldname])) {
315 return false;
316 }
317
318 return true;
319 }
320
321 protected function ensureCurrentTestPass(int $active_id, int $pass): int
322 {
323 if (is_int($pass) && $pass >= 0) {
324 return $pass;
325 }
326
327 return $this->lookupCurrentTestPass($active_id, $pass);
328 }
329
334 protected function lookupCurrentTestPass(int $active_id, int $pass): int
335 {
336 return \ilObjTest::_getPass($active_id);
337 }
338
342 protected function lookupTestId(int $active_id): int
343 {
344 $result = $this->db->queryF(
345 "SELECT test_fi FROM tst_active WHERE active_id = %s",
346 array('integer'),
347 array($active_id)
348 );
349 $test_id = -1;
350 if ($this->db->numRows($result) > 0) {
351 $row = $this->db->fetchAssoc($result);
352 $test_id = (int) $row["test_fi"];
353 }
354
355 return $test_id;
356 }
357
358 protected function log(int $active_id, string $langVar): void
359 {
361 $message = $this->lng->txtlng('assessment', $langVar, ilObjAssessmentFolder::_getLogLanguage());
362 assQuestion::logAction($message, $active_id, $this->getId());
363 }
364 }
365
369 public static function getAllowedImageMaterialFileExtensions(): array
370 {
371 $extensions = [];
372
373 foreach (self::$allowedImageMaterialFileExtensionsByMimeType as $mimeType => $mimeExtensions) {
375 $extensions = array_merge($extensions, $mimeExtensions);
376 }
377 return array_unique($extensions);
378 }
379
380 public function getShuffler(): Transformation
381 {
382 return $this->shuffler;
383 }
384
385 public function setShuffler(Transformation $shuffler): void
386 {
387 $this->shuffler = $shuffler;
388 }
389
391 {
392 $this->processLocker = $processLocker;
393 }
394
396 {
398 }
399
410 public function fromXML($item, int $questionpool_id, ?int $tst_id, &$tst_object, int &$question_counter, array $import_mapping, array &$solutionhints = []): array
411 {
412 $classname = $this->getQuestionType() . "Import";
413 $import = new $classname($this);
414 $import_mapping = $import->fromXML($item, $questionpool_id, $tst_id, $tst_object, $question_counter, $import_mapping);
415
416 foreach ($solutionhints as $hint) {
417 $h = new ilAssQuestionHint();
418 $h->setQuestionId($import->getQuestionId());
419 $h->setIndex($hint['index'] ?? "");
420 $h->setPoints($hint['points'] ?? "");
421 $h->setText($hint['txt'] ?? "");
422 $h->save();
423 }
424 return $import_mapping;
425 }
426
432 public function toXML(
433 bool $a_include_header = true,
434 bool $a_include_binary = true,
435 bool $a_shuffle = false,
436 bool $test_output = false,
437 bool $force_image_references = false
438 ): string {
439 $classname = $this->getQuestionType() . "Export";
440 $export = new $classname($this);
441 return $export->toXML($a_include_header, $a_include_binary, $a_shuffle, $test_output, $force_image_references);
442 }
443
449 public function isComplete(): bool
450 {
451 return false;
452 }
453
457 public function questionTitleExists(int $questionpool_id, string $title): bool
458 {
459 global $DIC;
460 $ilDB = $DIC['ilDB'];
461
462 $result = $ilDB->queryF(
463 "SELECT * FROM qpl_questions WHERE obj_fi = %s AND title = %s",
464 array('integer','text'),
465 array($questionpool_id, $title)
466 );
467 return ($result->numRows() > 0) ? true : false;
468 }
469
470 public function setTitle(string $title = ""): void
471 {
472 $this->title = $title;
473 }
474
475 public function setId(int $id = -1): void
476 {
477 $this->id = $id;
478 }
479
480 public function setTestId(int $id = -1): void
481 {
482 $this->test_id = $id;
483 }
484
485 public function setComment(string $comment = ""): void
486 {
487 $this->comment = $comment;
488 }
489
490 public function setOutputType(int $outputType = OUTPUT_HTML): void
491 {
492 $this->outputType = $outputType;
493 }
494
495 public function setShuffle(?bool $shuffle = true): void
496 {
497 $this->shuffle = $shuffle ?? false;
498 }
499
500 public function setAuthor(string $author = ""): void
501 {
502 if (!$author) {
503 $author = $this->current_user->getFullname();
504 }
505 $this->author = $author;
506 }
507
508 public function setOwner(int $owner = -1): void
509 {
510 $this->owner = $owner;
511 }
512
513 public function getTitle(): string
514 {
515 return $this->title;
516 }
517
518 public function getTitleForHTMLOutput(): string
519 {
520 return $this->refinery->string()->stripTags()->transform($this->title);
521 }
522
523 public function getTitleFilenameCompliant(): string
524 {
525 return ilFileUtils::getASCIIFilename($this->getTitle());
526 }
527
528 public function getId(): int
529 {
530 return $this->id;
531 }
532
533 public function getShuffle(): bool
534 {
535 return $this->shuffle;
536 }
537
538 public function getTestId(): int
539 {
540 return $this->test_id;
541 }
542
543 public function getComment(): string
544 {
545 return $this->comment;
546 }
547
548 public function getDescriptionForHTMLOutput(): string
549 {
550 return $this->refinery->string()->stripTags()->transform($this->comment);
551 }
552
553 public function getThumbSize(): int
554 {
555 return $this->thumb_size;
556 }
557
558 public function setThumbSize(int $a_size): void
559 {
560 if ($a_size >= self::MINIMUM_THUMB_SIZE) {
561 $this->thumb_size = $a_size;
562 } else {
563 throw new ilException("Thumb size must be at least " . self::MINIMUM_THUMB_SIZE . "px");
564 }
565 }
566
567 public function getMinimumThumbSize(): int
568 {
569 return self::MINIMUM_THUMB_SIZE;
570 }
571
572 public function getMaximumThumbSize(): int
573 {
574 return self::MAXIMUM_THUMB_SIZE;
575
576 }
577
578 public function getOutputType(): int
579 {
580 return $this->outputType;
581 }
582
583 public function supportsJavascriptOutput(): bool
584 {
585 return false;
586 }
587
588 public function supportsNonJsOutput(): bool
589 {
590 return true;
591 }
592
593 public function requiresJsSwitch(): bool
594 {
595 return $this->supportsJavascriptOutput() && $this->supportsNonJsOutput();
596 }
597
598 public function getAuthor(): string
599 {
600 return $this->author;
601 }
602
603 public function getAuthorForHTMLOutput(): string
604 {
605 return $this->refinery->string()->stripTags()->transform($this->author);
606 }
607
608 public function getOwner(): int
609 {
610 return $this->owner;
611 }
612
613 public function getObjId(): int
614 {
615 return $this->obj_id;
616 }
617
618 public function setObjId(int $obj_id = 0): void
619 {
620 $this->obj_id = $obj_id;
621 }
622
624 {
625 return $this->lifecycle;
626 }
627
628 public function setLifecycle(ilAssQuestionLifecycle $lifecycle): void
629 {
630 $this->lifecycle = $lifecycle;
631 }
632
633 public function setExternalId(?string $external_id): void
634 {
635 $this->external_id = $external_id;
636 }
637
638 public function getExternalId(): string
639 {
640 if ($this->external_id === null || $this->external_id === '') {
641 return $this->generateExternalId($this->getId());
642 }
643
644 return $this->external_id;
645 }
646
650 public static function _getMaximumPoints(int $question_id): float
651 {
652 global $DIC;
653 $ilDB = $DIC['ilDB'];
654
655 $points = 0.0;
656 $result = $ilDB->queryF(
657 "SELECT points FROM qpl_questions WHERE question_id = %s",
658 array('integer'),
659 array($question_id)
660 );
661 if ($ilDB->numRows($result) == 1) {
662 $row = $ilDB->fetchAssoc($result);
663 $points = (float) $row["points"];
664 }
665 return $points;
666 }
667
671 public static function _getQuestionInfo(int $question_id): array
672 {
673 global $DIC;
674 $ilDB = $DIC['ilDB'];
675
676 $result = $ilDB->queryF(
677 "SELECT qpl_questions.*, qpl_qst_type.type_tag FROM qpl_qst_type, qpl_questions WHERE qpl_questions.question_id = %s AND qpl_questions.question_type_fi = qpl_qst_type.question_type_id",
678 array('integer'),
679 array($question_id)
680 );
681
682 if ($ilDB->numRows($result)) {
683 return $ilDB->fetchAssoc($result);
684 }
685 return [];
686 }
687
688 public static function _getSuggestedSolutionCount(int $question_id): int
689 {
690 global $DIC;
691 $ilDB = $DIC['ilDB'];
692
693 $result = $ilDB->queryF(
694 "SELECT suggested_solution_id FROM qpl_sol_sug WHERE question_fi = %s",
695 array('integer'),
696 array($question_id)
697 );
698 return $ilDB->numRows($result);
699 }
700
705 public static function _getSuggestedSolutionOutput(int $question_id): string
706 {
707 $question = self::_instantiateQuestion($question_id);
708 if (!is_object($question)) {
709 return "";
710 }
711 return $question->getSuggestedSolutionOutput();
712 }
713
718 public function getSuggestedSolutionOutput(): string
719 {
720 $output = [];
721 foreach ($this->suggested_solutions as $solution) {
722 switch ($solution["type"]) {
723 case "lm":
724 case "st":
725 case "pg":
726 case "git":
727 $output[] = '<a href="' . assQuestion::_getInternalLinkHref($solution["internal_link"]) . '">' . $this->lng->txt("solution_hint") . '</a>';
728 break;
729 case "file":
730 $possible_texts = array_values(array_filter(array(
731 ilLegacyFormElementsUtil::prepareFormOutput($solution['value']['filename']),
732 ilLegacyFormElementsUtil::prepareFormOutput($solution['value']['name']),
733 $this->lng->txt('tst_show_solution_suggested')
734 )));
736 $output[] = '<a href="' . ilWACSignedPath::signFile($this->getSuggestedSolutionPathWeb() . $solution["value"]["name"]) . '">' . $possible_texts[0] . '</a>';
737 break;
738 case "text":
739 $solutionValue = $solution["value"];
740 $solutionValue = $this->fixSvgToPng($solutionValue);
741 $solutionValue = $this->fixUnavailableSkinImageSources($solutionValue);
742 $output[] = $this->prepareTextareaOutput($solutionValue, true);
743 break;
744 }
745 }
746 return implode("<br />", $output);
747 }
748
753 public function _getSuggestedSolution(int $question_id, int $subquestion_index = 0): array
754 {
755 return $this->loadSuggestedSolution($question_id, $subquestion_index);
756 }
757
763 public function loadSuggestedSolution(int $question_id, int $subquestion_index = 0): array
764 {
765 $result = $this->db->queryF(
766 "SELECT * FROM qpl_sol_sug WHERE question_fi = %s AND subquestion_index = %s",
767 array('integer','integer'),
768 array($question_id, $subquestion_index)
769 );
770 if ($this->db->numRows($result) == 1) {
771 $row = $this->db->fetchAssoc($result);
772 return array(
773 "internal_link" => $row["internal_link"],
774 "import_id" => $row["import_id"]
775 );
776 }
777 return [];
778 }
779
783 public function getSuggestedSolutions(): array
784 {
785 return $this->suggested_solutions;
786 }
787
788 public static function _getReachedPoints(int $active_id, int $question_id, int $pass): float
789 {
790 global $DIC;
791 $ilDB = $DIC['ilDB'];
792
793 $points = 0.0;
794
795 $result = $ilDB->queryF(
796 "SELECT * FROM tst_test_result WHERE active_fi = %s AND question_fi = %s AND pass = %s",
797 array('integer','integer','integer'),
798 array($active_id, $question_id, $pass)
799 );
800 if ($result->numRows() == 1) {
801 $row = $ilDB->fetchAssoc($result);
802 $points = (float) $row["points"];
803 }
804 return $points;
805 }
806
807 public function getReachedPoints(int $active_id, int $pass): float
808 {
809 return round(self::_getReachedPoints($active_id, $this->getId(), $pass), 2);
810 }
811
812 public function getMaximumPoints(): float
813 {
814 return $this->points;
815 }
816
824 final public function getAdjustedReachedPoints(int $active_id, int $pass, bool $authorizedSolution = true): float
825 {
826 // determine reached points for submitted solution
827 $reached_points = $this->calculateReachedPoints($active_id, $pass, $authorizedSolution);
828 $hintTracking = new ilAssQuestionHintTracking($this->getId(), $active_id, $pass);
829 $requestsStatisticData = $hintTracking->getRequestStatisticDataByQuestionAndTestpass();
830 $reached_points = $reached_points - $requestsStatisticData->getRequestsPoints();
831
832 // adjust reached points regarding to tests scoring options
833 $reached_points = $this->adjustReachedPointsByScoringOptions($reached_points, $active_id, $pass);
834
835 return $reached_points;
836 }
837
841 final public function calculateResultsFromSolution(int $active_id, int $pass, bool $obligationsEnabled = false): void
842 {
843 global $DIC;
844 $ilDB = $DIC['ilDB'];
845 $ilUser = $DIC['ilUser'];
846
847 // determine reached points for submitted solution
848 $reached_points = $this->calculateReachedPoints($active_id, $pass);
849 $questionHintTracking = new ilAssQuestionHintTracking($this->getId(), $active_id, $pass);
850 $requestsStatisticData = $questionHintTracking->getRequestStatisticDataByQuestionAndTestpass();
851 $reached_points = $reached_points - $requestsStatisticData->getRequestsPoints();
852
853 // adjust reached points regarding to tests scoring options
854 $reached_points = $this->adjustReachedPointsByScoringOptions($reached_points, $active_id, $pass);
855
856 if ($obligationsEnabled && ilObjTest::isQuestionObligatory($this->getId())) {
857 $isAnswered = $this->isAnswered($active_id, $pass);
858 } else {
859 $isAnswered = true;
860 }
861
862 if (is_null($reached_points)) {
863 $reached_points = 0.0;
864 }
865
866 // fau: testNav - check for existing authorized solution to know if a result record should be written
867 $existingSolutions = $this->lookupForExistingSolutions($active_id, $pass);
868
869 $this->getProcessLocker()->executeUserQuestionResultUpdateOperation(function () use ($ilDB, $active_id, $pass, $reached_points, $requestsStatisticData, $isAnswered, $existingSolutions) {
870 $query = "
871 DELETE FROM tst_test_result
872
873 WHERE active_fi = %s
874 AND question_fi = %s
875 AND pass = %s
876 ";
877
878 $types = array('integer', 'integer', 'integer');
879 $values = array($active_id, $this->getId(), $pass);
880
881 if ($this->getStep() !== null) {
882 $query .= "
883 AND step = %s
884 ";
885
886 $types[] = 'integer';
887 $values[] = $this->getStep();
888 }
889 $ilDB->manipulateF($query, $types, $values);
890
891 if ($existingSolutions['authorized']) {
892 $next_id = $ilDB->nextId("tst_test_result");
893 $fieldData = array(
894 'test_result_id' => array('integer', $next_id),
895 'active_fi' => array('integer', $active_id),
896 'question_fi' => array('integer', $this->getId()),
897 'pass' => array('integer', $pass),
898 'points' => array('float', $reached_points),
899 'tstamp' => array('integer', time()),
900 'hint_count' => array('integer', $requestsStatisticData->getRequestsCount()),
901 'hint_points' => array('float', $requestsStatisticData->getRequestsPoints()),
902 'answered' => array('integer', $isAnswered)
903 );
904
905 if ($this->getStep() !== null) {
906 $fieldData['step'] = array('integer', $this->getStep());
907 }
908
909 $ilDB->insert('tst_test_result', $fieldData);
910 }
911 });
912
915 sprintf(
916 $this->lng->txtlng(
917 "assessment",
918 "log_user_answered_question",
920 ),
921 $reached_points
922 ),
923 $active_id,
924 $this->getId()
925 );
926 }
927
928 // update test pass results
929 self::_updateTestPassResults($active_id, $pass, $obligationsEnabled, $this->getProcessLocker());
930 ilCourseObjectiveResult::_updateObjectiveResult($ilUser->getId(), $active_id, $this->getId());
931 }
932
937 final public function persistWorkingState(int $active_id, $pass, bool $obligationsEnabled = false, bool $authorized = true): bool
938 {
939 if (!$this->validateSolutionSubmit() && !$this->savePartial()) {
940 return false;
941 }
942
943 $saveStatus = false;
944
945 $this->getProcessLocker()->executePersistWorkingStateLockOperation(function () use ($active_id, $pass, $authorized, $obligationsEnabled, &$saveStatus) {
946 if ($pass === null) {
947 $pass = ilObjTest::_getPass($active_id);
948 }
949
950 $saveStatus = $this->saveWorkingData($active_id, $pass, $authorized);
951
952 if ($authorized) {
953 // fau: testNav - remove an intermediate solution if the authorized solution is saved
954 // the intermediate solution would set the displayed question status as "editing ..."
955 $this->removeIntermediateSolution($active_id, $pass);
956 // fau.
957 $this->calculateResultsFromSolution($active_id, $pass, $obligationsEnabled);
958 }
959 });
960
961 return $saveStatus;
962 }
963
967 final public function persistPreviewState(ilAssQuestionPreviewSession $previewSession): bool
968 {
969 $this->savePreviewData($previewSession);
970 return $this->validateSolutionSubmit();
971 }
972
973 public function validateSolutionSubmit(): bool
974 {
975 return true;
976 }
977
986 abstract public function saveWorkingData(int $active_id, int $pass, bool $authorized = true): bool;
987
988 protected function savePreviewData(ilAssQuestionPreviewSession $previewSession): void
989 {
990 $previewSession->setParticipantsSolution($this->getSolutionSubmit());
991 }
992
994 public static function _updateTestResultCache(int $active_id, ilAssQuestionProcessLocker $processLocker = null): void
995 {
996 global $DIC;
997 $ilDB = $DIC['ilDB'];
998
999 $pass = ilObjTest::_getResultPass($active_id);
1000
1001 if ($pass !== null) {
1002 $query = "
1003 SELECT tst_pass_result.*
1004 FROM tst_pass_result
1005 WHERE active_fi = %s
1006 AND pass = %s
1007 ";
1008
1009 $result = $ilDB->queryF(
1010 $query,
1011 array('integer','integer'),
1012 array($active_id, $pass)
1013 );
1014
1015 $test_pass_result_row = $ilDB->fetchAssoc($result);
1016
1017 if (!is_array($test_pass_result_row)) {
1018 $test_pass_result_row = [];
1019 }
1020 $max = (float) ($test_pass_result_row['maxpoints'] ?? 0);
1021 $reached = (float) ($test_pass_result_row['points'] ?? 0);
1022 $percentage = ($max <= 0.0 || $reached <= 0.0) ? 0 : ($reached / $max) * 100.0;
1023
1024 $obligationsAnswered = (int) ($test_pass_result_row['obligations_answered'] ?? 1);
1025
1026 $mark = ASS_MarkSchema::_getMatchingMarkFromActiveId($active_id, $percentage);
1027 $isPassed = isset($mark["passed"]) && $mark["passed"];
1028
1029 $hint_count = $test_pass_result_row['hint_count'] ?? 0;
1030 $hint_points = $test_pass_result_row['hint_points'] ?? 0.0;
1031
1032 $userTestResultUpdateCallback = function () use ($ilDB, $active_id, $pass, $max, $reached, $isPassed, $obligationsAnswered, $hint_count, $hint_points, $mark) {
1033 $passedOnceBefore = 0;
1034 $query = "SELECT passed_once FROM tst_result_cache WHERE active_fi = %s";
1035 $res = $ilDB->queryF($query, array('integer'), array($active_id));
1036 while ($passed_once_result_row = $ilDB->fetchAssoc($res)) {
1037 $passedOnceBefore = (int) $passed_once_result_row['passed_once'];
1038 }
1039
1040 $passedOnce = (int) ($isPassed || $passedOnceBefore);
1041
1042 $ilDB->manipulateF(
1043 "DELETE FROM tst_result_cache WHERE active_fi = %s",
1044 array('integer'),
1045 array($active_id)
1046 );
1047
1048 $ilDB->insert('tst_result_cache', array(
1049 'active_fi' => array('integer', $active_id),
1050 'pass' => array('integer', strlen($pass) ? $pass : 0),
1051 'max_points' => array('float', strlen($max) ? $max : 0),
1052 'reached_points' => array('float', strlen($reached) ? $reached : 0),
1053 'mark_short' => array('text', strlen($mark["short_name"] ?? '') ? $mark["short_name"] : " "),
1054 'mark_official' => array('text', strlen($mark["official_name"] ?? '') ? $mark["official_name"] : " "),
1055 'passed_once' => array('integer', $passedOnce),
1056 'passed' => array('integer', (int) $isPassed),
1057 'failed' => array('integer', (int) !$isPassed),
1058 'tstamp' => array('integer', time()),
1059 'hint_count' => array('integer', $hint_count),
1060 'hint_points' => array('float', $hint_points),
1061 'obligations_answered' => array('integer', $obligationsAnswered)
1062 ));
1063 };
1064
1065 if (is_object($processLocker)) {
1066 $processLocker->executeUserTestResultUpdateLockOperation($userTestResultUpdateCallback);
1067 } else {
1068 $userTestResultUpdateCallback();
1069 }
1070 }
1071 }
1072
1074 public static function _updateTestPassResults(
1075 int $active_id,
1076 int $pass,
1077 bool $obligationsEnabled = false,
1078 ilAssQuestionProcessLocker $processLocker = null,
1079 int $test_obj_id = null
1080 ): array {
1081 global $DIC;
1082 $ilDB = $DIC['ilDB'];
1083
1085 $time = ilObjTest::_getWorkingTimeOfParticipantForPass($active_id, $pass);
1086
1087
1088
1089 // update test pass results
1090
1091 $result = $ilDB->queryF(
1092 "
1093 SELECT SUM(points) reachedpoints,
1094 SUM(hint_count) hint_count,
1095 SUM(hint_points) hint_points,
1096 COUNT(DISTINCT(question_fi)) answeredquestions
1097 FROM tst_test_result
1098 WHERE active_fi = %s
1099 AND pass = %s
1100 ",
1101 array('integer','integer'),
1102 array($active_id, $pass)
1103 );
1104
1105 if ($result->numRows() > 0) {
1106 if ($obligationsEnabled) {
1107 $query = '
1108 SELECT answered answ
1109 FROM tst_test_question
1110 INNER JOIN tst_active
1111 ON active_id = %s
1112 AND tst_test_question.test_fi = tst_active.test_fi
1113 LEFT JOIN tst_test_result
1114 ON tst_test_result.active_fi = %s
1115 AND tst_test_result.pass = %s
1116 AND tst_test_question.question_fi = tst_test_result.question_fi
1117 WHERE obligatory = 1';
1118
1119 $result_obligatory = $ilDB->queryF(
1120 $query,
1121 array('integer','integer','integer'),
1122 array($active_id, $active_id, $pass)
1123 );
1124
1125 $obligations_answered = 1;
1126
1127 while ($row_obligatory = $ilDB->fetchAssoc($result_obligatory)) {
1128 if (!(int) $row_obligatory['answ']) {
1129 $obligations_answered = 0;
1130 break;
1131 }
1132 }
1133 } else {
1134 $obligations_answered = 1;
1135 }
1136
1137 $row = $ilDB->fetchAssoc($result);
1138
1139 if ($row['reachedpoints'] === null) {
1140 $row['reachedpoints'] = 0.0;
1141 }
1142 if ($row['hint_count'] === null) {
1143 $row['hint_count'] = 0;
1144 }
1145 if ($row['hint_points'] === null) {
1146 $row['hint_points'] = 0.0;
1147 }
1148
1149 $exam_identifier = ilObjTest::buildExamId($active_id, $pass, $test_obj_id);
1150
1151 $updatePassResultCallback = function () use ($ilDB, $data, $active_id, $pass, $row, $time, $obligations_answered, $exam_identifier) {
1152
1154 $ilDB->replace(
1155 'tst_pass_result',
1156 array(
1157 'active_fi' => array('integer', $active_id),
1158 'pass' => array('integer', strlen($pass) ? $pass : 0)),
1159 array(
1160 'points' => array('float', $row['reachedpoints'] ?: 0),
1161 'maxpoints' => array('float', $data['points']),
1162 'questioncount' => array('integer', $data['count']),
1163 'answeredquestions' => array('integer', $row['answeredquestions']),
1164 'workingtime' => array('integer', $time),
1165 'tstamp' => array('integer', time()),
1166 'hint_count' => array('integer', $row['hint_count']),
1167 'hint_points' => array('float', $row['hint_points']),
1168 'obligations_answered' => array('integer', $obligations_answered),
1169 'exam_id' => array('text', $exam_identifier)
1170 )
1171 );
1172 };
1173
1174 if (is_object($processLocker) && $processLocker instanceof ilAssQuestionProcessLocker) {
1175 $processLocker->executeUserPassResultUpdateLockOperation($updatePassResultCallback);
1176 } else {
1177 $updatePassResultCallback();
1178 }
1179 }
1180
1181 assQuestion::_updateTestResultCache($active_id, $processLocker);
1182
1183 return array(
1184 'active_fi' => $active_id,
1185 'pass' => $pass,
1186 'points' => ($row["reachedpoints"]) ?: 0.0,
1187 'maxpoints' => $data["points"],
1188 'questioncount' => $data["count"],
1189 'answeredquestions' => $row["answeredquestions"],
1190 'workingtime' => $time,
1191 'tstamp' => time(),
1192 'hint_count' => $row['hint_count'],
1193 'hint_points' => $row['hint_points'],
1194 'obligations_answered' => $obligations_answered,
1195 'exam_id' => $exam_identifier
1196 );
1197 }
1198
1199 public static function logAction(string $logtext, int $active_id, int $question_id): void
1200 {
1201 $original_id = self::_getOriginalId($question_id);
1202
1204 $GLOBALS['DIC']['ilUser']->getId(),
1206 $logtext,
1207 $question_id,
1208 $original_id
1209 );
1210 }
1211
1219 public function moveUploadedMediaFile(string $file, string $name)
1220 {
1221 $mediatempdir = CLIENT_WEB_DIR . "/assessment/temp";
1222 if (!@is_dir($mediatempdir)) {
1223 ilFileUtils::createDirectory($mediatempdir);
1224 }
1225 $temp_name = tempnam($mediatempdir, $name . "_____");
1226 $temp_name = str_replace("\\", "/", $temp_name);
1227 @unlink($temp_name);
1228 if (!ilFileUtils::moveUploadedFile($file, $name, $temp_name)) {
1229 return false;
1230 }
1231 return $temp_name;
1232 }
1233
1234 public function getSuggestedSolutionPath(): string
1235 {
1236 return CLIENT_WEB_DIR . "/assessment/$this->obj_id/$this->id/solution/";
1237 }
1238
1244 public function getImagePath($question_id = null, $object_id = null): string
1245 {
1246 if ($question_id === null) {
1247 $question_id = $this->id;
1248 }
1249
1250 if ($object_id === null) {
1251 $object_id = $this->obj_id;
1252 }
1253
1254 return $this->buildImagePath($question_id, $object_id);
1255 }
1256
1257 public function buildImagePath($questionId, $parentObjectId): string
1258 {
1259 return CLIENT_WEB_DIR . "/assessment/{$parentObjectId}/{$questionId}/images/";
1260 }
1261
1268 public function getFlashPath(): string
1269 {
1270 return CLIENT_WEB_DIR . "/assessment/$this->obj_id/$this->id/flash/";
1271 }
1272
1273 public function getSuggestedSolutionPathWeb(): string
1274 {
1275 $webdir = ilFileUtils::removeTrailingPathSeparators(CLIENT_WEB_DIR) . "/assessment/$this->obj_id/$this->id/solution/";
1276 return str_replace(
1277 ilFileUtils::removeTrailingPathSeparators(ILIAS_ABSOLUTE_PATH),
1279 $webdir
1280 );
1281 }
1282
1288 public function getImagePathWeb(): string
1289 {
1290 if (!$this->export_image_path) {
1291 $webdir = ilFileUtils::removeTrailingPathSeparators(CLIENT_WEB_DIR) . "/assessment/$this->obj_id/$this->id/images/";
1292 return str_replace(
1293 ilFileUtils::removeTrailingPathSeparators(ILIAS_ABSOLUTE_PATH),
1295 $webdir
1296 );
1297 }
1298 return $this->export_image_path;
1299 }
1300
1301 // hey: prevPassSolutions - accept and prefer intermediate only from current pass
1302 public function getTestOutputSolutions(int $activeId, int $pass): array
1303 {
1304 if ($this->getTestPresentationConfig()->isSolutionInitiallyPrefilled()) {
1305 return $this->getSolutionValues($activeId, $pass, true);
1306 }
1307 return $this->getUserSolutionPreferingIntermediate($activeId, $pass);
1308 }
1309 // hey.
1310
1311 public function getUserSolutionPreferingIntermediate(int $active_id, $pass = null): array
1312 {
1313 $solution = $this->getSolutionValues($active_id, $pass, false);
1314
1315 if (!count($solution)) {
1316 $solution = $this->getSolutionValues($active_id, $pass, true);
1317 }
1318
1319 return $solution;
1320 }
1321
1327 public function getSolutionValues($active_id, $pass = null, bool $authorized = true): array
1328 {
1329 if ($pass === null && is_numeric($active_id)) {
1330 $pass = $this->getSolutionMaxPass((int) $active_id);
1331 }
1332
1333 if ($this->getStep() !== null) {
1334 $query = "
1335 SELECT *
1336 FROM tst_solutions
1337 WHERE active_fi = %s
1338 AND question_fi = %s
1339 AND pass = %s
1340 AND step = %s
1341 AND authorized = %s
1342 ORDER BY solution_id";
1343
1344 $result = $this->db->queryF(
1345 $query,
1346 array('integer', 'integer', 'integer', 'integer', 'integer'),
1347 array((int) $active_id, $this->getId(), $pass, $this->getStep(), (int) $authorized)
1348 );
1349 } else {
1350 $query = "
1351 SELECT *
1352 FROM tst_solutions
1353 WHERE active_fi = %s
1354 AND question_fi = %s
1355 AND pass = %s
1356 AND authorized = %s
1357 ORDER BY solution_id
1358 ";
1359
1360 $result = $this->db->queryF(
1361 $query,
1362 array('integer', 'integer', 'integer', 'integer'),
1363 array((int) $active_id, $this->getId(), $pass, (int) $authorized)
1364 );
1365 }
1366
1367 $values = [];
1368
1369 while ($row = $this->db->fetchAssoc($result)) {
1370 $values[] = $row;
1371 }
1372
1373 return $values;
1374 }
1375
1379 public function isInUse(int $question_id = 0): bool
1380 {
1381 return $this->usageNumber($question_id) > 0;
1382 }
1383
1387 public function usageNumber(int $question_id = 0): int
1388 {
1389 if ($question_id < 1) {
1390 $question_id = $this->getId();
1391 }
1392
1393 $result = $this->db->queryF(
1394 "SELECT COUNT(qpl_questions.question_id) question_count FROM qpl_questions, tst_test_question WHERE qpl_questions.original_id = %s AND qpl_questions.question_id = tst_test_question.question_fi",
1395 array('integer'),
1396 array($question_id)
1397 );
1398 $row = $this->db->fetchAssoc($result);
1399 $count = (int) $row["question_count"];
1400
1401 $result = $this->db->queryF(
1402 "
1403 SELECT tst_active.test_fi
1404 FROM qpl_questions
1405 INNER JOIN tst_test_rnd_qst ON tst_test_rnd_qst.question_fi = qpl_questions.question_id
1406 INNER JOIN tst_active ON tst_active.active_id = tst_test_rnd_qst.active_fi
1407 WHERE qpl_questions.original_id = %s
1408 GROUP BY tst_active.test_fi",
1409 array('integer'),
1410 array($question_id)
1411 );
1412 $count += (int) $this->db->numRows($result);
1413
1414 return $count;
1415 }
1416
1420 public function isClone(int $question_id = 0): bool
1421 {
1422 if ($question_id < 1) {
1423 $question_id = $this->id;
1424 }
1425 $result = $this->db->queryF(
1426 "SELECT COUNT(original_id) cnt FROM qpl_questions WHERE question_id = %s",
1427 array('integer'),
1428 array($question_id)
1429 );
1430 $row = $this->db->fetchAssoc($result);
1431 return ((int) $row["cnt"]) > 0;
1432 }
1433
1434 public static function getQuestionTypeFromDb(int $question_id): string
1435 {
1436 global $DIC;
1437 $ilDB = $DIC['ilDB'];
1438
1439 $result = $ilDB->queryF(
1440 "SELECT qpl_qst_type.type_tag FROM qpl_qst_type, qpl_questions WHERE qpl_questions.question_id = %s AND qpl_questions.question_type_fi = qpl_qst_type.question_type_id",
1441 array('integer'),
1442 array($question_id)
1443 );
1444 $data = $ilDB->fetchAssoc($result);
1445 return $data["type_tag"] ?? '';
1446 }
1447
1451 public function getAdditionalTableName()
1452 {
1453 return "";
1454 }
1455
1459 public function getAnswerTableName()
1460 {
1461 return "";
1462 }
1463
1464 public function deleteAnswers(int $question_id): void
1465 {
1466 $answer_table_name = $this->getAnswerTableName();
1467
1468 if (!is_array($answer_table_name)) {
1469 $answer_table_name = array($answer_table_name);
1470 }
1471
1472 foreach ($answer_table_name as $table) {
1473 if (strlen($table)) {
1474 $this->db->manipulateF(
1475 "DELETE FROM $table WHERE question_fi = %s",
1476 array('integer'),
1477 array($question_id)
1478 );
1479 }
1480 }
1481 }
1482
1483 public function deleteAdditionalTableData(int $question_id): void
1484 {
1485 $additional_table_name = $this->getAdditionalTableName();
1486
1487 if (!is_array($additional_table_name)) {
1488 $additional_table_name = array($additional_table_name);
1489 }
1490
1491 foreach ($additional_table_name as $table) {
1492 if (strlen($table)) {
1493 $this->db->manipulateF(
1494 "DELETE FROM $table WHERE question_fi = %s",
1495 array('integer'),
1496 array($question_id)
1497 );
1498 }
1499 }
1500 }
1501
1502 protected function deletePageOfQuestion(int $question_id): void
1503 {
1504 if (ilAssQuestionPage::_exists('qpl', $question_id, "", true)) {
1505 $page = new ilAssQuestionPage($question_id);
1506 $page->delete();
1507 }
1508 }
1509
1510 public function delete(int $question_id): void
1511 {
1512 if ($question_id < 1) {
1513 return;
1514 }
1515
1516 $result = $this->db->queryF(
1517 "SELECT obj_fi FROM qpl_questions WHERE question_id = %s",
1518 array('integer'),
1519 array($question_id)
1520 );
1521 if ($this->db->numRows($result) == 1) {
1522 $row = $this->db->fetchAssoc($result);
1523 $obj_id = $row["obj_fi"];
1524 } else {
1525 return; // nothing to do
1526 }
1527 try {
1528 $this->deletePageOfQuestion($question_id);
1529 } catch (Exception $e) {
1530 $this->ilLog->root()->error("EXCEPTION: Could not delete page of question $question_id: $e");
1531 return;
1532 }
1533
1534 $affectedRows = $this->db->manipulateF(
1535 "DELETE FROM qpl_questions WHERE question_id = %s",
1536 array('integer'),
1537 array($question_id)
1538 );
1539 if ($affectedRows == 0) {
1540 return;
1541 }
1542
1543 try {
1544 $this->deleteAdditionalTableData($question_id);
1545 $this->deleteAnswers($question_id);
1546 $this->feedbackOBJ->deleteGenericFeedbacks($question_id, $this->isAdditionalContentEditingModePageObject());
1547 $this->feedbackOBJ->deleteSpecificAnswerFeedbacks($question_id, $this->isAdditionalContentEditingModePageObject());
1548 } catch (Exception $e) {
1549 $this->ilLog->root()->error("EXCEPTION: Could not delete additional table data of question $question_id: $e");
1550 return;
1551 }
1552
1553 try {
1554 // delete the question in the tst_test_question table (list of test questions)
1555 $affectedRows = $this->db->manipulateF(
1556 "DELETE FROM tst_test_question WHERE question_fi = %s",
1557 array('integer'),
1558 array($question_id)
1559 );
1560 } catch (Exception $e) {
1561 $this->ilLog->root()->error("EXCEPTION: Could not delete delete question $question_id from a test: $e");
1562 return;
1563 }
1564
1565 try {
1566 // delete suggested solutions contained in the question
1567 $affectedRows = $this->db->manipulateF(
1568 "DELETE FROM qpl_sol_sug WHERE question_fi = %s",
1569 array('integer'),
1570 array($question_id)
1571 );
1572 } catch (Exception $e) {
1573 $this->ilLog->root()->error("EXCEPTION: Could not delete suggested solutions of question $question_id: $e");
1574 return;
1575 }
1576
1577 try {
1578 $directory = CLIENT_WEB_DIR . "/assessment/" . $obj_id . "/$question_id";
1579 if (preg_match("/\d+/", $obj_id) and preg_match("/\d+/", $question_id) and is_dir($directory)) {
1580 ilFileUtils::delDir($directory);
1581 }
1582 } catch (Exception $e) {
1583 $this->ilLog->root()->error("EXCEPTION: Could not delete question file directory $directory of question $question_id: $e");
1584 return;
1585 }
1586
1587 try {
1588 $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $question_id);
1589 // remaining usages are not in text anymore -> delete them
1590 // and media objects (note: delete method of ilObjMediaObject
1591 // checks whether object is used in another context; if yes,
1592 // the object is not deleted!)
1593 foreach ($mobs as $mob) {
1594 ilObjMediaObject::_removeUsage($mob, "qpl:html", $question_id);
1595 if (ilObjMediaObject::_exists($mob)) {
1596 $mob_obj = new ilObjMediaObject($mob);
1597 $mob_obj->delete();
1598 }
1599 }
1600 } catch (Exception $e) {
1601 $this->ilLog->root()->error("EXCEPTION: Error deleting the media objects of question $question_id: $e");
1602 return;
1603 }
1604 ilAssQuestionHintTracking::deleteRequestsByQuestionIds(array($question_id));
1606 $assignmentList = new ilAssQuestionSkillAssignmentList($this->db);
1607 $assignmentList->setParentObjId($obj_id);
1608 $assignmentList->setQuestionIdFilter($question_id);
1609 $assignmentList->loadFromDb();
1610 foreach ($assignmentList->getAssignmentsByQuestionId($question_id) as $assignment) {
1611 /* @var ilAssQuestionSkillAssignment $assignment */
1612 $assignment->deleteFromDb();
1613
1614 // remove skill usage
1615 if (!$assignment->isSkillUsed()) {
1617 $assignment->getParentObjId(),
1618 $assignment->getSkillBaseId(),
1619 $assignment->getSkillTrefId(),
1620 false
1621 );
1622 }
1623 }
1624
1625 $this->deleteTaxonomyAssignments();
1626
1627 try {
1628 ilObjQuestionPool::_updateQuestionCount($this->getObjId());
1629 } catch (Exception $e) {
1630 $this->ilLog->root()->error("EXCEPTION: Error updating the question pool question count of question pool " . $this->getObjId() . " when deleting question $question_id: $e");
1631 return;
1632 }
1633 }
1634
1635 private function deleteTaxonomyAssignments(): void
1636 {
1637 $taxIds = ilObjTaxonomy::getUsageOfObject($this->getObjId());
1638
1639 foreach ($taxIds as $taxId) {
1640 $taxNodeAssignment = new ilTaxNodeAssignment('qpl', $this->getObjId(), 'quest', $taxId);
1641 $taxNodeAssignment->deleteAssignmentsOfItem($this->getId());
1642 }
1643 }
1644
1645 public function getTotalAnswers(): int
1646 {
1647 // get all question references to the question id
1648 $result = $this->db->queryF(
1649 "SELECT question_id FROM qpl_questions WHERE original_id = %s OR question_id = %s",
1650 array('integer','integer'),
1651 array($this->id, $this->id)
1652 );
1653 if ($this->db->numRows($result) == 0) {
1654 return 0;
1655 }
1656 $found_id = [];
1657 while ($row = $this->db->fetchAssoc($result)) {
1658 $found_id[] = $row["question_id"];
1659 }
1660
1661 $result = $this->db->query("SELECT * FROM tst_test_result WHERE " . $this->db->in('question_fi', $found_id, false, 'integer'));
1662
1663 return $this->db->numRows($result);
1664 }
1665
1666 public static function _getTotalRightAnswers(int $a_q_id): int
1667 {
1668 global $DIC;
1669 $ilDB = $DIC['ilDB'];
1670
1671 $result = $ilDB->queryF(
1672 "SELECT question_id FROM qpl_questions WHERE original_id = %s OR question_id = %s",
1673 array('integer','integer'),
1674 array($a_q_id, $a_q_id)
1675 );
1676 if ($result->numRows() == 0) {
1677 return 0;
1678 }
1679
1680 $found_id = [];
1681 while ($row = $ilDB->fetchAssoc($result)) {
1682 $found_id[] = $row["question_id"];
1683 }
1684
1685 $result = $ilDB->query("SELECT * FROM tst_test_result WHERE " . $ilDB->in('question_fi', $found_id, false, 'integer'));
1686 $answers = [];
1687 while ($row = $ilDB->fetchAssoc($result)) {
1688 $reached = $row["points"];
1689 $max = self::_getMaximumPoints($row["question_fi"]);
1690 $answers[] = array("reached" => $reached, "max" => $max);
1691 }
1692 $max = 0.0;
1693 $reached = 0.0;
1694 foreach ($answers as $key => $value) {
1695 $max += $value["max"];
1696 $reached += $value["reached"];
1697 }
1698 if ($max > 0) {
1699 return $reached / $max;
1700 }
1701 return 0;
1702 }
1703
1704 public static function _getTitle(int $a_q_id): string
1705 {
1706 global $DIC;
1707 $ilDB = $DIC['ilDB'];
1708
1709 $result = $ilDB->queryF(
1710 "SELECT title FROM qpl_questions WHERE question_id = %s",
1711 array('integer'),
1712 array($a_q_id)
1713 );
1714
1715 if ($result->numRows() == 1) {
1716 $row = $ilDB->fetchAssoc($result);
1717 return $row["title"];
1718 }
1719 return "";
1720 }
1721
1722 public static function _getQuestionText(int $a_q_id): string
1723 {
1724 global $DIC;
1725 $ilDB = $DIC['ilDB'];
1726
1727 $result = $ilDB->queryF(
1728 "SELECT question_text FROM qpl_questions WHERE question_id = %s",
1729 array('integer'),
1730 array($a_q_id)
1731 );
1732
1733 if ($result->numRows() == 1) {
1734 $row = $ilDB->fetchAssoc($result);
1735 return $row["question_text"] ?? '';
1736 }
1737
1738 return "";
1739 }
1740
1741 public static function isFileAvailable(string $file): bool
1742 {
1743 if (!file_exists($file)) {
1744 return false;
1745 }
1746
1747 if (!is_file($file)) {
1748 return false;
1749 }
1750
1751 if (!is_readable($file)) {
1752 return false;
1753 }
1754
1755 return true;
1756 }
1757
1758 public function copyXHTMLMediaObjectsOfQuestion(int $a_q_id): void
1759 {
1760 $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $a_q_id);
1761 foreach ($mobs as $mob) {
1762 ilObjMediaObject::_saveUsage($mob, "qpl:html", $this->getId());
1763 }
1764 }
1765
1766 public function syncXHTMLMediaObjectsOfQuestion(): void
1767 {
1768 $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId());
1769 foreach ($mobs as $mob) {
1770 ilObjMediaObject::_saveUsage($mob, "qpl:html", $this->original_id);
1771 }
1772 }
1773
1774 public function createPageObject(): void
1775 {
1776 $qpl_id = $this->getObjId();
1777 $this->page = new ilAssQuestionPage(0);
1778 $this->page->setId($this->getId());
1779 $this->page->setParentId($qpl_id);
1780 $this->page->setXMLContent("<PageObject><PageContent>" .
1781 "<Question QRef=\"il__qst_" . $this->getId() . "\"/>" .
1782 "</PageContent></PageObject>");
1783 $this->page->create(false);
1784 }
1785
1786 public function copyPageOfQuestion(int $a_q_id): void
1787 {
1788 if ($a_q_id > 0) {
1789 $page = new ilAssQuestionPage($a_q_id);
1790
1791 $xml = str_replace("il__qst_" . $a_q_id, "il__qst_" . $this->id, $page->getXMLContent());
1792 $this->page->setXMLContent($xml);
1793 $this->page->updateFromXML();
1794 }
1795 }
1796
1797 public function getPageOfQuestion(): string
1798 {
1799 $page = new ilAssQuestionPage($this->id);
1800 return $page->getXMLContent();
1801 }
1802
1803 public static function _getQuestionType(int $question_id): string
1804 {
1805 global $DIC;
1806 $ilDB = $DIC['ilDB'];
1807
1808 if ($question_id < 1) {
1809 return "";
1810 }
1811 $result = $ilDB->queryF(
1812 "SELECT type_tag FROM qpl_questions, qpl_qst_type WHERE qpl_questions.question_id = %s AND qpl_questions.question_type_fi = qpl_qst_type.question_type_id",
1813 array('integer'),
1814 array($question_id)
1815 );
1816 if ($result->numRows() == 1) {
1817 $data = $ilDB->fetchAssoc($result);
1818 return $data["type_tag"];
1819 }
1820
1821 return "";
1822 }
1823
1824 public static function _getQuestionTitle(int $question_id): string
1825 {
1826 global $DIC;
1827 $ilDB = $DIC['ilDB'];
1828
1829 if ($question_id < 1) {
1830 return "";
1831 }
1832
1833 $result = $ilDB->queryF(
1834 "SELECT title FROM qpl_questions WHERE qpl_questions.question_id = %s",
1835 array('integer'),
1836 array($question_id)
1837 );
1838 if ($result->numRows() == 1) {
1839 $data = $ilDB->fetchAssoc($result);
1840 return $data["title"];
1841 }
1842
1843 return "";
1844 }
1845
1846 public function setOriginalId(?int $original_id): void
1847 {
1848 $this->original_id = $original_id;
1849 }
1850
1851 public function getOriginalId(): ?int
1852 {
1853 return $this->original_id;
1854 }
1855
1856 protected static $imageSourceFixReplaceMap = array(
1857 'ok.svg' => 'ok.png',
1858 'not_ok.svg' => 'not_ok.png',
1859 'checkbox_checked.svg' => 'checkbox_checked.png',
1860 'checkbox_unchecked.svg' => 'checkbox_unchecked.png',
1861 'radiobutton_checked.svg' => 'radiobutton_checked.png',
1862 'radiobutton_unchecked.svg' => 'radiobutton_unchecked.png'
1863 );
1864
1865 public function fixSvgToPng(string $imageFilenameContainingString): string
1866 {
1867 $needles = array_keys(self::$imageSourceFixReplaceMap);
1868 $replacements = array_values(self::$imageSourceFixReplaceMap);
1869 return str_replace($needles, $replacements, $imageFilenameContainingString);
1870 }
1871
1872 public function fixUnavailableSkinImageSources(string $html): string
1873 {
1874 $matches = null;
1875 if (preg_match_all('/src="(.*?)"/m', $html, $matches)) {
1876 $sources = $matches[1];
1877
1878 $needleReplacementMap = [];
1879
1880 foreach ($sources as $src) {
1881 $file = ilFileUtils::removeTrailingPathSeparators(ILIAS_ABSOLUTE_PATH) . DIRECTORY_SEPARATOR . $src;
1882
1883 if (file_exists($file)) {
1884 continue;
1885 }
1886
1887 $levels = explode(DIRECTORY_SEPARATOR, $src);
1888 if (count($levels) < 5 || $levels[0] !== 'Customizing' || $levels[2] !== 'skin') {
1889 continue;
1890 }
1891
1892 $component = '';
1893
1894 if ($levels[4] === 'Modules' || $levels[4] === 'Services') {
1895 $component = $levels[4] . DIRECTORY_SEPARATOR . $levels[5];
1896 }
1897
1898 $needleReplacementMap[$src] = ilUtil::getImagePath(basename($src), $component);
1899 }
1900
1901 if (count($needleReplacementMap)) {
1902 $html = str_replace(array_keys($needleReplacementMap), array_values($needleReplacementMap), $html);
1903 }
1904 }
1905
1906 return $html;
1907 }
1908
1909 public function loadFromDb(int $question_id): void
1910 {
1911 $result = $this->db->queryF(
1912 'SELECT external_id FROM qpl_questions WHERE question_id = %s',
1913 ['integer'],
1914 [$question_id]
1915 );
1916 if ($this->db->numRows($result) === 1) {
1917 $data = $this->db->fetchAssoc($result);
1918 $this->external_id = $data['external_id'];
1919 }
1920
1921 $result = $this->db->queryF(
1922 "SELECT internal_link, import_id, subquestion_index, type, value" .
1923 " FROM qpl_sol_sug WHERE question_fi = %s",
1924 ["integer"],
1925 [$this->getId()]
1926 );
1927
1928 $suggestedSolutions = [];
1929
1930 while ($row = $this->db->fetchAssoc($result)) {
1931 $value = $row["value"];
1932
1933 try {
1934 $unserializedValue = unserialize($value, ['allowed_classes' => false]);
1935 if (is_array($unserializedValue)) {
1936 $value = $unserializedValue;
1937 }
1938 } catch (Exception $ex) {
1939 }
1940
1941 if (is_string($value)) {
1942 $value = ilRTE::_replaceMediaObjectImageSrc($value, 1);
1943 }
1944
1945 $suggestedSolutions[$row["subquestion_index"]] = [
1946 "type" => $row["type"],
1947 "value" => $value,
1948 "internal_link" => $row["internal_link"],
1949 "import_id" => $row["import_id"]
1950 ];
1951 }
1952 $this->suggested_solutions = $suggestedSolutions;
1953 }
1954
1961 public function createNewQuestion(bool $a_create_page = true): int
1962 {
1963 $ilUser = $this->current_user;
1964
1965 $complete = "0";
1966 $obj_id = ($this->getObjId() <= 0) ? (ilObject::_lookupObjId((strlen($this->dic->testQuestionPool()->internal()->request()->getRefId())) ? $this->dic->testQuestionPool()->internal()->request()->getRefId() : $_POST["sel_qpl"])) : $this->getObjId();
1967 if ($obj_id > 0) {
1968 if ($a_create_page) {
1969 $tstamp = 0;
1970 } else {
1971 // question pool must not try to purge
1972 $tstamp = time();
1973 }
1974
1975 $next_id = $this->db->nextId('qpl_questions');
1976 $this->db->insert("qpl_questions", array(
1977 "question_id" => array("integer", $next_id),
1978 "question_type_fi" => array("integer", $this->getQuestionTypeID()),
1979 "obj_fi" => array("integer", $obj_id),
1980 "title" => array("text", null),
1981 "description" => array("text", null),
1982 "author" => array("text", $this->getAuthor()),
1983 "owner" => array("integer", $ilUser->getId()),
1984 "question_text" => array("clob", null),
1985 "points" => array("float", "0.0"),
1986 "nr_of_tries" => array("integer", $this->getDefaultNrOfTries()), // #10771
1987 "complete" => array("text", $complete),
1988 "created" => array("integer", time()),
1989 "original_id" => array("integer", null),
1990 "tstamp" => array("integer", $tstamp),
1991 "external_id" => array("text", $this->getExternalId()),
1992 'add_cont_edit_mode' => array('text', $this->getAdditionalContentEditingMode())
1993 ));
1994 $this->setId($next_id);
1995
1996 if ($a_create_page) {
1997 // create page object of question
1998 $this->createPageObject();
1999 }
2000 }
2001
2002 return $this->getId();
2003 }
2004
2005 public function saveQuestionDataToDb(int $original_id = -1): void
2006 {
2007 if ($this->getId() == -1) {
2008 $next_id = $this->db->nextId('qpl_questions');
2009 $this->db->insert("qpl_questions", array(
2010 "question_id" => array("integer", $next_id),
2011 "question_type_fi" => array("integer", $this->getQuestionTypeID()),
2012 "obj_fi" => array("integer", $this->getObjId()),
2013 "title" => array("text", $this->getTitle()),
2014 "description" => array("text", $this->getComment()),
2015 "author" => array("text", $this->getAuthor()),
2016 "owner" => array("integer", $this->getOwner()),
2017 "question_text" => array("clob", ilRTE::_replaceMediaObjectImageSrc($this->getQuestion(), 0)),
2018 "points" => array("float", $this->getMaximumPoints()),
2019 "nr_of_tries" => array("integer", $this->getNrOfTries()),
2020 "created" => array("integer", time()),
2021 "original_id" => array("integer", ($original_id != -1) ? $original_id : null),
2022 "tstamp" => array("integer", time()),
2023 "external_id" => array("text", $this->getExternalId()),
2024 'add_cont_edit_mode' => array('text', $this->getAdditionalContentEditingMode())
2025 ));
2026 $this->setId($next_id);
2027 // create page object of question
2028 $this->createPageObject();
2029 } else {
2030 // Vorhandenen Datensatz aktualisieren
2031 $this->db->update("qpl_questions", array(
2032 "obj_fi" => array("integer", $this->getObjId()),
2033 "title" => array("text", $this->getTitle()),
2034 "description" => array("text", $this->getComment()),
2035 "author" => array("text", $this->getAuthor()),
2036 "question_text" => array("clob", ilRTE::_replaceMediaObjectImageSrc($this->getQuestion(), 0)),
2037 "points" => array("float", $this->getMaximumPoints()),
2038 "nr_of_tries" => array("integer", $this->getNrOfTries()),
2039 "tstamp" => array("integer", time()),
2040 'complete' => array('integer', $this->isComplete()),
2041 "external_id" => array("text", $this->getExternalId())
2042 ), array(
2043 "question_id" => array("integer", $this->getId())
2044 ));
2045 }
2046 }
2047
2048 public function saveToDb(): void
2049 {
2050 $this->updateSuggestedSolutions();
2051
2052 // remove unused media objects from ILIAS
2053 $this->cleanupMediaObjectUsage();
2054
2055 $complete = "0";
2056 if ($this->isComplete()) {
2057 $complete = "1";
2058 }
2059
2060 $this->db->update('qpl_questions', array(
2061 'tstamp' => array('integer', time()),
2062 'owner' => array('integer', $this->getOwner()),
2063 'complete' => array('integer', $complete),
2064 'lifecycle' => array('text', $this->getLifecycle()->getIdentifier()),
2065 ), array(
2066 'question_id' => array('integer', $this->getId())
2067 ));
2069 }
2070
2074 public function setNewOriginalId(int $newId): void
2075 {
2076 self::saveOriginalId($this->getId(), $newId);
2077 }
2078
2079 public static function saveOriginalId(int $questionId, int $originalId): void
2080 {
2081 global $DIC;
2082 $ilDB = $DIC->database();
2083 $query = "UPDATE qpl_questions SET tstamp = %s, original_id = %s WHERE question_id = %s";
2084
2085 $ilDB->manipulateF(
2086 $query,
2087 array('integer','integer', 'text'),
2088 array(time(), $originalId, $questionId)
2089 );
2090 }
2091
2092 public static function resetOriginalId(int $questionId): void
2093 {
2094 global $DIC;
2095 $ilDB = $DIC->database();
2096
2097 $query = "UPDATE qpl_questions SET tstamp = %s, original_id = NULL WHERE question_id = %s";
2098
2099 $ilDB->manipulateF(
2100 $query,
2101 array('integer', 'text'),
2102 array(time(), $questionId)
2103 );
2104 }
2105
2106 protected function onDuplicate(int $originalParentId, int $originalQuestionId, int $duplicateParentId, int $duplicateQuestionId): void
2107 {
2108 $this->duplicateSuggestedSolutionFiles($originalParentId, $originalQuestionId);
2109 $this->feedbackOBJ->duplicateFeedback($originalQuestionId, $duplicateQuestionId);
2110 $this->duplicateQuestionHints($originalQuestionId, $duplicateQuestionId);
2111 $this->duplicateSkillAssignments($originalParentId, $originalQuestionId, $duplicateParentId, $duplicateQuestionId);
2112 }
2113
2114 protected function beforeSyncWithOriginal(int $origQuestionId, int $dupQuestionId, int $origParentObjId, int $dupParentObjId): void
2115 {
2116 }
2117
2118 protected function afterSyncWithOriginal(int $origQuestionId, int $dupQuestionId, int $origParentObjId, int $dupParentObjId): void
2119 {
2120 $this->feedbackOBJ->syncFeedback($origQuestionId, $dupQuestionId);
2121 }
2122
2123 protected function onCopy(int $sourceParentId, int $sourceQuestionId, int $targetParentId, int $targetQuestionId): void
2124 {
2125 $this->copySuggestedSolutionFiles($sourceParentId, $sourceQuestionId);
2126
2127 // duplicate question feeback
2128 $this->feedbackOBJ->duplicateFeedback($sourceQuestionId, $targetQuestionId);
2129
2130 // duplicate question hints
2131 $this->duplicateQuestionHints($sourceQuestionId, $targetQuestionId);
2132
2133 // duplicate skill assignments
2134 $this->duplicateSkillAssignments($sourceParentId, $sourceQuestionId, $targetParentId, $targetQuestionId);
2135 }
2136
2137 public function deleteSuggestedSolutions(): void
2138 {
2139 // delete the links in the qpl_sol_sug table
2140 $this->db->manipulateF(
2141 "DELETE FROM qpl_sol_sug WHERE question_fi = %s",
2142 array('integer'),
2143 array($this->getId())
2144 );
2146 $this->suggested_solutions = [];
2147 ilFileUtils::delDir($this->getSuggestedSolutionPath());
2148 }
2149
2153 public function getSuggestedSolution(int $subquestion_index = 0): array
2154 {
2155 if (array_key_exists($subquestion_index, $this->suggested_solutions)) {
2156 return $this->suggested_solutions[$subquestion_index];
2157 }
2158 return [];
2159 }
2160
2168 public function getSuggestedSolutionTitle(int $subquestion_index = 0): string
2169 {
2170 if (array_key_exists($subquestion_index, $this->suggested_solutions)) {
2171 $title = $this->suggested_solutions[$subquestion_index]["internal_link"];
2172 // TO DO: resolve internal link an get link type and title
2173 } else {
2174 $title = "";
2175 }
2176 return $title;
2177 }
2178
2188 public function setSuggestedSolution(string $solution_id = "", int $subquestion_index = 0, bool $is_import = false): void
2189 {
2190 if (strcmp($solution_id, "") != 0) {
2191 $import_id = "";
2192 if ($is_import) {
2193 $import_id = $solution_id;
2194 $solution_id = $this->_resolveInternalLink($import_id);
2195 }
2196 $this->suggested_solutions[$subquestion_index] = array(
2197 "internal_link" => $solution_id,
2198 "import_id" => $import_id
2199 );
2200 }
2201 }
2202
2206 protected function duplicateSuggestedSolutionFiles(int $parent_id, int $question_id): void
2207 {
2208 foreach ($this->suggested_solutions as $index => $solution) {
2209 if (!is_array($solution) ||
2210 !array_key_exists("type", $solution) ||
2211 strcmp($solution["type"], "file") !== 0) {
2212 continue;
2213 }
2214
2215 $filepath = $this->getSuggestedSolutionPath();
2216 $filepath_original = str_replace(
2217 "/{$this->obj_id}/{$this->id}/solution",
2218 "/$parent_id/$question_id/solution",
2219 $filepath
2220 );
2221 if (!file_exists($filepath)) {
2222 ilFileUtils::makeDirParents($filepath);
2223 }
2224 $filename = $solution["value"]["name"];
2225 if (strlen($filename)) {
2226 if (!copy($filepath_original . $filename, $filepath . $filename)) {
2227 $this->ilLog->root()->error("File could not be duplicated!!!!");
2228 $this->ilLog->root()->error("object: " . print_r($this, true));
2229 }
2230 }
2231 }
2232 }
2233
2234 protected function syncSuggestedSolutionFiles(
2235 int $target_question_id,
2236 int $target_obj_id
2237 ): void {
2238 $filepath = $this->getSuggestedSolutionPath();
2239 $filepath_original = str_replace(
2240 "{$this->getObjId()}/{$this->id}/solution",
2241 "{$target_obj_id}/{$target_question_id}/solution",
2242 $filepath
2243 );
2244 ilFileUtils::delDir($filepath_original);
2245 foreach ($this->suggested_solutions as $index => $solution) {
2246 if (strcmp($solution["type"], "file") == 0) {
2247 if (!file_exists($filepath_original)) {
2248 ilFileUtils::makeDirParents($filepath_original);
2249 }
2250 $filename = $solution["value"]["name"];
2251 if (strlen($filename)) {
2252 if (!@copy($filepath . $filename, $filepath_original . $filename)) {
2253 $this->ilLog->root()->error("File could not be duplicated!!!!");
2254 $this->ilLog->root()->error("object: " . print_r($this, true));
2255 }
2256 }
2257 }
2258 }
2259 }
2260
2261 protected function copySuggestedSolutionFiles(int $source_questionpool_id, int $source_question_id): void
2262 {
2263 foreach ($this->suggested_solutions as $index => $solution) {
2264 if (strcmp($solution["type"], "file") == 0) {
2265 $filepath = $this->getSuggestedSolutionPath();
2266 $filepath_original = str_replace("/$this->obj_id/$this->id/solution", "/$source_questionpool_id/$source_question_id/solution", $filepath);
2267 if (!file_exists($filepath)) {
2268 ilFileUtils::makeDirParents($filepath);
2269 }
2270 $filename = $solution["value"]["name"];
2271 if (strlen($filename)) {
2272 if (!copy($filepath_original . $filename, $filepath . $filename)) {
2273 $this->ilLog->root()->error("File could not be copied!!!!");
2274 $this->ilLog->root()->error("object: " . print_r($this, true));
2275 }
2276 }
2277 }
2278 }
2279 }
2280
2281 public function updateSuggestedSolutions(int $original_id = -1, int $original_obj_id = -1): void
2282 {
2283 $id = $original_id !== -1 ? $original_id : $this->getId();
2284 $this->db->manipulateF(
2285 "DELETE FROM qpl_sol_sug WHERE question_fi = %s",
2286 array('integer'),
2287 array($id)
2288 );
2290 foreach ($this->suggested_solutions as $index => $solution) {
2291 $next_id = $this->db->nextId('qpl_sol_sug');
2292
2293 $value = $solution['value'] ?? '';
2294 if (is_array($value)) {
2295 $value = serialize($value);
2296 }
2297
2298 $this->db->insert(
2299 'qpl_sol_sug',
2300 array(
2301 'suggested_solution_id' => array('integer', $next_id),
2302 'question_fi' => array('integer', $id ),
2303 'type' => array('text', $solution['type'] ?? ''),
2304 'value' => array('clob', ilRTE::_replaceMediaObjectImageSrc($value)),
2305 'internal_link' => array( 'text', $solution['internal_link'] ?? ''),
2306 'import_id' => array('text', null),
2307 'subquestion_index' => array('integer', $index),
2308 'tstamp' => array('integer', time()),
2309 )
2310 );
2311 if (preg_match("/il_(\d*?)_(\w+)_(\d+)/", $solution["internal_link"], $matches)) {
2312 ilInternalLink::_saveLink("qst", $id, $matches[2], (int) $matches[3], (int) $matches[1]);
2313 }
2314 }
2315 if ($original_id !== -1
2316 && $original_obj_id !== -1) {
2317 $this->syncSuggestedSolutionFiles($id, $original_obj_id);
2318 }
2319 $this->cleanupMediaObjectUsage();
2320 }
2321
2331 public function saveSuggestedSolution(string $type, $solution_id = "", int $subquestion_index = 0, $value = ""): void
2332 {
2333 $this->db->manipulateF(
2334 "DELETE FROM qpl_sol_sug WHERE question_fi = %s AND subquestion_index = %s",
2335 array("integer", "integer"),
2336 array(
2337 $this->getId(),
2338 $subquestion_index
2339 )
2340 );
2341
2342 $next_id = $this->db->nextId('qpl_sol_sug');
2344 $affectedRows = $this->db->insert(
2345 'qpl_sol_sug',
2346 array(
2347 'suggested_solution_id' => array( 'integer', $next_id ),
2348 'question_fi' => array( 'integer', $this->getId() ),
2349 'type' => array( 'text', $type ),
2350 'value' => array( 'clob', ilRTE::_replaceMediaObjectImageSrc((is_array($value)) ? serialize($value) : $value, 0) ),
2351 'internal_link' => array( 'text', $solution_id ),
2352 'import_id' => array( 'text', null ),
2353 'subquestion_index' => array( 'integer', $subquestion_index ),
2354 'tstamp' => array( 'integer', time() ),
2355 )
2356 );
2357 if ($affectedRows == 1) {
2358 $this->suggested_solutions[$subquestion_index] = array(
2359 "type" => $type,
2360 "value" => $value,
2361 "internal_link" => $solution_id,
2362 "import_id" => ""
2363 );
2364 }
2365 $this->cleanupMediaObjectUsage();
2366 }
2367
2368 public function _resolveInternalLink(string $internal_link): string
2369 {
2370 if (preg_match("/il_(\d+)_(\w+)_(\d+)/", $internal_link, $matches)) {
2371 switch ($matches[2]) {
2372 case "lm":
2373 $resolved_link = ilLMObject::_getIdForImportId($internal_link);
2374 break;
2375 case "pg":
2376 $resolved_link = ilInternalLink::_getIdForImportId("PageObject", $internal_link);
2377 break;
2378 case "st":
2379 $resolved_link = ilInternalLink::_getIdForImportId("StructureObject", $internal_link);
2380 break;
2381 case "git":
2382 $resolved_link = ilInternalLink::_getIdForImportId("GlossaryItem", $internal_link);
2383 break;
2384 case "mob":
2385 $resolved_link = ilInternalLink::_getIdForImportId("MediaObject", $internal_link);
2386 break;
2387 }
2388 if (strcmp($resolved_link, "") == 0) {
2389 $resolved_link = $internal_link;
2390 }
2391 } else {
2392 $resolved_link = $internal_link;
2393 }
2394 return $resolved_link;
2395 }
2396
2397 public function _resolveIntLinks(int $question_id): void
2398 {
2399 $resolvedlinks = 0;
2400 $result = $this->db->queryF(
2401 "SELECT * FROM qpl_sol_sug WHERE question_fi = %s",
2402 array('integer'),
2403 array($question_id)
2404 );
2405 if ($this->db->numRows($result) > 0) {
2406 while ($row = $this->db->fetchAssoc($result)) {
2407 $internal_link = $row["internal_link"];
2408 $resolved_link = $this->_resolveInternalLink($internal_link);
2409 if (strcmp($internal_link, $resolved_link) != 0) {
2410 // internal link was resolved successfully
2411 $affectedRows = $this->db->manipulateF(
2412 "UPDATE qpl_sol_sug SET internal_link = %s WHERE suggested_solution_id = %s",
2413 array('text','integer'),
2414 array($resolved_link, $row["suggested_solution_id"])
2415 );
2416 $resolvedlinks++;
2417 }
2418 }
2419 }
2420 if ($resolvedlinks) {
2421 ilInternalLink::_deleteAllLinksOfSource("qst", $question_id);
2422
2423 $result = $this->db->queryF(
2424 "SELECT * FROM qpl_sol_sug WHERE question_fi = %s",
2425 array('integer'),
2426 array($question_id)
2427 );
2428 if ($this->db->numRows($result) > 0) {
2429 while ($row = $this->db->fetchAssoc($result)) {
2430 if (preg_match("/il_(\d*?)_(\w+)_(\d+)/", $row["internal_link"], $matches)) {
2431 ilInternalLink::_saveLink("qst", $question_id, $matches[2], $matches[3], $matches[1]);
2432 }
2433 }
2434 }
2435 }
2436 }
2437
2438 public static function _getInternalLinkHref(string $target = ""): string
2439 {
2440 global $DIC;
2441 $linktypes = array(
2442 "lm" => "LearningModule",
2443 "pg" => "PageObject",
2444 "st" => "StructureObject",
2445 "git" => "GlossaryItem",
2446 "mob" => "MediaObject"
2447 );
2448 $href = "";
2449 if (preg_match("/il__(\w+)_(\d+)/", $target, $matches)) {
2450 $type = $matches[1];
2451 $target_id = $matches[2];
2452 switch ($linktypes[$matches[1]]) {
2453 case "MediaObject":
2454 $href = "./ilias.php?baseClass=ilLMPresentationGUI&obj_type=" . $linktypes[$type]
2455 . "&cmd=media&ref_id=" . $DIC->testQuestionPool()->internal()->request()->getRefId()
2456 . "&mob_id=" . $target_id;
2457 break;
2458 case "StructureObject":
2459 case "GlossaryItem":
2460 case "PageObject":
2461 case "LearningModule":
2462 default:
2463 $href = "./goto.php?target=" . $type . "_" . $target_id;
2464 break;
2465 }
2466 }
2467 return $href;
2468 }
2469
2470 public static function _getOriginalId(int $question_id): int
2471 {
2472 global $DIC;
2473 $ilDB = $DIC['ilDB'];
2474 $result = $ilDB->queryF(
2475 "SELECT * FROM qpl_questions WHERE question_id = %s",
2476 array('integer'),
2477 array($question_id)
2478 );
2479 if ($ilDB->numRows($result) > 0) {
2480 $row = $ilDB->fetchAssoc($result);
2481 if ($row["original_id"] > 0) {
2482 return $row["original_id"];
2483 }
2484
2485 return (int) $row["question_id"];
2486 }
2487
2488 return -1;
2489 }
2490
2491 public static function originalQuestionExists(int $questionId): bool
2492 {
2493 global $DIC;
2494 $ilDB = $DIC['ilDB'];
2495
2496 $query = "
2497 SELECT COUNT(dupl.question_id) cnt
2498 FROM qpl_questions dupl
2499 INNER JOIN qpl_questions orig
2500 ON orig.question_id = dupl.original_id
2501 WHERE dupl.question_id = %s
2502 ";
2503
2504 $res = $ilDB->queryF($query, array('integer'), array($questionId));
2505 $row = $ilDB->fetchAssoc($res);
2506
2507 return $row['cnt'] > 0;
2508 }
2509
2510 public function syncWithOriginal(): void
2511 {
2512 if (!$this->getOriginalId()) {
2513 return; // No original -> no sync
2514 }
2515 $currentID = $this->getId();
2516 $currentObjId = $this->getObjId();
2517 $originalID = $this->getOriginalId();
2518 $originalObjId = self::lookupParentObjId($this->getOriginalId());
2519
2520 if (!$originalObjId) {
2521 return; // Original does not exist -> no sync
2522 }
2523
2524 $this->beforeSyncWithOriginal($this->getOriginalId(), $this->getId(), $originalObjId, $this->getObjId());
2525
2526 // Now we become the original
2527 $this->setId($this->getOriginalId());
2528 $this->setOriginalId(null);
2529 $this->setObjId($originalObjId);
2530 // And save ourselves as the original
2531 $this->saveToDb();
2532
2533 // Now we delete the originals page content
2534 $this->deletePageOfQuestion($originalID);
2535 $this->createPageObject();
2536 $this->copyPageOfQuestion($currentID);
2537
2538 $this->setId($currentID);
2539 $this->setOriginalId($originalID);
2540 $this->setObjId($currentObjId);
2541
2542 $this->updateSuggestedSolutions($this->getOriginalId(), $originalObjId);
2543 $this->syncXHTMLMediaObjectsOfQuestion();
2544
2545 $this->afterSyncWithOriginal($this->getOriginalId(), $this->getId(), $originalObjId, $this->getObjId());
2546 $this->syncHints();
2547 }
2548
2556 public function _questionExists($question_id)
2557 {
2558 if ($question_id < 1) {
2559 return false;
2560 }
2561
2562 $result = $this->db->queryF(
2563 "SELECT question_id FROM qpl_questions WHERE question_id = %s",
2564 array('integer'),
2565 array($question_id)
2566 );
2567 return $result->numRows() == 1;
2568 }
2569
2570 public function _questionExistsInPool(int $question_id): bool
2571 {
2572 if ($question_id < 1) {
2573 return false;
2574 }
2575
2576 $result = $this->db->queryF(
2577 "SELECT question_id FROM qpl_questions INNER JOIN object_data ON obj_fi = obj_id WHERE question_id = %s AND type = 'qpl'",
2578 array('integer'),
2579 array($question_id)
2580 );
2581 return $this->db->numRows($result) == 1;
2582 }
2583
2588 public static function _instanciateQuestion(int $question_id): assQuestion
2589 {
2590 return self::_instantiateQuestion($question_id);
2591 }
2592
2596 public static function _instantiateQuestion(int $question_id): assQuestion
2597 {
2598 return self::instantiateQuestion($question_id);
2599 }
2600
2606 public static function instantiateQuestion(int $question_id): assQuestion
2607 {
2608 global $DIC;
2609 $ilCtrl = $DIC['ilCtrl'];
2610 $ilDB = $DIC['ilDB'];
2611 $lng = $DIC['lng'];
2612
2613 $question_type = assQuestion::_getQuestionType($question_id);
2614 if ($question_type === '') {
2615 throw new InvalidArgumentException('No question with ID ' . $question_id . ' exists');
2616 }
2617
2618 assQuestion::_includeClass($question_type);
2619 $question = new $question_type();
2620 $question->loadFromDb($question_id);
2621
2622 $feedbackObjectClassname = self::getFeedbackClassNameByQuestionType($question_type);
2623 $question->feedbackOBJ = new $feedbackObjectClassname($question, $ilCtrl, $ilDB, $lng);
2624
2625 return $question;
2626 }
2627
2628 public function getPoints(): float
2629 {
2630 if (strcmp($this->points, "") == 0) {
2631 return 0.0;
2632 }
2633
2634 return $this->points;
2635 }
2636
2637 public function setPoints(float $points): void
2638 {
2639 $this->points = $points;
2640 }
2641
2642 public function getSolutionMaxPass(int $active_id): ?int
2643 {
2644 return self::_getSolutionMaxPass($this->getId(), $active_id);
2645 }
2646
2650 public static function _getSolutionMaxPass(int $question_id, int $active_id): ?int
2651 {
2652 /* include_once "./Modules/Test/classes/class.ilObjTest.php";
2653 $pass = ilObjTest::_getPass($active_id);
2654 return $pass;*/
2655
2656 // the following code was the old solution which added the non answered
2657 // questions of a pass from the answered questions of the previous pass
2658 // with the above solution, only the answered questions of the last pass are counted
2659 global $DIC;
2660 $ilDB = $DIC['ilDB'];
2661
2662 $result = $ilDB->queryF(
2663 "SELECT MAX(pass) maxpass FROM tst_test_result WHERE active_fi = %s AND question_fi = %s",
2664 array('integer','integer'),
2665 array($active_id, $question_id)
2666 );
2667 if ($result->numRows() === 1) {
2668 $row = $ilDB->fetchAssoc($result);
2669 return $row["maxpass"];
2670 }
2671
2672 return null;
2673 }
2674
2675 public static function _isWriteable(int $question_id, int $user_id): bool
2676 {
2677 global $DIC;
2678 $ilDB = $DIC['ilDB'];
2679
2680 if (($question_id < 1) || ($user_id < 1)) {
2681 return false;
2682 }
2683
2684 $result = $ilDB->queryF(
2685 "SELECT obj_fi FROM qpl_questions WHERE question_id = %s",
2686 array('integer'),
2687 array($question_id)
2688 );
2689 if ($ilDB->numRows($result) == 1) {
2690 $row = $ilDB->fetchAssoc($result);
2691 $qpl_object_id = (int) $row["obj_fi"];
2692 return ilObjQuestionPool::_isWriteable($qpl_object_id, $user_id);
2693 }
2694
2695 return false;
2696 }
2697
2698 public static function _isUsedInRandomTest(int $question_id): bool
2699 {
2700 global $DIC;
2701 $ilDB = $DIC['ilDB'];
2702
2703 $result = $ilDB->queryF(
2704 "SELECT test_random_question_id FROM tst_test_rnd_qst WHERE question_fi = %s",
2705 array('integer'),
2706 array($question_id)
2707 );
2708 return $ilDB->numRows($result) > 0;
2709 }
2710
2722 abstract public function calculateReachedPoints($active_id, $pass = null, $authorizedSolution = true, $returndetails = false);
2723
2724 public function deductHintPointsFromReachedPoints(ilAssQuestionPreviewSession $previewSession, $reachedPoints): ?float
2725 {
2726 global $DIC;
2727
2728 $hintTracking = new ilAssQuestionPreviewHintTracking($DIC->database(), $previewSession);
2729 $requestsStatisticData = $hintTracking->getRequestStatisticData();
2730 $reachedPoints = $reachedPoints - $requestsStatisticData->getRequestsPoints();
2731
2732 return $reachedPoints;
2733 }
2734
2736 {
2737 $reachedPoints = $this->calculateReachedPointsForSolution($previewSession->getParticipantsSolution());
2738 $reachedPoints = $this->deductHintPointsFromReachedPoints($previewSession, $reachedPoints);
2739
2740 return $this->ensureNonNegativePoints($reachedPoints);
2741 }
2742
2743 protected function ensureNonNegativePoints($points)
2744 {
2745 return $points > 0 ? $points : 0;
2746 }
2747
2748 public function isPreviewSolutionCorrect(ilAssQuestionPreviewSession $previewSession): bool
2749 {
2750 $reachedPoints = $this->calculateReachedPointsFromPreviewSession($previewSession);
2751
2752 return !($reachedPoints < $this->getMaximumPoints());
2753 }
2754
2755
2766 final public function adjustReachedPointsByScoringOptions($points, $active_id, $pass = null): float
2767 {
2768 $count_system = ilObjTest::_getCountSystem($active_id);
2769 if ($count_system == 1) {
2770 if (abs($this->getMaximumPoints() - $points) > 0.0000000001) {
2771 $points = 0;
2772 }
2773 }
2774 $score_cutting = ilObjTest::_getScoreCutting($active_id);
2775 if ($score_cutting == 0) {
2776 if ($points < 0) {
2777 $points = 0;
2778 }
2779 }
2780 return $points;
2781 }
2782
2787 public static function _isWorkedThrough(int $active_id, int $question_id, int $pass): bool
2788 {
2789 return self::lookupResultRecordExist($active_id, $question_id, $pass);
2790 }
2791
2798 public static function _areAnswered(int $a_user_id, array $a_question_ids): bool
2799 {
2800 global $DIC;
2801 $ilDB = $DIC['ilDB'];
2802
2803 $res = $ilDB->queryF(
2804 "SELECT DISTINCT(question_fi) FROM tst_test_result JOIN tst_active " .
2805 "ON (active_id = active_fi) " .
2806 "WHERE " . $ilDB->in('question_fi', $a_question_ids, false, 'integer') .
2807 " AND user_fi = %s",
2808 array('integer'),
2809 array($a_user_id)
2810 );
2811 return ($res->numRows() == count($a_question_ids)) ? true : false;
2812 }
2813
2819 public function isHTML($a_text): bool
2820 {
2821 return ilUtil::isHTML($a_text);
2822 }
2823
2827 public function prepareTextareaOutput(string $txt_output, bool $prepare_for_latex_output = false, bool $omitNl2BrWhenTextArea = false)
2828 {
2830 $txt_output,
2831 $prepare_for_latex_output,
2832 $omitNl2BrWhenTextArea
2833 );
2834 }
2835
2840 public function QTIMaterialToString(ilQTIMaterial $a_material): string
2841 {
2842 $result = "";
2843 $mobs = ilSession::get('import_mob_xhtml') ?? [];
2844 for ($i = 0; $i < $a_material->getMaterialCount(); $i++) {
2845 $material = $a_material->getMaterial($i);
2846 if (strcmp($material["type"], "mattext") == 0) {
2847 $result .= $material["material"]->getContent();
2848 }
2849 if (strcmp($material["type"], "matimage") == 0) {
2850 $matimage = $material["material"];
2851 if (preg_match("/(il_([0-9]+)_mob_([0-9]+))/", $matimage->getLabel(), $matches)) {
2852 $mobs[] = [
2853 "mob" => $matimage->getLabel(),
2854 "uri" => $matimage->getUri()
2855 ];
2856 }
2857 }
2858 }
2859 ilSession::set('import_mob_xhtml', $mobs);
2860 return $result;
2861 }
2862
2863 public function addQTIMaterial(ilXmlWriter $a_xml_writer, string $a_material, bool $close_material_tag = true, bool $add_mobs = true): void
2864 {
2865 $a_xml_writer->xmlStartTag("material");
2866 $attrs = array(
2867 "texttype" => "text/plain"
2868 );
2869 if ($this->isHTML($a_material)) {
2870 $attrs["texttype"] = "text/xhtml";
2871 }
2872 $a_xml_writer->xmlElement("mattext", $attrs, ilRTE::_replaceMediaObjectImageSrc($a_material, 0));
2873 if ($add_mobs) {
2874 $mobs = ilObjMediaObject::_getMobsOfObject("qpl:html", $this->getId());
2875 foreach ($mobs as $mob) {
2876 $moblabel = "il_" . IL_INST_ID . "_mob_" . $mob;
2877 if (strpos($a_material, "mm_$mob") !== false) {
2878 if (ilObjMediaObject::_exists($mob)) {
2879 $mob_obj = new ilObjMediaObject($mob);
2880 $imgattrs = array(
2881 "label" => $moblabel,
2882 "uri" => "objects/" . "il_" . IL_INST_ID . "_mob_" . $mob . "/" . $mob_obj->getTitle()
2883 );
2884 }
2885 $a_xml_writer->xmlElement("matimage", $imgattrs, null);
2886 }
2887 }
2888 }
2889 if ($close_material_tag) {
2890 $a_xml_writer->xmlEndTag("material");
2891 }
2892 }
2893
2894 public function buildHashedImageFilename(string $plain_image_filename, bool $unique = false): string
2895 {
2896 $extension = "";
2897
2898 if (preg_match("/.*\.(png|jpg|gif|jpeg)$/i", $plain_image_filename, $matches)) {
2899 $extension = "." . $matches[1];
2900 }
2901
2902 if ($unique) {
2903 $plain_image_filename = uniqid($plain_image_filename . microtime(true), true);
2904 }
2905
2906 return md5($plain_image_filename) . $extension;
2907 }
2908
2919 public static function _setReachedPoints(int $active_id, int $question_id, float $points, float $maxpoints, int $pass, bool $manualscoring, bool $obligationsEnabled): bool
2920 {
2921 global $DIC;
2922 $ilDB = $DIC['ilDB'];
2923 $refinery = $DIC['refinery'];
2924
2925 $float_trafo = $refinery->kindlyTo()->float();
2926 try {
2927 $points = $float_trafo->transform($points);
2928 } catch (ILIAS\Refinery\ConstraintViolationException $e) {
2929 return false;
2930 }
2931
2932 if ($points <= $maxpoints) {
2933 if ($pass === null) {
2934 $pass = assQuestion::_getSolutionMaxPass($question_id, $active_id);
2935 }
2936
2937 $rowsnum = 0;
2938 $old_points = 0;
2939
2940 if ($pass !== null) {
2941 $result = $ilDB->queryF(
2942 "SELECT points FROM tst_test_result WHERE active_fi = %s AND question_fi = %s AND pass = %s",
2943 array('integer','integer','integer'),
2944 array($active_id, $question_id, $pass)
2945 );
2946 $manual = ($manualscoring) ? 1 : 0;
2947 $rowsnum = $result->numRows();
2948 }
2949 if ($rowsnum > 0) {
2950 $row = $ilDB->fetchAssoc($result);
2951 $old_points = $row["points"];
2952 if ($old_points != $points) {
2953 $affectedRows = $ilDB->manipulateF(
2954 "UPDATE tst_test_result SET points = %s, manual = %s, tstamp = %s WHERE active_fi = %s AND question_fi = %s AND pass = %s",
2955 array('float', 'integer', 'integer', 'integer', 'integer', 'integer'),
2956 array($points, $manual, time(), $active_id, $question_id, $pass)
2957 );
2958 }
2959 } else {
2960 $next_id = $ilDB->nextId('tst_test_result');
2961 $affectedRows = $ilDB->manipulateF(
2962 "INSERT INTO tst_test_result (test_result_id, active_fi, question_fi, points, pass, manual, tstamp) VALUES (%s, %s, %s, %s, %s, %s, %s)",
2963 array('integer', 'integer','integer', 'float', 'integer', 'integer','integer'),
2964 array($next_id, $active_id, $question_id, $points, $pass, $manual, time())
2965 );
2966 }
2967
2968 if (self::isForcePassResultUpdateEnabled() || $old_points != $points || $rowsnum == 0) {
2969 assQuestion::_updateTestPassResults($active_id, $pass, $obligationsEnabled);
2972 global $DIC;
2973 $lng = $DIC['lng'];
2974 $ilUser = $DIC['ilUser'];
2975 $username = ilObjTestAccess::_getParticipantData($active_id);
2976 assQuestion::logAction(sprintf(
2977 $lng->txtlng(
2978 "assessment",
2979 "log_answer_changed_points",
2981 ),
2982 $username,
2983 $old_points,
2984 $points,
2985 $ilUser->getFullname() . " (" . $ilUser->getLogin() . ")"
2986 ), $active_id, $question_id);
2987 }
2988 }
2989
2990 return true;
2991 }
2992
2993 return false;
2994 }
2995
2996 public function getQuestion(): string
2997 {
2998 return $this->question;
2999 }
3000
3001 public function getQuestionForHTMLOutput(): string
3002 {
3003 return $this->purifyAndPrepareTextAreaOutput($this->question);
3004 }
3005
3006 protected function purifyAndPrepareTextAreaOutput(string $content): string
3007 {
3008 $purified_content = $this->getHtmlQuestionContentPurifier()->purify($content);
3009 if ($this->isAdditionalContentEditingModePageObject()
3010 || !(new ilSetting('advanced_editing'))->get('advanced_editing_javascript_editor') === 'tinymce') {
3011 $purified_content = nl2br($purified_content);
3012 }
3013 return $this->prepareTextareaOutput(
3014 $purified_content,
3015 true,
3016 true
3017 );
3018 }
3019
3020 public function setQuestion(string $question = ""): void
3021 {
3022 $this->question = $question;
3023 }
3024
3030 abstract public function getQuestionType(): string;
3031
3032 public function getQuestionTypeID(): int
3033 {
3034 $result = $this->db->queryF(
3035 "SELECT question_type_id FROM qpl_qst_type WHERE type_tag = %s",
3036 array('text'),
3037 array($this->getQuestionType())
3038 );
3039 if ($this->db->numRows($result) == 1) {
3040 $row = $this->db->fetchAssoc($result);
3041 return (int) $row["question_type_id"];
3042 }
3043 return 0;
3044 }
3045
3046 public function syncHints(): void
3047 {
3048 // delete hints of the original
3049 $this->db->manipulateF(
3050 "DELETE FROM qpl_hints WHERE qht_question_fi = %s",
3051 array('integer'),
3052 array($this->original_id)
3053 );
3054
3055 // get hints of the actual question
3056 $result = $this->db->queryF(
3057 "SELECT * FROM qpl_hints WHERE qht_question_fi = %s",
3058 array('integer'),
3059 array($this->getId())
3060 );
3061
3062 // save hints to the original
3063 if ($this->db->numRows($result) > 0) {
3064 while ($row = $this->db->fetchAssoc($result)) {
3065 $next_id = $this->db->nextId('qpl_hints');
3066 $this->db->insert(
3067 'qpl_hints',
3068 array(
3069 'qht_hint_id' => array('integer', $next_id),
3070 'qht_question_fi' => array('integer', $this->original_id),
3071 'qht_hint_index' => array('integer', $row["qht_hint_index"]),
3072 'qht_hint_points' => array('float', $row["qht_hint_points"]),
3073 'qht_hint_text' => array('text', $row["qht_hint_text"]),
3074 )
3075 );
3076 }
3077 }
3078 }
3079
3080 protected function getRTETextWithMediaObjects(): string
3081 {
3082 // must be called in parent classes. add additional RTE text in the parent
3083 // classes and call this method to add the standard RTE text
3084 $collected = $this->getQuestion();
3085 $collected .= $this->feedbackOBJ->getGenericFeedbackContent($this->getId(), false);
3086 $collected .= $this->feedbackOBJ->getGenericFeedbackContent($this->getId(), true);
3087 $collected .= $this->feedbackOBJ->getAllSpecificAnswerFeedbackContents($this->getId());
3088
3089 foreach ($this->suggested_solutions as $solution_array) {
3090 if (is_string(['value'])) {
3091 $collected .= $solution_array["value"];
3092 }
3093 }
3094 $questionHintList = ilAssQuestionHintList::getListByQuestionId($this->getId());
3095 foreach ($questionHintList as $questionHint) {
3096 /* @var $questionHint ilAssQuestionHint */
3097 $collected .= $questionHint->getText();
3098 }
3099
3100 return $collected;
3101 }
3102
3103 public function cleanupMediaObjectUsage(): void
3104 {
3105 $combinedtext = $this->getRTETextWithMediaObjects();
3106 ilRTE::_cleanupMediaObjectUsage($combinedtext, "qpl:html", $this->getId());
3107 }
3108
3109 public function getInstances(): array
3110 {
3111 $result = $this->db->queryF(
3112 "SELECT question_id FROM qpl_questions WHERE original_id = %s",
3113 array("integer"),
3114 array($this->getId())
3115 );
3116 $instances = [];
3117 $ids = [];
3118 while ($row = $this->db->fetchAssoc($result)) {
3119 $ids[] = $row["question_id"];
3120 }
3121 foreach ($ids as $question_id) {
3122 // check non random tests
3123 $result = $this->db->queryF(
3124 "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",
3125 array("integer"),
3126 array($question_id)
3127 );
3128 while ($row = $this->db->fetchAssoc($result)) {
3129 $instances[$row['obj_fi']] = ilObject::_lookupTitle($row['obj_fi']);
3130 }
3131 // check random tests
3132 $result = $this->db->queryF(
3133 "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",
3134 array("integer"),
3135 array($question_id)
3136 );
3137 while ($row = $this->db->fetchAssoc($result)) {
3138 $instances[$row['obj_fi']] = ilObject::_lookupTitle($row['obj_fi']);
3139 }
3140 }
3141 foreach ($instances as $key => $value) {
3142 $instances[$key] = array("obj_id" => $key, "title" => $value, "author" => ilObjTest::_lookupAuthor($key), "refs" => ilObject::_getAllReferences($key));
3143 }
3144 return $instances;
3145 }
3146
3147 public static function _needsManualScoring(int $question_id): bool
3148 {
3150 $questiontype = assQuestion::_getQuestionType($question_id);
3151 if (in_array($questiontype, $scoring)) {
3152 return true;
3153 }
3154
3155 return false;
3156 }
3157
3164 public function getActiveUserData(int $active_id): array
3165 {
3166 $result = $this->db->queryF(
3167 "SELECT * FROM tst_active WHERE active_id = %s",
3168 array('integer'),
3169 array($active_id)
3170 );
3171 if ($this->db->numRows($result)) {
3172 $row = $this->db->fetchAssoc($result);
3173 return array("user_id" => $row["user_fi"], "test_id" => $row["test_fi"]);
3174 }
3175
3176 return [];
3177 }
3178
3179 public function hasSpecificFeedback(): bool
3180 {
3181 return static::HAS_SPECIFIC_FEEDBACK;
3182 }
3183
3184 public static function _includeClass(string $question_type, int $gui = 0): void
3185 {
3186 if (self::isCoreQuestionType($question_type)) {
3187 self::includeCoreClass($question_type, $gui);
3188 }
3189 }
3190
3191 public static function getFeedbackClassNameByQuestionType(string $questionType): string
3192 {
3193 return str_replace('ass', 'ilAss', $questionType) . 'Feedback';
3194 }
3195
3196 public static function isCoreQuestionType(string $questionType): bool
3197 {
3198 return file_exists("Modules/TestQuestionPool/classes/class.{$questionType}GUI.php");
3199 }
3200
3201 public static function includeCoreClass($questionType, $withGuiClass): void
3202 {
3203 if ($withGuiClass) {
3204 // object class is included by gui classes constructor
3205 } else {
3206 }
3207
3208 $feedbackClassName = self::getFeedbackClassNameByQuestionType($questionType);
3209 }
3210
3211 public static function _getQuestionTypeName($type_tag): string
3212 {
3213 global $DIC;
3214 if (file_exists("./Modules/TestQuestionPool/classes/class." . $type_tag . ".php")) {
3215 $lng = $DIC['lng'];
3216 return $lng->txt($type_tag);
3217 }
3218 $component_factory = $DIC['component.factory'];
3219
3220 foreach ($component_factory->getActivePluginsInSlot("qst") as $pl) {
3221 if ($pl->getQuestionType() === $type_tag) {
3222 return $pl->getQuestionTypeTranslation();
3223 }
3224 }
3225 return "";
3226 }
3227
3232 public static function _instanciateQuestionGUI(int $question_id): assQuestionGUI
3233 {
3234 return self::instantiateQuestionGUI($question_id);
3235 }
3236
3237 public static function instantiateQuestionGUI(int $a_question_id): assQuestionGUI
3238 {
3239 //Shouldn't you live in assQuestionGUI, Mister?
3240
3241 global $DIC;
3242 $ilCtrl = $DIC['ilCtrl'];
3243 $ilDB = $DIC['ilDB'];
3244 $lng = $DIC['lng'];
3245 $ilUser = $DIC['ilUser'];
3246
3247 if (strcmp($a_question_id, "") != 0) {
3248 $question_type = assQuestion::_getQuestionType($a_question_id);
3249
3250 assQuestion::_includeClass($question_type, 1);
3251
3252 $question_type_gui = $question_type . 'GUI';
3253 $question_gui = new $question_type_gui();
3254 $question_gui->object->loadFromDb($a_question_id);
3255
3256 $feedbackObjectClassname = self::getFeedbackClassNameByQuestionType($question_type);
3257 $question_gui->object->feedbackOBJ = new $feedbackObjectClassname($question_gui->object, $ilCtrl, $ilDB, $lng);
3258
3259 $assSettings = new ilSetting('assessment');
3260 $processLockerFactory = new ilAssQuestionProcessLockerFactory($assSettings, $ilDB);
3261 $processLockerFactory->setQuestionId($question_gui->object->getId());
3262 $processLockerFactory->setUserId($ilUser->getId());
3263 $processLockerFactory->setAssessmentLogEnabled(ilObjAssessmentFolder::_enabledAssessmentLogging());
3264 $question_gui->object->setProcessLocker($processLockerFactory->getLocker());
3265 } else {
3266 global $DIC;
3267 $ilLog = $DIC['ilLog'];
3268 $ilLog->write('Instantiate question called without question id. (instantiateQuestionGUI@assQuestion)', $ilLog->WARNING);
3269 throw new InvalidArgumentException('Instantiate question called without question id. (instantiateQuestionGUI@assQuestion)');
3270 }
3271 return $question_gui;
3272 }
3273
3274 public function setExportDetailsXLS(ilAssExcelFormatHelper $worksheet, int $startrow, int $active_id, int $pass): int
3275 {
3276 $worksheet->setFormattedExcelTitle($worksheet->getColumnCoord(0) . $startrow, $this->lng->txt($this->getQuestionType()));
3277 $worksheet->setFormattedExcelTitle($worksheet->getColumnCoord(1) . $startrow, $this->getTitle());
3278
3279 return $startrow;
3280 }
3281
3287 public function __get($value)
3288 {
3289 throw new BadMethodCallException('assQuestion::__get is discouraged, used with: ' . $value);
3290 }
3291
3297 public function __set($key, $value)
3298 {
3299 throw new BadMethodCallException('assQuestion::__set is discouraged, used with: ' . $key);
3300 }
3301
3307 public function __isset($key)
3308 {
3309 throw new BadMethodCallException('assQuestion::__isset is discouraged, used with: ' . $key);
3310 }
3311
3312 public function getNrOfTries(): int
3313 {
3314 return $this->nr_of_tries;
3315 }
3316
3317 public function setNrOfTries(int $a_nr_of_tries): void
3318 {
3319 $this->nr_of_tries = $a_nr_of_tries;
3320 }
3321
3322 public function setExportImagePath(string $path): void
3323 {
3324 $this->export_image_path = $path;
3325 }
3326
3327 public static function _questionExistsInTest(int $question_id, int $test_id): bool
3328 {
3329 global $DIC;
3330 $ilDB = $DIC['ilDB'];
3331
3332 if ($question_id < 1) {
3333 return false;
3334 }
3335
3336 $result = $ilDB->queryF(
3337 "SELECT question_fi FROM tst_test_question WHERE question_fi = %s AND test_fi = %s",
3338 array('integer', 'integer'),
3339 array($question_id, $test_id)
3340 );
3341 return $ilDB->numRows($result) == 1;
3342 }
3343
3344 public function formatSAQuestion($a_q): string
3345 {
3346 return $this->getSelfAssessmentFormatter()->format($a_q);
3347 }
3348
3350 {
3351 return new \ilAssSelfAssessmentQuestionFormatter();
3352 }
3353
3354 // scorm2004-start ???
3355
3356 public function setPreventRteUsage(bool $prevent_rte_usage): void
3357 {
3358 $this->prevent_rte_usage = $prevent_rte_usage;
3359 }
3360
3361 public function getPreventRteUsage(): bool
3362 {
3363 return $this->prevent_rte_usage;
3364 }
3365
3367 {
3368 $this->lmMigrateQuestionTypeGenericContent($migrator);
3369 $this->lmMigrateQuestionTypeSpecificContent($migrator);
3370 $this->saveToDb();
3371
3372 $this->feedbackOBJ->migrateContentForLearningModule($migrator, $this->getId());
3373 }
3374
3376 {
3377 $this->setQuestion($migrator->migrateToLmContent($this->getQuestion()));
3378 }
3379
3381 {
3382 // overwrite if any question type specific content except feedback needs to be migrated
3383 }
3384
3385 public function setSelfAssessmentEditingMode(bool $selfassessmenteditingmode): void
3386 {
3387 $this->selfassessmenteditingmode = $selfassessmenteditingmode;
3388 }
3389
3390 public function getSelfAssessmentEditingMode(): bool
3391 {
3392 return $this->selfassessmenteditingmode;
3393 }
3394
3395 public function setDefaultNrOfTries(int $defaultnroftries): void
3396 {
3397 $this->defaultnroftries = $defaultnroftries;
3398 }
3399
3400 public function getDefaultNrOfTries(): int
3401 {
3402 return $this->defaultnroftries;
3403 }
3404
3405 // scorm2004-end ???
3406
3407 public static function lookupParentObjId(int $questionId): int
3408 {
3409 global $DIC;
3410 $ilDB = $DIC['ilDB'];
3411
3412 $query = "SELECT obj_fi FROM qpl_questions WHERE question_id = %s";
3413
3414 $res = $ilDB->queryF($query, array('integer'), array($questionId));
3415 $row = $ilDB->fetchAssoc($res);
3416
3417 return $row['obj_fi'];
3418 }
3419
3426 public static function lookupOriginalParentObjId(int $originalQuestionId): int
3427 {
3428 return self::lookupParentObjId($originalQuestionId);
3429 }
3430
3431 protected function duplicateQuestionHints(int $originalQuestionId, int $duplicateQuestionId): void
3432 {
3433 $hintIds = ilAssQuestionHintList::duplicateListForQuestion($originalQuestionId, $duplicateQuestionId);
3434
3435 if ($this->isAdditionalContentEditingModePageObject()) {
3436 foreach ($hintIds as $originalHintId => $duplicateHintId) {
3437 $this->ensureHintPageObjectExists($originalHintId);
3438 $originalPageObject = new ilAssHintPage($originalHintId);
3439 $originalXML = $originalPageObject->getXMLContent();
3440
3441 $duplicatePageObject = new ilAssHintPage();
3442 $duplicatePageObject->setId($duplicateHintId);
3443 $duplicatePageObject->setParentId($this->getId());
3444 $duplicatePageObject->setXMLContent($originalXML);
3445 $duplicatePageObject->createFromXML();
3446 }
3447 }
3448 }
3449
3450 protected function duplicateSkillAssignments(int $srcParentId, int $srcQuestionId, int $trgParentId, int $trgQuestionId): void
3451 {
3452 $assignmentList = new ilAssQuestionSkillAssignmentList($this->db);
3453 $assignmentList->setParentObjId($srcParentId);
3454 $assignmentList->setQuestionIdFilter($srcQuestionId);
3455 $assignmentList->loadFromDb();
3456
3457 foreach ($assignmentList->getAssignmentsByQuestionId($srcQuestionId) as $assignment) {
3458 $assignment->setParentObjId($trgParentId);
3459 $assignment->setQuestionId($trgQuestionId);
3460 $assignment->saveToDb();
3461
3462 // add skill usage
3464 $trgParentId,
3465 $assignment->getSkillBaseId(),
3466 $assignment->getSkillTrefId()
3467 );
3468 }
3469 }
3470
3471 public function syncSkillAssignments(int $srcParentId, int $srcQuestionId, int $trgParentId, int $trgQuestionId): void
3472 {
3473 $assignmentList = new ilAssQuestionSkillAssignmentList($this->db);
3474 $assignmentList->setParentObjId($trgParentId);
3475 $assignmentList->setQuestionIdFilter($trgQuestionId);
3476 $assignmentList->loadFromDb();
3477
3478 foreach ($assignmentList->getAssignmentsByQuestionId($trgQuestionId) as $assignment) {
3479 $assignment->deleteFromDb();
3480
3481 // remove skill usage
3482 if (!$assignment->isSkillUsed()) {
3484 $assignment->getParentObjId(),
3485 $assignment->getSkillBaseId(),
3486 $assignment->getSkillTrefId(),
3487 false
3488 );
3489 }
3490 }
3491
3492 $this->duplicateSkillAssignments($srcParentId, $srcQuestionId, $trgParentId, $trgQuestionId);
3493 }
3494
3495 public function ensureHintPageObjectExists($pageObjectId): void
3496 {
3497 if (!ilAssHintPage::_exists('qht', $pageObjectId)) {
3498 $pageObject = new ilAssHintPage();
3499 $pageObject->setParentId($this->getId());
3500 $pageObject->setId($pageObjectId);
3501 $pageObject->createFromXML();
3502 }
3503 }
3504
3505 public function isAnswered(int $active_id, int $pass): bool
3506 {
3507 return true;
3508 }
3509
3510 public static function isObligationPossible(int $questionId): bool
3511 {
3512 return false;
3513 }
3514
3515 public function isAutosaveable(): bool
3516 {
3517 return true;
3518 }
3519
3520 protected static function getNumExistingSolutionRecords(int $activeId, int $pass, int $questionId): int
3521 {
3522 global $DIC;
3523 $ilDB = $DIC['ilDB'];
3524
3525 $query = "
3526 SELECT count(active_fi) cnt
3527
3528 FROM tst_solutions
3529
3530 WHERE active_fi = %s
3531 AND question_fi = %s
3532 AND pass = %s
3533 ";
3534
3535 $res = $ilDB->queryF(
3536 $query,
3537 array('integer','integer','integer'),
3538 array($activeId, $questionId, $pass)
3539 );
3540
3541 $row = $ilDB->fetchAssoc($res);
3542
3543 return (int) $row['cnt'];
3544 }
3545
3546 public function getAdditionalContentEditingMode(): string
3547 {
3548 return $this->additionalContentEditingMode;
3549 }
3550
3551 public function setAdditionalContentEditingMode(?string $additionalContentEditingMode): void
3552 {
3553 if (!in_array((string) $additionalContentEditingMode, $this->getValidAdditionalContentEditingModes())) {
3554 throw new ilTestQuestionPoolException('invalid additional content editing mode given: ' . $additionalContentEditingMode);
3555 }
3556
3557 $this->additionalContentEditingMode = $additionalContentEditingMode;
3558 }
3559
3561 {
3562 return $this->getAdditionalContentEditingMode() == assQuestion::ADDITIONAL_CONTENT_EDITING_MODE_IPE;
3563 }
3564
3565 public function isValidAdditionalContentEditingMode(string $additionalContentEditingMode): bool
3566 {
3567 if (in_array($additionalContentEditingMode, $this->getValidAdditionalContentEditingModes())) {
3568 return true;
3569 }
3570
3571 return false;
3572 }
3573
3575 {
3576 return array(
3577 self::ADDITIONAL_CONTENT_EDITING_MODE_RTE,
3578 self::ADDITIONAL_CONTENT_EDITING_MODE_IPE
3579 );
3580 }
3581
3586 {
3587 return ilHtmlPurifierFactory::getInstanceByType('qpl_usersolution');
3588 }
3589
3594 {
3595 return ilHtmlPurifierFactory::getInstanceByType('qpl_usersolution');
3596 }
3597
3598 protected function buildQuestionDataQuery(): string
3599 {
3600 return "
3601 SELECT qpl_questions.*,
3602 {$this->getAdditionalTableName()}.*
3603 FROM qpl_questions
3604 LEFT JOIN {$this->getAdditionalTableName()}
3605 ON {$this->getAdditionalTableName()}.question_fi = qpl_questions.question_id
3606 WHERE qpl_questions.question_id = %s
3607 ";
3608 }
3609
3610 public function setLastChange($lastChange): void
3611 {
3612 $this->lastChange = $lastChange;
3613 }
3614
3615 public function getLastChange()
3616 {
3617 return $this->lastChange;
3618 }
3619
3620 protected function getCurrentSolutionResultSet(int $active_id, int $pass, bool $authorized = true): \ilDBStatement
3621 {
3622 if ($this->getStep() !== null) {
3623 $query = "
3624 SELECT *
3625 FROM tst_solutions
3626 WHERE active_fi = %s
3627 AND question_fi = %s
3628 AND pass = %s
3629 AND step = %s
3630 AND authorized = %s
3631 ";
3632
3633 return $this->db->queryF(
3634 $query,
3635 array('integer', 'integer', 'integer', 'integer', 'integer'),
3636 array($active_id, $this->getId(), $pass, $this->getStep(), (int) $authorized)
3637 );
3638 }
3639
3640 $query = "
3641 SELECT *
3642 FROM tst_solutions
3643 WHERE active_fi = %s
3644 AND question_fi = %s
3645 AND pass = %s
3646 AND authorized = %s
3647 ";
3648
3649 return $this->db->queryF(
3650 $query,
3651 array('integer', 'integer', 'integer', 'integer'),
3652 array($active_id, $this->getId(), $pass, (int) $authorized)
3653 );
3654 }
3655
3656 protected function removeSolutionRecordById(int $solutionId): int
3657 {
3658 return $this->db->manipulateF(
3659 "DELETE FROM tst_solutions WHERE solution_id = %s",
3660 array('integer'),
3661 array($solutionId)
3662 );
3663 }
3664
3665 // hey: prevPassSolutions - selected file reuse, copy solution records
3669 protected function getSolutionRecordById(int $solutionId): array
3670 {
3671 $result = $this->db->queryF(
3672 "SELECT * FROM tst_solutions WHERE solution_id = %s",
3673 array('integer'),
3674 array($solutionId)
3675 );
3676
3677 if ($this->db->numRows($result) > 0) {
3678 return $this->db->fetchAssoc($result);
3679 }
3680 return [];
3681 }
3682 // hey.
3683
3684 public function removeIntermediateSolution(int $active_id, int $pass): void
3685 {
3686 $this->getProcessLocker()->executeUserSolutionUpdateLockOperation(function () use ($active_id, $pass) {
3687 $this->removeCurrentSolution($active_id, $pass, false);
3688 });
3689 }
3690
3694 public function removeCurrentSolution(int $active_id, int $pass, bool $authorized = true): int
3695 {
3696 if ($this->getStep() !== null) {
3697 $query = '
3698 DELETE FROM tst_solutions
3699 WHERE active_fi = %s
3700 AND question_fi = %s
3701 AND pass = %s
3702 AND step = %s
3703 AND authorized = %s
3704 ';
3705
3706 return $this->db->manipulateF(
3707 $query,
3708 array('integer', 'integer', 'integer', 'integer', 'integer'),
3709 array($active_id, $this->getId(), $pass, $this->getStep(), (int) $authorized)
3710 );
3711 }
3712
3713 $query = "
3714 DELETE FROM tst_solutions
3715 WHERE active_fi = %s
3716 AND question_fi = %s
3717 AND pass = %s
3718 AND authorized = %s
3719 ";
3720
3721 return $this->db->manipulateF(
3722 $query,
3723 array('integer', 'integer', 'integer', 'integer'),
3724 array($active_id, $this->getId(), $pass, (int) $authorized)
3725 );
3726 }
3727
3728 // fau: testNav - add timestamp as parameter to saveCurrentSolution
3729 public function saveCurrentSolution(int $active_id, int $pass, $value1, $value2, bool $authorized = true, $tstamp = 0): int
3730 {
3731 $next_id = $this->db->nextId("tst_solutions");
3732
3733 $fieldData = array(
3734 "solution_id" => array("integer", $next_id),
3735 "active_fi" => array("integer", $active_id),
3736 "question_fi" => array("integer", $this->getId()),
3737 "value1" => array("clob", $value1),
3738 "value2" => array("clob", $value2),
3739 "pass" => array("integer", $pass),
3740 "tstamp" => array("integer", ((int) $tstamp > 0) ? (int) $tstamp : time()),
3741 'authorized' => array('integer', (int) $authorized)
3742 );
3743
3744 if ($this->getStep() !== null) {
3745 $fieldData['step'] = array("integer", $this->getStep());
3746 }
3747
3748 return $this->db->insert("tst_solutions", $fieldData);
3749 }
3750 // fau.
3751
3752 public function updateCurrentSolution(int $solutionId, $value1, $value2, bool $authorized = true): int
3753 {
3754 $fieldData = array(
3755 "value1" => array("clob", $value1),
3756 "value2" => array("clob", $value2),
3757 "tstamp" => array("integer", time()),
3758 'authorized' => array('integer', (int) $authorized)
3759 );
3760
3761 if ($this->getStep() !== null) {
3762 $fieldData['step'] = array("integer", $this->getStep());
3763 }
3764
3765 return $this->db->update("tst_solutions", $fieldData, array(
3766 'solution_id' => array('integer', $solutionId)
3767 ));
3768 }
3769
3770 // fau: testNav - added parameter to keep the timestamp (default: false)
3771 public function updateCurrentSolutionsAuthorization(int $activeId, int $pass, bool $authorized, bool $keepTime = false): int
3772 {
3773 $fieldData = array(
3774 'authorized' => array('integer', (int) $authorized)
3775 );
3776
3777 if (!$keepTime) {
3778 $fieldData['tstamp'] = array('integer', time());
3779 }
3780
3781 $whereData = array(
3782 'question_fi' => array('integer', $this->getId()),
3783 'active_fi' => array('integer', $activeId),
3784 'pass' => array('integer', $pass)
3785 );
3786
3787 if ($this->getStep() !== null) {
3788 $whereData['step'] = array("integer", $this->getStep());
3789 }
3790
3791 return $this->db->update('tst_solutions', $fieldData, $whereData);
3792 }
3793 // fau.
3794
3795 // hey: prevPassSolutions - motivation slowly decreases on imagemap
3796 public const KEY_VALUES_IMPLOSION_SEPARATOR = ':';
3797
3798 public static function implodeKeyValues(array $keyValues): string
3799 {
3800 return implode(assQuestion::KEY_VALUES_IMPLOSION_SEPARATOR, $keyValues);
3801 }
3802
3803 public static function explodeKeyValues(string $keyValues): array
3804 {
3805 return explode(assQuestion::KEY_VALUES_IMPLOSION_SEPARATOR, $keyValues);
3806 }
3807
3808 protected function deleteDummySolutionRecord(int $activeId, int $passIndex): void
3809 {
3810 foreach ($this->getSolutionValues($activeId, $passIndex, false) as $solutionRec) {
3811 if ($solutionRec['value1'] == '' && $solutionRec['value2'] == '') {
3812 $this->removeSolutionRecordById($solutionRec['solution_id']);
3813 }
3814 }
3815 }
3816
3817 protected function isDummySolutionRecord(array $solutionRecord): bool
3818 {
3819 return !strlen($solutionRecord['value1']) && !strlen($solutionRecord['value2']);
3820 }
3821
3822 protected function deleteSolutionRecordByValues(int $activeId, int $passIndex, bool $authorized, array $matchValues): void
3823 {
3824 $types = array("integer", "integer", "integer", "integer");
3825 $values = array($activeId, $this->getId(), $passIndex, (int) $authorized);
3826 $valuesCondition = [];
3827
3828 foreach ($matchValues as $valueField => $value) {
3829 switch ($valueField) {
3830 case 'value1':
3831 case 'value2':
3832 $valuesCondition[] = "{$valueField} = %s";
3833 $types[] = 'text';
3834 $values[] = $value;
3835 break;
3836
3837 default:
3838 throw new ilTestQuestionPoolException('invalid value field given: ' . $valueField);
3839 }
3840 }
3841
3842 $valuesCondition = implode(' AND ', $valuesCondition);
3843
3844 $query = "
3845 DELETE FROM tst_solutions
3846 WHERE active_fi = %s
3847 AND question_fi = %s
3848 AND pass = %s
3849 AND authorized = %s
3850 AND $valuesCondition
3851 ";
3852
3853 if ($this->getStep() !== null) {
3854 $query .= " AND step = %s ";
3855 $types[] = 'integer';
3856 $values[] = $this->getStep();
3857 }
3858
3859 $this->db->manipulateF($query, $types, $values);
3860 }
3861
3862 protected function duplicateIntermediateSolutionAuthorized(int $activeId, int $passIndex): void
3863 {
3864 foreach ($this->getSolutionValues($activeId, $passIndex, false) as $rec) {
3865 $this->saveCurrentSolution($activeId, $passIndex, $rec['value1'], $rec['value2'], true, $rec['tstamp']);
3866 }
3867 }
3868
3869 protected function forceExistingIntermediateSolution(int $activeId, int $passIndex, bool $considerDummyRecordCreation): void
3870 {
3871 $intermediateSolution = $this->getSolutionValues($activeId, $passIndex, false);
3872
3873 if (!count($intermediateSolution)) {
3874 // make the authorized solution intermediate (keeping timestamps)
3875 // this keeps the solution_ids in synch with eventually selected in $_POST['deletefiles']
3876 $this->updateCurrentSolutionsAuthorization($activeId, $passIndex, false, true);
3877
3878 // create a backup as authorized solution again (keeping timestamps)
3879 $this->duplicateIntermediateSolutionAuthorized($activeId, $passIndex);
3880
3881 if ($considerDummyRecordCreation) {
3882 // create an additional dummy record to indicate the existence of an intermediate solution
3883 // even if all entries are deleted from the intermediate solution later
3884 $this->saveCurrentSolution($activeId, $passIndex, null, null, false);
3885 }
3886 }
3887 }
3888 // hey.
3889
3890
3891
3895 public function setStep($step): void
3896 {
3897 $this->step = $step;
3898 }
3899
3903 public function getStep(): ?int
3904 {
3905 return $this->step;
3906 }
3907
3908 public static function convertISO8601FormatH_i_s_ExtendedToSeconds(string $time): int
3909 {
3910 $sec = 0;
3911 $time_array = explode(':', $time);
3912 if (count($time_array) == 3) {
3913 $sec += (int) $time_array[0] * 3600;
3914 $sec += (int) $time_array[1] * 60;
3915 $sec += (int) $time_array[2];
3916 }
3917 return $sec;
3918 }
3919
3920 public function toJSON(): string
3921 {
3922 return json_encode([]);
3923 }
3924
3925 abstract public function duplicate(bool $for_test = true, string $title = "", string $author = "", string $owner = "", $testObjId = null): int;
3926
3927 // hey: prevPassSolutions - check for authorized solution
3928 public function intermediateSolutionExists(int $active_id, int $pass): bool
3929 {
3930 $solutionAvailability = $this->lookupForExistingSolutions($active_id, $pass);
3931 return (bool) $solutionAvailability['intermediate'];
3932 }
3933
3934 public function authorizedSolutionExists(int $active_id, ?int $pass): bool
3935 {
3936 if ($pass === null) {
3937 return false;
3938 }
3939 $solutionAvailability = $this->lookupForExistingSolutions($active_id, $pass);
3940 return (bool) $solutionAvailability['authorized'];
3941 }
3942
3943 public function authorizedOrIntermediateSolutionExists(int $active_id, int $pass): bool
3944 {
3945 $solutionAvailability = $this->lookupForExistingSolutions($active_id, $pass);
3946 return $solutionAvailability['authorized'] || $solutionAvailability['intermediate'];
3947 }
3948 // hey.
3949
3950 protected function lookupMaxStep(int $active_id, int $pass): int
3951 {
3952 $result = $this->db->queryF(
3953 "SELECT MAX(step) max_step FROM tst_solutions WHERE active_fi = %s AND pass = %s AND question_fi = %s",
3954 array("integer", "integer", "integer"),
3955 array($active_id, $pass, $this->getId())
3956 );
3957
3958 $row = $this->db->fetchAssoc($result);
3959
3960 return (int) $row['max_step'];
3961 }
3962
3963 // fau: testNav - new function lookupForExistingSolutions
3968 public function lookupForExistingSolutions(int $activeId, int $pass): array
3969 {
3970 $return = array(
3971 'authorized' => false,
3972 'intermediate' => false
3973 );
3974
3975 $query = "
3976 SELECT authorized, COUNT(*) cnt
3977 FROM tst_solutions
3978 WHERE active_fi = %s
3979 AND question_fi = %s
3980 AND pass = %s
3981 ";
3982
3983 if ($this->getStep() !== null) {
3984 $query .= " AND step = " . $this->db->quote((int) $this->getStep(), 'integer') . " ";
3985 }
3986
3987 $query .= "
3988 GROUP BY authorized
3989 ";
3990
3991 $result = $this->db->queryF($query, array('integer', 'integer', 'integer'), array($activeId, $this->getId(), $pass));
3992
3993 while ($row = $this->db->fetchAssoc($result)) {
3994 if ($row['authorized']) {
3995 $return['authorized'] = $row['cnt'] > 0;
3996 } else {
3997 $return['intermediate'] = $row['cnt'] > 0;
3998 }
3999 }
4000 return $return;
4001 }
4002 // fau.
4003
4004 public function isAddableAnswerOptionValue(int $qIndex, string $answerOptionValue): bool
4005 {
4006 return false;
4007 }
4008
4009 public function addAnswerOptionValue(int $qIndex, string $answerOptionValue, float $points): void
4010 {
4011 }
4012
4013 public function removeAllExistingSolutions(): void
4014 {
4015 $query = "DELETE FROM tst_solutions WHERE question_fi = %s";
4016 $this->db->manipulateF($query, array('integer'), array($this->getId()));
4017 }
4018
4019 public function removeExistingSolutions(int $activeId, int $pass): int
4020 {
4021 $query = "
4022 DELETE FROM tst_solutions
4023 WHERE active_fi = %s
4024 AND question_fi = %s
4025 AND pass = %s
4026 ";
4027
4028 if ($this->getStep() !== null) {
4029 $query .= " AND step = " . $this->db->quote((int) $this->getStep(), 'integer') . " ";
4030 }
4031
4032 return $this->db->manipulateF(
4033 $query,
4034 array('integer', 'integer', 'integer'),
4035 array($activeId, $this->getId(), $pass)
4036 );
4037 }
4038
4039 public function resetUsersAnswer(int $activeId, int $pass): void
4040 {
4041 $this->removeExistingSolutions($activeId, $pass);
4042 $this->removeResultRecord($activeId, $pass);
4043
4044 $this->log($activeId, "log_user_solution_willingly_deleted");
4045
4046 self::_updateTestPassResults(
4047 $activeId,
4048 $pass,
4049 $this->areObligationsToBeConsidered(),
4050 $this->getProcessLocker(),
4051 $this->getTestId()
4052 );
4053 }
4054
4055 public function removeResultRecord(int $activeId, int $pass): int
4056 {
4057 $query = "
4058 DELETE FROM tst_test_result
4059 WHERE active_fi = %s
4060 AND question_fi = %s
4061 AND pass = %s
4062 ";
4063
4064 if ($this->getStep() !== null) {
4065 $query .= " AND step = " . $this->db->quote((int) $this->getStep(), 'integer') . " ";
4066 }
4067
4068 return $this->db->manipulateF(
4069 $query,
4070 array('integer', 'integer', 'integer'),
4071 array($activeId, $this->getId(), $pass)
4072 );
4073 }
4074
4075 public static function missingResultRecordExists(int $activeId, int $pass, array $questionIds): bool
4076 {
4077 global $DIC;
4078 $ilDB = $DIC['ilDB'];
4079
4080 $IN_questionIds = $ilDB->in('question_fi', $questionIds, false, 'integer');
4081
4082 $query = "
4083 SELECT COUNT(*) cnt
4084 FROM tst_test_result
4085 WHERE active_fi = %s
4086 AND pass = %s
4087 AND $IN_questionIds
4088 ";
4089
4090 $row = $ilDB->fetchAssoc($ilDB->queryF(
4091 $query,
4092 array('integer', 'integer'),
4093 array($activeId, $pass)
4094 ));
4095
4096 return $row['cnt'] < count($questionIds);
4097 }
4098
4099 public static function getQuestionsMissingResultRecord(int $activeId, int $pass, array $questionIds): array
4100 {
4101 global $DIC;
4102 $ilDB = $DIC['ilDB'];
4103
4104 $IN_questionIds = $ilDB->in('question_fi', $questionIds, false, 'integer');
4105
4106 $query = "
4107 SELECT question_fi
4108 FROM tst_test_result
4109 WHERE active_fi = %s
4110 AND pass = %s
4111 AND $IN_questionIds
4112 ";
4113
4114 $res = $ilDB->queryF(
4115 $query,
4116 array('integer', 'integer'),
4117 array($activeId, $pass)
4118 );
4119
4120 $questionsHavingResultRecord = [];
4121
4122 while ($row = $ilDB->fetchAssoc($res)) {
4123 $questionsHavingResultRecord[] = $row['question_fi'];
4124 }
4125
4126 $questionsMissingResultRecordt = array_diff(
4127 $questionIds,
4128 $questionsHavingResultRecord
4129 );
4130
4131 return $questionsMissingResultRecordt;
4132 }
4133
4134 public static function lookupResultRecordExist(int $activeId, int $questionId, int $pass): bool
4135 {
4136 global $DIC;
4137 $ilDB = $DIC['ilDB'];
4138
4139 $query = "
4140 SELECT COUNT(*) cnt
4141 FROM tst_test_result
4142 WHERE active_fi = %s
4143 AND question_fi = %s
4144 AND pass = %s
4145 ";
4146
4147 $row = $ilDB->fetchAssoc($ilDB->queryF($query, array('integer', 'integer', 'integer'), array($activeId, $questionId, $pass)));
4148
4149 return $row['cnt'] > 0;
4150 }
4151
4152 public function fetchValuePairsFromIndexedValues(array $indexedValues): array
4153 {
4154 $valuePairs = [];
4155
4156 foreach ($indexedValues as $value1 => $value2) {
4157 $valuePairs[] = array('value1' => $value1, 'value2' => $value2);
4158 }
4159
4160 return $valuePairs;
4161 }
4162
4163 public function fetchIndexedValuesFromValuePairs(array $valuePairs): array
4164 {
4165 $indexedValues = [];
4166
4167 foreach ($valuePairs as $valuePair) {
4168 $indexedValues[ $valuePair['value1'] ] = $valuePair['value2'];
4169 }
4170
4171 return $indexedValues;
4172 }
4173
4174 public function areObligationsToBeConsidered(): bool
4175 {
4176 return $this->obligationsToBeConsidered;
4177 }
4178
4179 public function setObligationsToBeConsidered(bool $obligationsToBeConsidered): void
4180 {
4181 $this->obligationsToBeConsidered = $obligationsToBeConsidered;
4182 }
4183
4184 public function updateTimestamp(): void
4185 {
4186 $this->db->manipulateF(
4187 "UPDATE qpl_questions SET tstamp = %s WHERE question_id = %s",
4188 array('integer', 'integer'),
4189 array(time(), $this->getId())
4190 );
4191 }
4192
4193 // fau: testNav - new function getTestQuestionConfig()
4194 // hey: prevPassSolutions - get caching independent from configuration (config once)
4195 // renamed: getTestPresentationConfig() -> does the caching
4196 // completed: extracted instance building
4197 // avoids configuring cached instances on every access
4198 // allows a stable reconfigure of the instance from outside
4199 private ?ilTestQuestionConfig $testQuestionConfigInstance = null;
4200
4202 {
4203 if ($this->testQuestionConfigInstance === null) {
4204 $this->testQuestionConfigInstance = $this->buildTestPresentationConfig();
4205 }
4206
4207 return $this->testQuestionConfigInstance;
4208 }
4209
4217 {
4218 return new ilTestQuestionConfig();
4219 }
4220 // hey.
4221 // fau.
4222
4223 public function savePartial(): bool
4224 {
4225 return false;
4226 }
4227
4228 /* doubles isInUse? */
4229 public function isInActiveTest(): bool
4230 {
4231 $query = 'SELECT user_fi FROM tst_active ' . PHP_EOL
4232 . 'JOIN tst_test_question ON tst_test_question.test_fi = tst_active.test_fi ' . PHP_EOL
4233 . 'JOIN qpl_questions ON qpl_questions.question_id = tst_test_question.question_fi ' . PHP_EOL
4234 . 'WHERE qpl_questions.obj_fi = ' . $this->db->quote($this->getObjId(), 'integer');
4235
4236 $res = $this->db->query($query);
4237 return $res->numRows() > 0;
4238 }
4239
4254 public static function extendedTrim(string $value): string
4255 {
4256 return preg_replace(self::TRIM_PATTERN, '', $value);
4257 }
4258
4259}
$id
plugin.php for ilComponentBuildPluginInfoObjectiveTest::testAddPlugins
Definition: plugin.php:23
if(!defined('PATH_SEPARATOR')) $GLOBALS['_PEAR_default_error_mode']
Definition: PEAR.php:64
$filename
Definition: buildRTE.php:78
$comment
Definition: buildRTE.php:72
Customizing of pimple-DIC for ILIAS.
Definition: Container.php:32
Provides fluid interface to LoggingServices.
Builds data types.
Definition: Factory.php:21
Class Services.
Definition: Services.php:38
return true
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Abstract basic class which is to be extended by the concrete assessment question type classes.
array $suggested_solutions
loadSuggestedSolution(int $question_id, int $subquestion_index=0)
Returns a suggested solution for a given subquestion index.
_getSuggestedSolution(int $question_id, int $subquestion_index=0)
setExportDetailsXLS(ilAssExcelFormatHelper $worksheet, int $startrow, int $active_id, int $pass)
fixSvgToPng(string $imageFilenameContainingString)
isAddableAnswerOptionValue(int $qIndex, string $answerOptionValue)
const KEY_VALUES_IMPLOSION_SEPARATOR
lookupTestId(int $active_id)
@refactor Move to ilObjTest or similar
static _instanciateQuestionGUI(int $question_id)
loadFromDb(int $question_id)
string $export_image_path
(Web) Path to images
const HAS_SPECIFIC_FEEDBACK
const ADDITIONAL_CONTENT_EDITING_MODE_RTE
deletePageOfQuestion(int $question_id)
usageNumber(int $question_id=0)
Returns the number of place the question is in use in pools or tests.
static _isWorkedThrough(int $active_id, int $question_id, int $pass)
Returns true if the question was worked through in the given pass Worked through means that the user ...
static originalQuestionExists(int $questionId)
generateExternalId(int $question_id)
ilAssQuestionLifecycle $lifecycle
_questionExistsInPool(int $question_id)
static _getMaximumPoints(int $question_id)
Returns the maximum points, a learner can reach answering the question.
onDuplicate(int $originalParentId, int $originalQuestionId, int $duplicateParentId, int $duplicateQuestionId)
float $points
The maximum available points for the question.
setOriginalId(?int $original_id)
static _getTotalRightAnswers(int $a_q_id)
string $question
The question text.
static _getReachedPoints(int $active_id, int $question_id, int $pass)
ilDBInterface $db
bool $obligationsToBeConsidered
const ADDITIONAL_CONTENT_EDITING_MODE_IPE
setProcessLocker(ilAssQuestionProcessLocker $processLocker)
setObligationsToBeConsidered(bool $obligationsToBeConsidered)
static logAction(string $logtext, int $active_id, int $question_id)
ilObjUser $current_user
fromXML($item, int $questionpool_id, ?int $tst_id, &$tst_object, int &$question_counter, array $import_mapping, array &$solutionhints=[])
Receives parameters from a QTI parser and creates a valid ILIAS question object.
setId(int $id=-1)
ensureHintPageObjectExists($pageObjectId)
const MAXIMUM_THUMB_SIZE
updateCurrentSolutionsAuthorization(int $activeId, int $pass, bool $authorized, bool $keepTime=false)
lmMigrateQuestionTypeGenericContent(ilAssSelfAssessmentMigrator $migrator)
getUserSolutionPreferingIntermediate(int $active_id, $pass=null)
savePreviewData(ilAssQuestionPreviewSession $previewSession)
const MINIMUM_THUMB_SIZE
prepareTextareaOutput(string $txt_output, bool $prepare_for_latex_output=false, bool $omitNl2BrWhenTextArea=false)
duplicateSuggestedSolutionFiles(int $parent_id, int $question_id)
Duplicates the files of a suggested solution if the question is duplicated.
isInUse(int $question_id=0)
Checks whether the question is in use or not in pools or tests.
isComplete()
Returns true, if a question is complete for use.
static isAllowedImageFileExtension(string $mimeType, string $fileExtension)
ilGlobalPageTemplate $tpl
static _areAnswered(int $a_user_id, array $a_question_ids)
Checks if an array of question ids is answered by a user or not.
int $outputType
Contains the output type of a question.
authorizedOrIntermediateSolutionExists(int $active_id, int $pass)
static _getQuestionType(int $question_id)
adjustReachedPointsByScoringOptions($points, $active_id, $pass=null)
Adjust the given reached points by checks for all special scoring options in the test container.
isNonEmptyItemListPostSubmission(string $postSubmissionFieldname)
setAdditionalContentEditingMode(?string $additionalContentEditingMode)
static missingResultRecordExists(int $activeId, int $pass, array $questionIds)
deleteSolutionRecordByValues(int $activeId, int $passIndex, bool $authorized, array $matchValues)
buildHashedImageFilename(string $plain_image_filename, bool $unique=false)
isPreviewSolutionCorrect(ilAssQuestionPreviewSession $previewSession)
static _getQuestionInfo(int $question_id)
setOutputType(int $outputType=OUTPUT_HTML)
authorizedSolutionExists(int $active_id, ?int $pass)
fetchValuePairsFromIndexedValues(array $indexedValues)
static getAllowedFileExtensionsForMimeType(string $mimeType)
lmMigrateQuestionTypeSpecificContent(ilAssSelfAssessmentMigrator $migrator)
static isFileAvailable(string $file)
updateSuggestedSolutions(int $original_id=-1, int $original_obj_id=-1)
static _getQuestionTypeName($type_tag)
static isObligationPossible(int $questionId)
getSuggestedSolutionTitle(int $subquestion_index=0)
Returns the title of a suggested solution at a given subquestion_index.
addQTIMaterial(ilXmlWriter $a_xml_writer, string $a_material, bool $close_material_tag=true, bool $add_mobs=true)
__isset($key)
Object issetter.
onCopy(int $sourceParentId, int $sourceQuestionId, int $targetParentId, int $targetQuestionId)
saveCurrentSolution(int $active_id, int $pass, $value1, $value2, bool $authorized=true, $tstamp=0)
removeResultRecord(int $activeId, int $pass)
deleteAdditionalTableData(int $question_id)
syncSkillAssignments(int $srcParentId, int $srcQuestionId, int $trgParentId, int $trgQuestionId)
isHTML($a_text)
Checks if a given string contains HTML or not.
buildTestPresentationConfig()
build basic test question configuration instance
moveUploadedMediaFile(string $file, string $name)
Move an uploaded media file to an public accessible temp dir to present it.
setShuffle(?bool $shuffle=true)
persistPreviewState(ilAssQuestionPreviewSession $previewSession)
persists the preview state for current user and question
static $forcePassResultsUpdateEnabled
getSolutionValues($active_id, $pass=null, bool $authorized=true)
Loads solutions of a given user from the database an returns it.
setTestId(int $id=-1)
ilTestQuestionConfig $testQuestionConfig
static _questionExistsInTest(int $question_id, int $test_id)
static getQuestionsMissingResultRecord(int $activeId, int $pass, array $questionIds)
resetUsersAnswer(int $activeId, int $pass)
ILIAS HTTP Services $http
static extendedTrim(string $value)
Trim non-printable characters from the beginning and end of a string.
_resolveInternalLink(string $internal_link)
migrateContentForLearningModule(ilAssSelfAssessmentMigrator $migrator)
updateCurrentSolution(int $solutionId, $value1, $value2, bool $authorized=true)
fetchIndexedValuesFromValuePairs(array $valuePairs)
deductHintPointsFromReachedPoints(ilAssQuestionPreviewSession $previewSession, $reachedPoints)
static $allowedImageMaterialFileExtensionsByMimeType
deleteAnswers(int $question_id)
getSolutionRecordById(int $solutionId)
ILIAS Refinery Factory $refinery
setQuestion(string $question="")
calculateReachedPoints($active_id, $pass=null, $authorizedSolution=true, $returndetails=false)
Returns the points, a learner has reached answering the question.
ilAssQuestionProcessLocker $processLocker
calculateReachedPointsFromPreviewSession(ilAssQuestionPreviewSession $previewSession)
QTIMaterialToString(ilQTIMaterial $a_material)
Reads an QTI material tag and creates a text or XHTML string.
buildImagePath($questionId, $parentObjectId)
isValidAdditionalContentEditingMode(string $additionalContentEditingMode)
isAnswered(int $active_id, int $pass)
static _getSolutionMaxPass(int $question_id, int $active_id)
Returns the maximum pass a users question solution.
getFlashPath()
Returns the image path for web accessable flash files of a question.
_resolveIntLinks(int $question_id)
__get($value)
Object getter.
getImagePath($question_id=null, $object_id=null)
Returns the image path for web accessable images of a question.
static instantiateQuestionGUI(int $a_question_id)
getCurrentSolutionResultSet(int $active_id, int $pass, bool $authorized=true)
afterSyncWithOriginal(int $origQuestionId, int $dupQuestionId, int $origParentObjId, int $dupParentObjId)
__construct(string $title="", string $comment="", string $author="", int $owner=-1, string $question="")
assQuestion constructor
copyPageOfQuestion(int $a_q_id)
static _getOriginalId(int $question_id)
setPreventRteUsage(bool $prevent_rte_usage)
setSelfAssessmentEditingMode(bool $selfassessmenteditingmode)
calculateResultsFromSolution(int $active_id, int $pass, bool $obligationsEnabled=false)
Calculates the question results from a previously saved question solution.
static _includeClass(string $question_type, int $gui=0)
ILIAS DI LoggingServices $ilLog
saveQuestionDataToDb(int $original_id=-1)
setAuthor(string $author="")
static _instanciateQuestion(int $question_id)
static setForcePassResultUpdateEnabled(bool $forcePassResultsUpdateEnabled)
getSuggestedSolution(int $subquestion_index=0)
Returns a suggested solution for a given subquestion index.
getTestOutputSolutions(int $activeId, int $pass)
getQuestionType()
Returns the question type of the question.
setThumbSize(int $a_size)
static isCoreQuestionType(string $questionType)
static _instantiateQuestion(int $question_id)
removeExistingSolutions(int $activeId, int $pass)
static _getQuestionText(int $a_q_id)
getValidAdditionalContentEditingModes()
isClone(int $question_id=0)
Checks whether the question is a clone of another question or not.
static implodeKeyValues(array $keyValues)
static getQuestionTypeFromDb(int $question_id)
static explodeKeyValues(string $keyValues)
bool $shuffle
Indicates whether the answers will be shuffled or not.
setExportImagePath(string $path)
deleteDummySolutionRecord(int $activeId, int $passIndex)
static lookupOriginalParentObjId(int $originalQuestionId)
returns the parent object id for given original question id (should be a qpl id, but theoretically it...
static convertISO8601FormatH_i_s_ExtendedToSeconds(string $time)
__set($key, $value)
Object setter.
static _isUsedInRandomTest(int $question_id)
getActiveUserData(int $active_id)
Returns the user id and the test id for a given active id.
setNewOriginalId(int $newId)
static $allowedCharsetsByMimeType
lookupCurrentTestPass(int $active_id, int $pass)
static saveOriginalId(int $questionId, int $originalId)
static isForcePassResultUpdateEnabled()
persistWorkingState(int $active_id, $pass, bool $obligationsEnabled=false, bool $authorized=true)
persists the working state for current testactive and testpass
static _getSuggestedSolutionOutput(int $question_id)
bool $selfassessmenteditingmode
copySuggestedSolutionFiles(int $source_questionpool_id, int $source_question_id)
duplicateQuestionHints(int $originalQuestionId, int $duplicateQuestionId)
addAnswerOptionValue(int $qIndex, string $answerOptionValue, float $points)
purifyAndPrepareTextAreaOutput(string $content)
ensureCurrentTestPass(int $active_id, int $pass)
removeIntermediateSolution(int $active_id, int $pass)
static fetchMimeTypeIdentifier(string $contentType)
int $test_id
The database id of a test in which the question is contained.
setComment(string $comment="")
static $allowedFileExtensionsByMimeType
Transformation $shuffler
static _getTitle(int $a_q_id)
duplicateSkillAssignments(int $srcParentId, int $srcQuestionId, int $trgParentId, int $trgQuestionId)
setObjId(int $obj_id=0)
removeSolutionRecordById(int $solutionId)
static _needsManualScoring(int $question_id)
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...
static includeCoreClass($questionType, $withGuiClass)
forceExistingIntermediateSolution(int $activeId, int $passIndex, bool $considerDummyRecordCreation)
ilAssQuestionFeedback $feedbackOBJ
static getFeedbackClassNameByQuestionType(string $questionType)
setSuggestedSolution(string $solution_id="", int $subquestion_index=0, bool $is_import=false)
Sets a suggested solution for the question.
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)
log(int $active_id, string $langVar)
getAdjustedReachedPoints(int $active_id, int $pass, bool $authorizedSolution=true)
returns the reached points ...
static _getQuestionTitle(int $question_id)
setOwner(int $owner=-1)
copyXHTMLMediaObjectsOfQuestion(int $a_q_id)
setNrOfTries(int $a_nr_of_tries)
syncSuggestedSolutionFiles(int $target_question_id, int $target_obj_id)
setLifecycle(ilAssQuestionLifecycle $lifecycle)
static getNumExistingSolutionRecords(int $activeId, int $pass, int $questionId)
fixUnavailableSkinImageSources(string $html)
static instantiateQuestion(int $question_id)
saveWorkingData(int $active_id, int $pass, bool $authorized=true)
Saves the learners input of the question to the database.
isAdditionalContentEditingModePageObject()
setExternalId(?string $external_id)
duplicate(bool $for_test=true, string $title="", string $author="", string $owner="", $testObjId=null)
lookupForExistingSolutions(int $activeId, int $pass)
Lookup if an authorized or intermediate solution exists.
int $obj_id
Object id of the container object.
static resetOriginalId(int $questionId)
static _updateTestResultCache(int $active_id, ilAssQuestionProcessLocker $processLocker=null)
@TODO Move this to a proper place.
setTitle(string $title="")
removeCurrentSolution(int $active_id, int $pass, bool $authorized=true)
beforeSyncWithOriginal(int $origQuestionId, int $dupQuestionId, int $origParentObjId, int $dupParentObjId)
isDummySolutionRecord(array $solutionRecord)
static _getInternalLinkHref(string $target="")
static isAllowedImageMimeType($mimeType)
static lookupResultRecordExist(int $activeId, int $questionId, int $pass)
setDefaultNrOfTries(int $defaultnroftries)
static _getSuggestedSolutionCount(int $question_id)
questionTitleExists(int $questionpool_id, string $title)
Returns TRUE if the question title exists in a question pool in the database.
static lookupParentObjId(int $questionId)
static _isWriteable(int $question_id, int $user_id)
_questionExists($question_id)
Returns true if the question already exists in the database.
intermediateSolutionExists(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)
setLastChange($lastChange)
setShuffler(Transformation $shuffler)
static getAllowedImageMaterialFileExtensions()
static _setReachedPoints(int $active_id, int $question_id, float $points, float $maxpoints, int $pass, bool $manualscoring, bool $obligationsEnabled)
Sets the points, a learner has reached answering the question Additionally objective results are upda...
lookupMaxStep(int $active_id, int $pass)
const DEFAULT_THUMB_SIZE
setPoints(float $points)
ensureNonNegativePoints($points)
string $questionActionCmd
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setFormattedExcelTitle($coordinates, $value)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static deleteHintsByQuestionIds($questionIds)
Deletes all question hints relating to questions included in given question ids.
static duplicateListForQuestion($originalQuestionId, $duplicateQuestionId)
duplicates a hint list from given original question id to given duplicate question id and returns an ...
static getListByQuestionId($questionId)
instantiates a question hint list for the passed question id
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)
getColumnCoord(int $a_col)
Get column "name" from number.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
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 createDirectory(string $a_dir, int $a_mod=0755)
create directory
static removeTrailingPathSeparators(string $path)
static moveUploadedFile(string $a_file, string $a_name, string $a_target, bool $a_raise_errors=true, string $a_mode="move_uploaded")
move uploaded file
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)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Definition: class.ilLog.php:31
static _addLog( $user_id, $object_id, $logtext, $question_id=0, $original_id=0, $test_only=false, $test_ref_id=0)
Add an assessment log entry.
static _getManualScoringTypes()
Retrieve the manual scoring settings as type strings.
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($object_id)
Updates the number of available questions for a question pool in the database.
static getUsageOfObject(int $a_obj_id, bool $a_include_titles=false)
static _getParticipantData($active_id)
Retrieves a participant name from active id.
static _getResultPass($active_id)
Retrieves the pass number that should be counted for a given user.
static _getObjectIDFromActiveID($active_id)
Returns the ILIAS test object id for a given active id.
static _getCountSystem($active_id)
Gets the count system for the calculation of points.
static _lookupAuthor($obj_id)
Gets the authors name of the ilObjTest object.
static _getQuestionCountAndPointsForPassOfParticipant($active_id, $pass)
static _getPass($active_id)
Retrieves the actual pass of a given user for a given test.
static buildExamId($active_id, $pass, $test_obj_id=null)
static _getWorkingTimeOfParticipantForPass($active_id, $pass)
Returns the complete working time in seconds for a test participant.
static _getUserIdFromActiveId($active_id)
static _getScoreCutting($active_id)
Determines if the score of a question should be cut at 0 points or the score of the whole test.
static isQuestionObligatory($question_id)
checks wether the question with given id is marked as obligatory or not
User class.
static _getAllReferences(int $id)
get all reference ids for object ID
static _lookupObjId(int $ref_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.
getMaterial(int $a_index)
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...
static get(string $a_var)
static set(string $a_var, $a_val)
Set a value.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static setUsage(int $a_obj_id, int $a_skill_id, int $a_tref_id, bool $a_use=true)
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 isHTML(string $a_text)
Checks if a given string contains HTML or not.
static getImagePath(string $img, 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)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
xmlElement(string $tag, $attrs=null, $data=null, $encode=true, $escape=true)
Writes a basic element (no children, just textual content)
xmlEndTag(string $tag)
Writes an endtag.
xmlStartTag(string $tag, ?array $attrs=null, bool $empty=false, bool $encode=true, bool $escape=true)
Writes a starttag.
const CLIENT_WEB_DIR
Definition: constants.php:47
const IL_INST_ID
Definition: constants.php:40
global $DIC
Definition: feed.php:28
$target_id
Definition: goto.php:52
$mobs
Definition: imgupload.php:70
$ilUser
Definition: imgupload.php:34
const OUTPUT_HTML
const OUTPUT_JAVASCRIPT
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.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Interface for html sanitizing functionality.
$path
Definition: ltiservices.php:32
$res
Definition: ltiservices.php:69
if($format !==null) $name
Definition: metadata.php:247
$index
Definition: metadata.php:145
$i
Definition: metadata.php:41
$xml
Definition: metadata.php:351
static http()
Fetches the global http state from ILIAS.
string $key
Consumer key/client ID value.
Definition: System.php:193
Refinery Factory $refinery
Class ChatMainBarProvider \MainMenu\Provider.
$query
$type
$lng
$message
Definition: xapiexit.php:32