ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
class.ilObjTest.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
28use ILIAS\Test\ExportImport\Factory as ExportImportFactory;
29use ILIAS\Test\ExportImport\Types as ExportImportTypes;
37use ILIAS\Test\Settings\GlobalSettings\Repository as GlobalSettingsRepository;
51use ILIAS\Refinery\Factory as Refinery;
54
65class ilObjTest extends ilObject
66{
68
69 public const QUESTION_SET_TYPE_FIXED = 'FIXED_QUEST_SET';
70 public const QUESTION_SET_TYPE_RANDOM = 'RANDOM_QUEST_SET';
71 public const INVITATION_OFF = 0;
72 public const INVITATION_ON = 1;
73 public const SCORE_LAST_PASS = 0;
74 public const SCORE_BEST_PASS = 1;
75
76 private ?bool $activation_limited = null;
77 private array $mob_ids;
78 private array $file_ids = [];
79 private bool $online;
83 private ?MarkSchema $mark_schema = null;
84 public int $test_id = -1;
86 public string $author;
87
91 public $metadata;
92 public array $questions = [];
93
98
102 public $test_sequence = false;
103
104 private int $template_id = 0;
105
106 protected bool $print_best_solution_with_result = true;
107
108 protected bool $activation_visibility = false;
109 protected ?int $activation_starting_time = null;
110 protected ?int $activation_ending_time = null;
111
117 private $participantDataExist = null;
118
119 private ?int $tmpCopyWizardCopyId = null;
120
123 protected Refinery $refinery;
127
128 protected GlobalSettingsRepository $global_settings_repo;
129 protected ?MainSettings $main_settings = null;
133
137
138 protected ExportImportFactory $export_factory;
139
143
147
148 protected LOMetadata $lo_metadata;
149
156 public function __construct(int $id = 0, bool $a_call_by_reference = true)
157 {
158 $this->type = "tst";
159
161 global $DIC;
162 $this->ctrl = $DIC['ilCtrl'];
163 $this->refinery = $DIC['refinery'];
164 $this->settings = $DIC['ilSetting'];
165 $this->bench = $DIC['ilBench'];
166 $this->component_repository = $DIC['component.repository'];
167 $this->component_factory = $DIC['component.factory'];
168 $this->filesystem_web = $DIC->filesystem()->web();
169 $this->lo_metadata = $DIC->learningObjectMetadata();
170
171 $local_dic = $this->getLocalDIC();
172 $this->participant_access_filter = $local_dic['participant.access_filter.factory'];
173 $this->test_man_scoring_done_helper = $local_dic['scoring.manual.done_helper'];
174 $this->logger = $local_dic['logging.logger'];
175 $this->log_viewer = $local_dic['logging.viewer'];
176 $this->global_settings_repo = $local_dic['settings.global.repository'];
177 $this->marks_repository = $local_dic['marks.repository'];
178 $this->questionrepository = $local_dic['question.general_properties.repository'];
179 $this->testrequest = $local_dic['request_data_collector'];
180 $this->participant_repository = $local_dic['participant.repository'];
181 $this->export_factory = $local_dic['exportimport.factory'];
182 $this->test_result_repository = $local_dic['results.data.repository'];
183
184 parent::__construct($id, $a_call_by_reference);
185
186 $this->lng->loadLanguageModule("assessment");
187 $this->score_settings = null;
188
189 $this->question_set_config_factory = new ilTestQuestionSetConfigFactory(
190 $this->tree,
191 $this->db,
192 $this->lng,
193 $this->logger,
194 $this->component_repository,
195 $this,
196 $this->questionrepository
197 );
198 }
199
200 public function getLocalDIC(): TestDIC
201 {
202 return TestDIC::dic();
203 }
204
205 public function getTestLogger(): TestLogger
206 {
207 return $this->logger;
208 }
209
211 {
212 return $this->log_viewer;
213 }
214
216 {
217 return $this->question_set_config_factory->getQuestionSetConfig();
218 }
219
223 public function getTitleFilenameCompliant(): string
224 {
226 }
227
228 public function getTmpCopyWizardCopyId(): ?int
229 {
231 }
232
233 public function setTmpCopyWizardCopyId(int $tmpCopyWizardCopyId): void
234 {
235 $this->tmpCopyWizardCopyId = $tmpCopyWizardCopyId;
236 }
237
238 public function create(): int
239 {
240 $id = parent::create();
241 $this->createMetaData();
242 return $id;
243 }
244
245 public function update(): bool
246 {
247 if (!parent::update()) {
248 return false;
249 }
250
251 // put here object specific stuff
252 $this->updateMetaData();
253 return true;
254 }
255
256 public function read(): void
257 {
258 parent::read();
259 $this->main_settings = null;
260 $this->score_settings = null;
261 $this->mark_schema = null;
262 $this->loadFromDb();
263 }
264
265 public function delete(): bool
266 {
267 // always call parent delete function first!!
268 if (!parent::delete()) {
269 return false;
270 }
271
272 // delet meta data
273 $this->deleteMetaData();
274
275 //put here your module specific stuff
276 $this->deleteTest();
277
278 $qsaImportFails = new ilAssQuestionSkillAssignmentImportFails($this->getId());
279 $qsaImportFails->deleteRegisteredImportFails();
280 $sltImportFails = new ilTestSkillLevelThresholdImportFails($this->getId());
281 $sltImportFails->deleteRegisteredImportFails();
282
283 if ($this->logger->isLoggingEnabled()) {
284 $this->logger->logTestAdministrationInteraction(
285 $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
286 $this->getRefId(),
287 $this->user->getId(),
289 [
290 AdditionalInformationGenerator::KEY_TEST_TITLE => $test_title = $this->title
291 ]
292 )
293 );
294 }
295
296 return true;
297 }
298
299 public function deleteTest(): void
300 {
301 $participantData = new ilTestParticipantData($this->db, $this->lng);
302 $participantData->load($this->getTestId());
303 $this->removeTestResults($participantData);
304
305 $this->db->manipulateF(
306 "DELETE FROM tst_mark WHERE test_fi = %s",
307 ['integer'],
308 [$this->getTestId()]
309 );
310
311 $this->db->manipulateF(
312 "DELETE FROM tst_tests WHERE test_id = %s",
313 ['integer'],
314 [$this->getTestId()]
315 );
316
317 $tst_data_dir = ilFileUtils::getDataDir() . "/tst_data";
318 $directory = $tst_data_dir . "/tst_" . $this->getId();
319 if (is_dir($directory)) {
320 ilFileUtils::delDir($directory);
321 }
322 $mobs = ilObjMediaObject::_getMobsOfObject("tst:html", $this->getId());
323 // remaining usages are not in text anymore -> delete them
324 // and media objects (note: delete method of ilObjMediaObject
325 // checks whether object is used in another context; if yes,
326 // the object is not deleted!)
327 foreach ($mobs as $mob) {
328 ilObjMediaObject::_removeUsage($mob, "tst:html", $this->getId());
329 if (ilObjMediaObject::_exists($mob)) {
330 $mob_obj = new ilObjMediaObject($mob);
331 $mob_obj->delete();
332 }
333 }
334 }
335
341 public function createExportDirectory(): void
342 {
343 $tst_data_dir = ilFileUtils::getDataDir() . "/tst_data";
344 ilFileUtils::makeDir($tst_data_dir);
345 if (!is_writable($tst_data_dir)) {
346 $this->ilias->raiseError("Test Data Directory (" . $tst_data_dir
347 . ") not writeable.", $this->ilias->error_obj->MESSAGE);
348 }
349
350 // create learning module directory (data_dir/lm_data/lm_<id>)
351 $tst_dir = $tst_data_dir . "/tst_" . $this->getId();
352 ilFileUtils::makeDir($tst_dir);
353 if (!@is_dir($tst_dir)) {
354 $this->ilias->raiseError("Creation of Test Directory failed.", $this->ilias->error_obj->MESSAGE);
355 }
356 // create Export subdirectory (data_dir/lm_data/lm_<id>/Export)
357 $export_dir = $tst_dir . "/export";
358 ilFileUtils::makeDir($export_dir);
359 if (!@is_dir($export_dir)) {
360 $this->ilias->raiseError("Creation of Export Directory failed.", $this->ilias->error_obj->MESSAGE);
361 }
362 }
363
364 public function getExportDirectory(): string
365 {
366 $export_dir = ilFileUtils::getDataDir() . "/tst_data" . "/tst_" . $this->getId() . "/export";
367 return $export_dir;
368 }
369
370 public function getExportFiles(string $dir = ''): array
371 {
372 // quit if import dir not available
373 if (!@is_dir($dir) || !is_writable($dir)) {
374 return [];
375 }
376
377 $files = [];
378 foreach (new DirectoryIterator($dir) as $file) {
382 if ($file->isDir()) {
383 continue;
384 }
385
386 $files[] = $file->getBasename();
387 }
388
389 sort($files);
390
391 return $files;
392 }
393
399 public static function _createImportDirectory(): string
400 {
401 global $DIC;
402 $ilias = $DIC['ilias'];
403 $tst_data_dir = ilFileUtils::getDataDir() . "/tst_data";
404 ilFileUtils::makeDir($tst_data_dir);
405
406 if (!is_writable($tst_data_dir)) {
407 $ilias->raiseError("Test Data Directory (" . $tst_data_dir
408 . ") not writeable.", $ilias->error_obj->FATAL);
409 }
410
411 // create test directory (data_dir/tst_data/tst_import)
412 $tst_dir = $tst_data_dir . "/tst_import";
413 ilFileUtils::makeDir($tst_dir);
414 if (!@is_dir($tst_dir)) {
415 $ilias->raiseError("Creation of test import directory failed.", $ilias->error_obj->FATAL);
416 }
417
418 // assert that this is empty and does not contain old data
419 ilFileUtils::delDir($tst_dir, true);
420
421 return $tst_dir;
422 }
423
424 final public function isComplete(ilTestQuestionSetConfig $test_question_set_config): bool
425 {
426 if ($this->getMarkSchema() === null
427 || $this->getMarkSchema()->getMarkSteps() === []) {
428 return false;
429 }
430
431 if (!$test_question_set_config->isQuestionSetConfigured()) {
432 return false;
433 }
434
435 return true;
436 }
437
438 public function saveCompleteStatus(ilTestQuestionSetConfig $test_question_set_config): void
439 {
440 $complete = 0;
441 if ($this->isComplete($test_question_set_config)) {
442 $complete = 1;
443 }
444 if ($this->getTestId() > 0) {
445 $this->db->manipulateF(
446 'UPDATE tst_tests SET complete = %s WHERE test_id = %s',
447 ['text', 'integer'],
448 [$complete, $this->test_id]
449 );
450 }
451 }
452
453 public function saveToDb(bool $properties_only = false): void
454 {
455 if ($this->test_id === -1) {
456 // Create new dataset
457 $next_id = $this->db->nextId('tst_tests');
458
459 $this->db->insert(
460 'tst_tests',
461 [
462 'test_id' => ['integer', $next_id],
463 'obj_fi' => ['integer', $this->getId()],
464 'created' => ['integer', time()],
465 'tstamp' => ['integer', time()],
466 'template_id' => ['integer', $this->getTemplate()]
467 ]
468 );
469
470 $this->test_id = $next_id;
471 } else {
472 if ($this->evalTotalPersons() > 0) {
473 // reset the finished status of participants if the nr of test passes did change
474 if ($this->getNrOfTries() > 0) {
475 // set all unfinished tests with nr of passes >= allowed passes finished
476 $aresult = $this->db->queryF(
477 "SELECT active_id FROM tst_active WHERE test_fi = %s AND tries >= %s AND submitted = %s",
478 ['integer', 'integer', 'integer'],
479 [$this->getTestId(), $this->getNrOfTries(), 0]
480 );
481 while ($row = $this->db->fetchAssoc($aresult)) {
482 $this->db->manipulateF(
483 "UPDATE tst_active SET submitted = %s, submittimestamp = %s WHERE active_id = %s",
484 ['integer', 'timestamp', 'integer'],
485 [1, date('Y-m-d H:i:s'), $row["active_id"]]
486 );
487 }
488
489 // set all finished tests with nr of passes < allowed passes not finished
490 $aresult = $this->db->queryF(
491 "SELECT active_id FROM tst_active WHERE test_fi = %s AND tries < %s AND submitted = %s",
492 ['integer', 'integer', 'integer'],
493 [$this->getTestId(), $this->getNrOfTries() - 1, 1]
494 );
495 while ($row = $this->db->fetchAssoc($aresult)) {
496 $this->db->manipulateF(
497 "UPDATE tst_active SET submitted = %s, submittimestamp = %s WHERE active_id = %s",
498 ['integer', 'timestamp', 'integer'],
499 [0, null, $row["active_id"]]
500 );
501 }
502 } else {
503 // set all finished tests with nr of passes >= allowed passes not finished
504 $aresult = $this->db->queryF(
505 "SELECT active_id FROM tst_active WHERE test_fi = %s AND submitted = %s",
506 ['integer', 'integer'],
507 [$this->getTestId(), 1]
508 );
509 while ($row = $this->db->fetchAssoc($aresult)) {
510 $this->db->manipulateF(
511 "UPDATE tst_active SET submitted = %s, submittimestamp = %s WHERE active_id = %s",
512 ['integer', 'timestamp', 'integer'],
513 [0, null, $row["active_id"]]
514 );
515 }
516 }
517 }
518 }
519
520 if ($properties_only) {
521 return;
522 }
523
524 if ($this->getQuestionSetType() == self::QUESTION_SET_TYPE_FIXED) {
525 $this->saveQuestionsToDb();
526 }
527
528 $this->marks_repository->storeMarkSchema($this->getMarkSchema());
529 }
530
531 public function saveQuestionsToDb(): void
532 {
533 $this->db->manipulateF(
534 'DELETE FROM tst_test_question WHERE test_fi = %s',
535 ['integer'],
536 [$this->getTestId()]
537 );
538 foreach ($this->questions as $key => $value) {
539 $next_id = $this->db->nextId('tst_test_question');
540 $this->db->insert('tst_test_question', [
541 'test_question_id' => ['integer', $next_id],
542 'test_fi' => ['integer', $this->getTestId()],
543 'question_fi' => ['integer', $value],
544 'sequence' => ['integer', $key],
545 'tstamp' => ['integer', time()]
546 ]);
547 }
548 }
549
553 public function copyQuestions(array $question_ids): void
554 {
555 $copy_count = 0;
556 $question_titles = $this->getQuestionTitles();
557
558 foreach ($question_ids as $id) {
559 $question = assQuestion::instantiateQuestionGUI($id);
560 if ($question) {
561 $title = $question->getObject()->getTitle();
562 $i = 2;
563 while (in_array($title . ' (' . $i . ')', $question_titles)) {
564 $i++;
565 }
566
567 $title .= ' (' . $i . ')';
568
569 $question_titles[] = $title;
570
571 $new_id = $question->getObject()->duplicate(false, $title);
572
573 $clone = assQuestion::instantiateQuestionGUI($new_id);
574 $question = $clone->getObject();
575 $question->setObjId($this->getId());
576 $clone->setObject($question);
577 $clone->getObject()->saveToDb();
578
579 $this->insertQuestion($new_id, true);
580
581 $copy_count++;
582 }
583 }
584 }
585
589 public function getNrOfResultsForPass($active_id, $pass): int
590 {
591 $result = $this->db->queryF(
592 "SELECT test_result_id FROM tst_test_result WHERE active_fi = %s AND pass = %s",
593 ['integer','integer'],
594 [$active_id, $pass]
595 );
596 return $result->numRows();
597 }
598
599 public function loadFromDb(): void
600 {
601 $result = $this->db->queryF(
602 "SELECT test_id FROM tst_tests WHERE obj_fi = %s",
603 ['integer'],
604 [$this->getId()]
605 );
606 if ($result->numRows() === 1) {
607 $data = $this->db->fetchObject($result);
608 $this->setTestId($data->test_id);
609 $this->loadQuestions();
610 }
611 }
612
617 public function loadQuestions(int $active_id = 0, ?int $pass = null): void
618 {
619 $this->questions = [];
620 if ($this->isRandomTest()) {
621 if ($active_id === 0) {
622 $active_id = $this->getActiveIdOfUser($this->user->getId());
623 }
624 if (is_null($pass)) {
625 $pass = self::_getPass($active_id);
626 }
627 $result = $this->db->queryF(
628 'SELECT tst_test_rnd_qst.* '
629 . 'FROM tst_test_rnd_qst, qpl_questions '
630 . 'WHERE tst_test_rnd_qst.active_fi = %s '
631 . 'AND qpl_questions.question_id = tst_test_rnd_qst.question_fi '
632 . 'AND tst_test_rnd_qst.pass = %s '
633 . 'ORDER BY sequence',
634 ['integer', 'integer'],
635 [$active_id, $pass]
636 );
637 } else {
638 $result = $this->db->queryF(
639 'SELECT tst_test_question.* '
640 . 'FROM tst_test_question, qpl_questions '
641 . 'WHERE tst_test_question.test_fi = %s '
642 . 'AND qpl_questions.question_id = tst_test_question.question_fi '
643 . 'ORDER BY sequence',
644 ['integer'],
645 [$this->test_id]
646 );
647 }
648 $index = 1;
649 if ($this->test_id !== -1) {
650 //Omit loading of questions for non-id'ed test
651 while ($data = $this->db->fetchAssoc($result)) {
652 $this->questions[$index++] = $data["question_fi"];
653 }
654 }
655 }
656
657 public function getIntroduction(): string
658 {
659 $page_id = $this->getMainSettings()->getIntroductionSettings()->getIntroductionPageId();
660 if ($page_id !== null) {
661 return (new ilTestPageGUI('tst', $page_id))->showPage();
662 }
663
665 $this->getMainSettings()->getIntroductionSettings()->getIntroductionText(),
666 1
667 );
668 }
669
670 private function cloneIntroduction(): ?int
671 {
672 $page_id = $this->getMainSettings()->getIntroductionSettings()->getIntroductionPageId();
673 if ($page_id === null) {
674 return null;
675 }
676 return $this->clonePage($page_id);
677 }
678
679 public function getFinalStatement(): string
680 {
681 $page_id = $this->getMainSettings()->getFinishingSettings()->getConcludingRemarksPageId();
682 if ($page_id !== null) {
683 return (new ilTestPageGUI('tst', $page_id))->showPage();
684 }
686 $this->getMainSettings()->getFinishingSettings()->getConcludingRemarksText()
687 );
688 }
689
690 private function cloneConcludingRemarks(): ?int
691 {
692 $page_id = $this->getMainSettings()->getFinishingSettings()->getConcludingRemarksPageId();
693 if ($page_id === null) {
694 return null;
695 }
696 return $this->clonePage($page_id);
697 }
698
699 private function clonePage(int $source_page_id): int
700 {
701 $page_object = new ilTestPage();
702 $page_object->setParentId($this->getId());
703 $new_page_id = $page_object->createPageWithNextId();
704 (new ilTestPage($source_page_id))->copy($new_page_id);
705 return $new_page_id;
706 }
707
711 public function getTestId(): int
712 {
713 return $this->test_id;
714 }
715
716 public function isPostponingEnabled(): bool
717 {
718 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getPostponedQuestionsMoveToEnd();
719 }
720
721 public function isScoreReportingEnabled(): bool
722 {
723 return $this->getScoreSettings()->getResultSummarySettings()->getScoreReporting()->isReportingEnabled();
724 }
725
726 public function getAnswerFeedbackPoints(): bool
727 {
728 return $this->getMainSettings()->getQuestionBehaviourSettings()->getInstantFeedbackPointsEnabled();
729 }
730
731 public function getGenericAnswerFeedback(): bool
732 {
733 return $this->getMainSettings()->getQuestionBehaviourSettings()->getInstantFeedbackGenericEnabled();
734 }
735
736 public function getInstantFeedbackSolution(): bool
737 {
738 return $this->getMainSettings()->getQuestionBehaviourSettings()->getInstantFeedbackSolutionEnabled();
739 }
740
741 public function getCountSystem(): int
742 {
743 return $this->getScoreSettings()->getScoringSettings()->getCountSystem();
744 }
745
746 public static function _getCountSystem($active_id)
747 {
748 global $DIC;
749 $ilDB = $DIC['ilDB'];
750 $result = $ilDB->queryF(
751 "SELECT tst_tests.count_system FROM tst_tests, tst_active WHERE tst_active.active_id = %s AND tst_active.test_fi = tst_tests.test_id",
752 ['integer'],
753 [$active_id]
754 );
755 if ($result->numRows()) {
756 $row = $ilDB->fetchAssoc($result);
757 return $row["count_system"];
758 }
759 return false;
760 }
761
765 public function getScoreCutting(): int
766 {
767 return $this->getScoreSettings()->getScoringSettings()->getScoreCutting();
768 }
769
773 public function getPassScoring(): int
774 {
775 return $this->getScoreSettings()->getScoringSettings()->getPassScoring();
776 }
777
781 public static function _getPassScoring(int $active_id): int
782 {
783 global $DIC;
784 $ilDB = $DIC['ilDB'];
785 $result = $ilDB->queryF(
786 "SELECT tst_tests.pass_scoring FROM tst_tests, tst_active WHERE tst_tests.test_id = tst_active.test_fi AND tst_active.active_id = %s",
787 ['integer'],
788 [$active_id]
789 );
790 if ($result->numRows()) {
791 $row = $ilDB->fetchAssoc($result);
792 return (int) $row["pass_scoring"];
793 }
794 return 0;
795 }
796
800 public static function _getScoreCutting(int $active_id): bool
801 {
802 global $DIC;
803 $ilDB = $DIC['ilDB'];
804 $result = $ilDB->queryF(
805 "SELECT tst_tests.score_cutting FROM tst_tests, tst_active WHERE tst_active.active_id = %s AND tst_tests.test_id = tst_active.test_fi",
806 ['integer'],
807 [$active_id]
808 );
809 if ($result->numRows()) {
810 $row = $ilDB->fetchAssoc($result);
811 return (bool) $row["score_cutting"];
812 }
813 return false;
814 }
815
816 public function getMarkSchema(): MarkSchema
817 {
818 if ($this->mark_schema === null) {
819 $this->mark_schema = $this->marks_repository->getMarkSchemaFor($this->getTestId());
820 }
821
822 return $this->mark_schema;
823 }
824
826 {
827 $this->marks_repository->storeMarkSchema($mark_schema);
828 $this->mark_schema = null;
829 }
830
831 public function getNrOfTries(): int
832 {
833 return $this->getMainSettings()->getTestBehaviourSettings()->getNumberOfTries();
834 }
835
836 public function isBlockPassesAfterPassedEnabled(): bool
837 {
838 return $this->getMainSettings()->getTestBehaviourSettings()->getBlockAfterPassedEnabled();
839 }
840
841 public function getKioskMode(): bool
842 {
843 return $this->getMainSettings()->getTestBehaviourSettings()->getKioskModeEnabled();
844 }
845
846 public function getShowKioskModeTitle(): bool
847 {
848 return $this->getMainSettings()->getTestBehaviourSettings()->getShowTitleInKioskMode();
849 }
850 public function getShowKioskModeParticipant(): bool
851 {
852 return $this->getMainSettings()->getTestBehaviourSettings()->getShowParticipantNameInKioskMode();
853 }
854
855 public function getUsePreviousAnswers(): bool
856 {
857 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getUsePreviousAnswerAllowed();
858 }
859
860 public function getTitleOutput(): int
861 {
862 return $this->getMainSettings()->getQuestionBehaviourSettings()->getQuestionTitleOutputMode();
863 }
864
865 public function isPreviousSolutionReuseEnabled($active_id): bool
866 {
867 $result = $this->db->queryF(
868 "SELECT tst_tests.use_previous_answers FROM tst_tests, tst_active WHERE tst_tests.test_id = tst_active.test_fi AND tst_active.active_id = %s",
869 ["integer"],
870 [$active_id]
871 );
872 if ($result->numRows()) {
873 $row = $this->db->fetchAssoc($result);
874 $test_allows_reuse = $row["use_previous_answers"];
875 }
876
877 if ($test_allows_reuse === '1') {
878 $res = $this->user->getPref("tst_use_previous_answers");
879 if ($res === '1') {
880 return true;
881 }
882 }
883 return false;
884 }
885
886 public function getProcessingTime(): ?string
887 {
888 return $this->getMainSettings()->getTestBehaviourSettings()->getProcessingTime();
889 }
890
891 private function getProcessingTimeForXML(): string
892 {
893 $processing_time = $this->getMainSettings()->getTestBehaviourSettings()->getProcessingTime();
894 if ($processing_time === null
895 || $processing_time === ''
896 || !preg_match('/(\d{2}):(\d{2}):(\d{2})/is', $processing_time, $matches)
897 ) {
898 return '';
899 }
900
901 return sprintf(
902 "P0Y0M0DT%dH%dM%dS",
903 $matches[1],
904 $matches[2],
905 $matches[3]
906 );
907 }
908
909 public function getProcessingTimeInSeconds(int $active_id = 0): int
910 {
911 $processing_time = $this->getMainSettings()->getTestBehaviourSettings()->getProcessingTime() ?? '';
912 if (preg_match("/(\d{2}):(\d{2}):(\d{2})/", (string) $processing_time, $matches)) {
913 $extratime = $this->getExtraTime($active_id) * 60;
914 return ($matches[1] * 3600) + ($matches[2] * 60) + $matches[3] + $extratime;
915 } else {
916 return 0;
917 }
918 }
919
920 public function getEnableProcessingTime(): bool
921 {
922 return $this->getMainSettings()->getTestBehaviourSettings()->getProcessingTimeEnabled();
923 }
924
925 public function getResetProcessingTime(): bool
926 {
927 return $this->getMainSettings()->getTestBehaviourSettings()->getResetProcessingTime();
928 }
929
930 public function isStartingTimeEnabled(): bool
931 {
932 return $this->getMainSettings()->getAccessSettings()->getStartTimeEnabled();
933 }
934
935 public function getStartingTime(): int
936 {
937 $start_time = $this->getMainSettings()->getAccessSettings()->getStartTime();
938 return $start_time !== null ? $start_time->getTimestamp() : 0;
939 }
940
941 public function isEndingTimeEnabled(): bool
942 {
943 return $this->getMainSettings()->getAccessSettings()->getEndTimeEnabled();
944 }
945
946 public function getEndingTime(): int
947 {
948 $end_time = $this->getMainSettings()->getAccessSettings()->getEndTime();
949 return $end_time !== null ? $end_time->getTimestamp() : 0;
950 }
951
952 public function isPasswordEnabled(): bool
953 {
954 return $this->getMainSettings()->getAccessSettings()->getPasswordEnabled();
955 }
956
957 public function getPassword(): ?string
958 {
959 return $this->getMainSettings()->getAccessSettings()->getPassword();
960 }
961
962 public function removeQuestionsWithResults(array $question_ids): void
963 {
964 $scoring = new TestScoring(
965 $this,
966 $this->user,
967 $this->db,
968 $this->test_result_repository
969 );
970
971 array_walk(
972 $question_ids,
973 fn(int $v, int $k) => $this->removeQuestionWithResults($v, $scoring)
974 );
975 }
976
977 private function removeQuestionWithResults(int $question_id, TestScoring $scoring): void
978 {
979 $question = \assQuestion::instantiateQuestion($question_id);
980
981 $participant_data = new ilTestParticipantData($this->db, $this->lng);
982 $participant_data->load($this->test_id);
983
984 $question->removeAllExistingSolutions();
985 $scoring->removeAllQuestionResults($question_id);
986
987 $this->removeQuestion($question_id);
988 if (!$this->isRandomTest()) {
990 $question_id,
991 $participant_data->getActiveIds(),
992 $this->reindexFixedQuestionOrdering()
993 );
994 }
995
996 $scoring->updatePassAndTestResults($participant_data->getActiveIds());
997 ilLPStatusWrapper::_refreshStatus($this->getId(), $participant_data->getUserIds());
998 $question->delete($question_id);
999
1000 if ($this->getTestQuestions() === []) {
1002 $object_properties->storePropertyIsOnline(
1003 $object_properties->getPropertyIsOnline()->withOffline()
1004 );
1005 }
1006
1007 if ($this->logger->isLoggingEnabled()) {
1008 $this->logger->logTestAdministrationInteraction(
1009 $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
1010 $this->getRefId(),
1011 $this->user->getId(),
1012 TestAdministrationInteractionTypes::QUESTION_REMOVED_IN_CORRECTIONS,
1013 [
1014 AdditionalInformationGenerator::KEY_QUESTION_TITLE => $question->getTitleForHTMLOutput(),
1015 AdditionalInformationGenerator::KEY_QUESTION_TEXT => $question->getQuestion(),
1016 AdditionalInformationGenerator::KEY_QUESTION_ID => $question->getId(),
1017 AdditionalInformationGenerator::KEY_QUESTION_TYPE => $question->getQuestionType()
1018 ]
1019 )
1020 );
1021 }
1022 }
1023
1028 int $question_id,
1029 array $active_ids,
1030 ilTestReindexedSequencePositionMap $reindexed_sequence_position_map
1031 ): void {
1032 $test_sequence_factory = new ilTestSequenceFactory(
1033 $this,
1034 $this->db,
1035 $this->questionrepository
1036 );
1037
1038 foreach ($active_ids as $active_id) {
1039 $passSelector = new ilTestPassesSelector($this->db, $this);
1040 $passSelector->setActiveId($active_id);
1041
1042 foreach ($passSelector->getExistingPasses() as $pass) {
1043 $test_sequence = $test_sequence_factory->getSequenceByActiveIdAndPass($active_id, $pass);
1044 $test_sequence->loadFromDb();
1045
1046 $test_sequence->removeQuestion($question_id, $reindexed_sequence_position_map);
1047 $test_sequence->saveToDb();
1048 }
1049 }
1050 }
1051
1055 public function removeQuestions(array $question_ids): void
1056 {
1057 foreach ($question_ids as $question_id) {
1058 $this->removeQuestion((int) $question_id);
1059 }
1060
1061 $this->reindexFixedQuestionOrdering();
1062 }
1063
1064 public function removeQuestion(int $question_id): void
1065 {
1066 try {
1067 $question = self::_instanciateQuestion($question_id);
1068 $question_title = $question->getTitleForHTMLOutput();
1069 $question->delete($question_id);
1070 if ($this->logger->isLoggingEnabled()) {
1071 $this->logger->logTestAdministrationInteraction(
1072 $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
1073 $this->getRefId(),
1074 $this->user->getId(),
1075 TestAdministrationInteractionTypes::QUESTION_REMOVED,
1076 [
1077 AdditionalInformationGenerator::KEY_QUESTION_TITLE => $question_title
1078 ]
1079 )
1080 );
1081 }
1082 } catch (InvalidArgumentException $e) {
1083 $this->logger->error($e->getMessage());
1084 $this->logger->error($e->getTraceAsString());
1085 }
1086 }
1087
1096 public function removeTestResultsFromSoapLpAdministration(array $user_ids)
1097 {
1098 $this->removeTestResultsByUserIds($user_ids);
1099
1100 $participantData = new ilTestParticipantData($this->db, $this->lng);
1101 $participantData->setUserIdsFilter($user_ids);
1102 $participantData->load($this->getTestId());
1103
1104 $this->removeTestActives($participantData->getActiveIds());
1105
1106
1107 if ($this->logger->isLoggingEnabled()) {
1108 $this->logger->logTestAdministrationInteraction(
1109 $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
1110 $this->getRefId(),
1111 $this->user->getId(),
1112 TestAdministrationInteractionTypes::PARTICIPANT_DATA_REMOVED,
1113 [
1114 AdditionalInformationGenerator::KEY_USERS => $participantData->getUserIds()
1115 ]
1116 )
1117 );
1118 }
1119 }
1120
1121 public function removeTestResults(ilTestParticipantData $participant_data): void
1122 {
1123 if ($participant_data->getAnonymousActiveIds() !== []) {
1124 $this->removeTestResultsByActiveIds($participant_data->getAnonymousActiveIds());
1125
1126 $user_ids = array_map(
1127 static fn($active_id) => $participant_data->getUserIdByActiveId($active_id),
1128 $participant_data->getAnonymousActiveIds(),
1129 );
1130 $this->participant_repository->removeExtraTimeByUserId($this->getTestId(), $user_ids);
1131 }
1132
1133 if ($participant_data->getUserIds() !== []) {
1134 /* @var ilTestLP $testLP */
1135 $test_lp = ilObjectLP::getInstance($this->getId());
1136 if ($test_lp instanceof ilTestLP) {
1137 $test_lp->setTestObject($this);
1138 $test_lp->resetLPDataForUserIds($participant_data->getUserIds(), false);
1139 }
1140
1141 $this->participant_repository->removeExtraTimeByUserId($this->getTestId(), $participant_data->getUserIds());
1142 }
1143
1144 if ($participant_data->getActiveIds() !== []) {
1145 $this->removeTestActives($participant_data->getActiveIds());
1146
1147 $user_ids = array_map(
1148 static fn($active_id) => $participant_data->getUserIdByActiveId($active_id),
1149 $participant_data->getActiveIds(),
1150 );
1151 $this->participant_repository->removeExtraTimeByUserId($this->getTestId(), $user_ids);
1152 }
1153
1154 if ($this->logger->isLoggingEnabled()) {
1155 $this->logger->logTestAdministrationInteraction(
1156 $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
1157 $this->getRefId(),
1158 $this->user->getId(),
1159 TestAdministrationInteractionTypes::PARTICIPANT_DATA_REMOVED,
1160 [
1161 AdditionalInformationGenerator::KEY_USERS => $participant_data->getUserIds(),
1162 AdditionalInformationGenerator::KEY_ANON_IDS => $participant_data->getAnonymousActiveIds()
1163 ]
1164 )
1165 );
1166 }
1167 }
1168
1169 public function removeTestResultsByUserIds(array $user_ids): void
1170 {
1171 $participantData = new ilTestParticipantData($this->db, $this->lng);
1172 $participantData->setUserIdsFilter($user_ids);
1173 $participantData->load($this->getTestId());
1174
1175 $in_user_ids = $this->db->in('usr_id', $participantData->getUserIds(), false, 'integer');
1176 $this->db->manipulateF(
1177 "DELETE FROM usr_pref WHERE {$in_user_ids} AND keyword = %s",
1178 ['text'],
1179 ['tst_password_' . $this->getTestId()]
1180 );
1181
1182 if ($participantData->getActiveIds() !== []) {
1183 $this->removeTestResultsByActiveIds($participantData->getActiveIds());
1184 }
1185 }
1186
1187 private function removeTestResultsByActiveIds(array $active_ids): void
1188 {
1189 $in_active_ids = $this->db->in('active_fi', $active_ids, false, 'integer');
1190
1191 $this->db->manipulate("DELETE FROM tst_solutions WHERE {$in_active_ids}");
1192 $this->db->manipulate("DELETE FROM tst_qst_solved WHERE {$in_active_ids}");
1193 $this->db->manipulate("DELETE FROM tst_test_result WHERE {$in_active_ids}");
1194 $this->db->manipulate("DELETE FROM tst_pass_result WHERE {$in_active_ids}");
1195 $this->db->manipulate("DELETE FROM tst_result_cache WHERE {$in_active_ids}");
1196 $this->db->manipulate("DELETE FROM tst_sequence WHERE {$in_active_ids}");
1197 $this->db->manipulate("DELETE FROM tst_times WHERE {$in_active_ids}");
1198 $this->db->manipulate(
1200 . ' WHERE ' . $this->db->in('active_id', $active_ids, false, 'integer')
1201 );
1202
1203 if ($this->isRandomTest()) {
1204 $this->db->manipulate("DELETE FROM tst_test_rnd_qst WHERE {$in_active_ids}");
1205 }
1206
1207 $this->test_result_repository->removeTestResults($active_ids, $this->getId());
1208
1209 foreach ($active_ids as $active_id) {
1210 // remove file uploads
1211 if (is_dir(CLIENT_WEB_DIR . "/assessment/tst_" . $this->getTestId() . "/$active_id")) {
1212 ilFileUtils::delDir(CLIENT_WEB_DIR . "/assessment/tst_" . $this->getTestId() . "/$active_id");
1213 }
1214 }
1215 }
1216
1220 public function removeTestActives(array $active_ids): void
1221 {
1222 $IN_activeIds = $this->db->in('active_id', $active_ids, false, 'integer');
1223 $this->db->manipulate("DELETE FROM tst_active WHERE $IN_activeIds");
1224 }
1225
1233 public function questionMoveUp($question_id)
1234 {
1235 // Move a question up in sequence
1236 $result = $this->db->queryF(
1237 "SELECT * FROM tst_test_question WHERE test_fi=%s AND question_fi=%s",
1238 ['integer', 'integer'],
1239 [$this->getTestId(), $question_id]
1240 );
1241 $data = $this->db->fetchObject($result);
1242 if ($data->sequence > 1) {
1243 // OK, it's not the top question, so move it up
1244 $result = $this->db->queryF(
1245 "SELECT * FROM tst_test_question WHERE test_fi=%s AND sequence=%s",
1246 ['integer','integer'],
1247 [$this->getTestId(), $data->sequence - 1]
1248 );
1249 $data_previous = $this->db->fetchObject($result);
1250 // change previous dataset
1251 $this->db->manipulateF(
1252 "UPDATE tst_test_question SET sequence=%s WHERE test_question_id=%s",
1253 ['integer','integer'],
1254 [$data->sequence, $data_previous->test_question_id]
1255 );
1256 // move actual dataset up
1257 $this->db->manipulateF(
1258 "UPDATE tst_test_question SET sequence=%s WHERE test_question_id=%s",
1259 ['integer','integer'],
1260 [$data->sequence - 1, $data->test_question_id]
1261 );
1262 }
1263 $this->loadQuestions();
1264 }
1265
1273 public function questionMoveDown($question_id)
1274 {
1275 $current_question_result = $this->db->queryF(
1276 "SELECT * FROM tst_test_question WHERE test_fi=%s AND question_fi=%s",
1277 ['integer','integer'],
1278 [$this->getTestId(), $question_id]
1279 );
1280 $current_question_data = $this->db->fetchObject($current_question_result);
1281 $next_question_result = $this->db->queryF(
1282 "SELECT * FROM tst_test_question WHERE test_fi=%s AND sequence=%s",
1283 ['integer','integer'],
1284 [$this->getTestId(), $current_question_data->sequence + 1]
1285 );
1286 if ($this->db->numRows($next_question_result) === 1) {
1287 // OK, it's not the last question, so move it down
1288 $next_question_data = $this->db->fetchObject($next_question_result);
1289 // change next dataset
1290 $this->db->manipulateF(
1291 "UPDATE tst_test_question SET sequence=%s WHERE test_question_id=%s",
1292 ['integer','integer'],
1293 [$current_question_data->sequence, $next_question_data->test_question_id]
1294 );
1295 // move actual dataset down
1296 $this->db->manipulateF(
1297 "UPDATE tst_test_question SET sequence=%s WHERE test_question_id=%s",
1298 ['integer','integer'],
1299 [$current_question_data->sequence + 1, $current_question_data->test_question_id]
1300 );
1301 }
1302 $this->loadQuestions();
1303 }
1304
1311 public function duplicateQuestionForTest($question_id): int
1312 {
1313 $question = ilObjTest::_instanciateQuestion($question_id);
1314 $duplicate_id = $question->duplicate(true, '', '', -1, $this->getId());
1315 return $duplicate_id;
1316 }
1317
1318 public function insertQuestion(int $question_id, bool $link_only = false): int
1319 {
1320 if ($link_only) {
1321 $duplicate_id = $question_id;
1322 } else {
1323 $duplicate_id = $this->duplicateQuestionForTest($question_id);
1324 }
1325
1326 // get maximum sequence index in test
1327 $result = $this->db->queryF(
1328 "SELECT MAX(sequence) seq FROM tst_test_question WHERE test_fi=%s",
1329 ['integer'],
1330 [$this->getTestId()]
1331 );
1332 $sequence = 1;
1333
1334 if ($result->numRows() == 1) {
1335 $data = $this->db->fetchObject($result);
1336 $sequence = $data->seq + 1;
1337 }
1338
1339 $next_id = $this->db->nextId('tst_test_question');
1340 $this->db->manipulateF(
1341 "INSERT INTO tst_test_question (test_question_id, test_fi, question_fi, sequence, tstamp) VALUES (%s, %s, %s, %s, %s)",
1342 ['integer', 'integer','integer','integer','integer'],
1343 [$next_id, $this->getTestId(), $duplicate_id, $sequence, time()]
1344 );
1345 // remove test_active entries, because test has changed
1346 $this->db->manipulateF(
1347 "DELETE FROM tst_active WHERE test_fi = %s",
1348 ['integer'],
1349 [$this->getTestId()]
1350 );
1351 $this->loadQuestions();
1352 $this->saveCompleteStatus($this->question_set_config_factory->getQuestionSetConfig());
1353
1354 if ($this->logger->isLoggingEnabled()) {
1355 $this->logger->logTestAdministrationInteraction(
1356 $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
1357 $this->getRefId(),
1358 $this->user->getId(),
1359 TestAdministrationInteractionTypes::QUESTION_ADDED,
1360 [
1361 AdditionalInformationGenerator::KEY_QUESTION_ID => $question_id
1362 ] + (assQuestion::instantiateQuestion($question_id))
1363 ->toLog($this->logger->getAdditionalInformationGenerator())
1364 )
1365 );
1366 }
1367
1368 return $duplicate_id;
1369 }
1370
1371 private function getQuestionTitles(): array
1372 {
1373 $titles = [];
1374 if ($this->getQuestionSetType() === self::QUESTION_SET_TYPE_FIXED) {
1375 $result = $this->db->queryF(
1376 'SELECT qpl_questions.title FROM tst_test_question, qpl_questions '
1377 . 'WHERE tst_test_question.test_fi = %s AND tst_test_question.question_fi = qpl_questions.question_id '
1378 . 'ORDER BY tst_test_question.sequence',
1379 ['integer'],
1380 [$this->getTestId()]
1381 );
1382 while ($row = $this->db->fetchAssoc($result)) {
1383 array_push($titles, $row['title']);
1384 }
1385 }
1386 return $titles;
1387 }
1388
1396 public function getQuestionTitlesAndIndexes(): array
1397 {
1398 $titles = [];
1399 if ($this->getQuestionSetType() == self::QUESTION_SET_TYPE_FIXED) {
1400 $result = $this->db->queryF(
1401 'SELECT qpl_questions.title, qpl_questions.question_id '
1402 . 'FROM tst_test_question, qpl_questions '
1403 . 'WHERE tst_test_question.test_fi = %s AND tst_test_question.question_fi = qpl_questions.question_id '
1404 . 'ORDER BY tst_test_question.sequence',
1405 ['integer'],
1406 [$this->getTestId()]
1407 );
1408 while ($row = $this->db->fetchAssoc($result)) {
1409 $titles[$row['question_id']] = $row["title"];
1410 }
1411 }
1412 return $titles;
1413 }
1414
1415 // fau: testNav - add number parameter (to show if title should not be shown)
1425 public function getQuestionTitle($title, $nr = null, $points = null): string
1426 {
1427 switch ($this->getTitleOutput()) {
1428 case '0':
1429 case '1':
1430 return $title;
1431 break;
1432 case '2':
1433 if (isset($nr)) {
1434 return $this->lng->txt("ass_question") . ' ' . $nr;
1435 }
1436 return $this->lng->txt("ass_question");
1437 break;
1438 case 3:
1439 if (isset($nr)) {
1440 $txt = $this->lng->txt("ass_question") . ' ' . $nr;
1441 } else {
1442 $txt = $this->lng->txt("ass_question");
1443 }
1444 if ($points != '') {
1445 $lngv = $this->lng->txt('points');
1446 if ($points == 1) {
1447 $lngv = $this->lng->txt('point');
1448 }
1449 $txt .= ' - ' . $points . ' ' . $lngv;
1450 }
1451 return $txt;
1452 break;
1453
1454 }
1455 return $this->lng->txt("ass_question");
1456 }
1457 // fau.
1458
1467 public function getQuestionDataset($question_id): object
1468 {
1469 $result = $this->db->queryF(
1470 "SELECT qpl_questions.*, qpl_qst_type.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",
1471 ['integer'],
1472 [$question_id]
1473 );
1474 $row = $this->db->fetchObject($result);
1475 return $row;
1476 }
1477
1484 public function &getExistingQuestions($pass = null): array
1485 {
1486 $existing_questions = [];
1487 $active_id = $this->getActiveIdOfUser($this->user->getId());
1488 if ($this->isRandomTest()) {
1489 if (is_null($pass)) {
1490 $pass = 0;
1491 }
1492 $result = $this->db->queryF(
1493 "SELECT qpl_questions.original_id FROM qpl_questions, tst_test_rnd_qst WHERE tst_test_rnd_qst.active_fi = %s AND tst_test_rnd_qst.question_fi = qpl_questions.question_id AND tst_test_rnd_qst.pass = %s",
1494 ['integer','integer'],
1495 [$active_id, $pass]
1496 );
1497 } else {
1498 $result = $this->db->queryF(
1499 "SELECT qpl_questions.original_id FROM qpl_questions, tst_test_question WHERE tst_test_question.test_fi = %s AND tst_test_question.question_fi = qpl_questions.question_id",
1500 ['integer'],
1501 [$this->getTestId()]
1502 );
1503 }
1504 while ($data = $this->db->fetchObject($result)) {
1505 if ($data->original_id === null) {
1506 continue;
1507 }
1508
1509 array_push($existing_questions, $data->original_id);
1510 }
1511 return $existing_questions;
1512 }
1513
1521 public function getQuestionType($question_id)
1522 {
1523 if ($question_id < 1) {
1524 return -1;
1525 }
1526 $result = $this->db->queryF(
1527 "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",
1528 ['integer'],
1529 [$question_id]
1530 );
1531 if ($result->numRows() == 1) {
1532 $data = $this->db->fetchObject($result);
1533 return $data->type_tag;
1534 } else {
1535 return "";
1536 }
1537 }
1538
1545 public function startWorkingTime($active_id, $pass)
1546 {
1547 $next_id = $this->db->nextId('tst_times');
1548 $affectedRows = $this->db->manipulateF(
1549 "INSERT INTO tst_times (times_id, active_fi, started, finished, pass, tstamp) VALUES (%s, %s, %s, %s, %s, %s)",
1550 ['integer', 'integer', 'timestamp', 'timestamp', 'integer', 'integer'],
1551 [$next_id, $active_id, date("Y-m-d H:i:s"), date("Y-m-d H:i:s"), $pass, time()]
1552 );
1553 return $next_id;
1554 }
1555
1562 public function updateWorkingTime($times_id)
1563 {
1564 $affectedRows = $this->db->manipulateF(
1565 "UPDATE tst_times SET finished = %s, tstamp = %s WHERE times_id = %s",
1566 ['timestamp', 'integer', 'integer'],
1567 [date('Y-m-d H:i:s'), time(), $times_id]
1568 );
1569 }
1570
1577 public function &getWorkedQuestions($active_id, $pass = null): array
1578 {
1579 if (is_null($pass)) {
1580 $result = $this->db->queryF(
1581 "SELECT question_fi FROM tst_solutions WHERE active_fi = %s AND pass = %s GROUP BY question_fi",
1582 ['integer','integer'],
1583 [$active_id, 0]
1584 );
1585 } else {
1586 $result = $this->db->queryF(
1587 "SELECT question_fi FROM tst_solutions WHERE active_fi = %s AND pass = %s GROUP BY question_fi",
1588 ['integer','integer'],
1589 [$active_id, $pass]
1590 );
1591 }
1592 $result_array = [];
1593 while ($row = $this->db->fetchAssoc($result)) {
1594 array_push($result_array, $row["question_fi"]);
1595 }
1596 return $result_array;
1597 }
1598
1607 public function isTestFinishedToViewResults($active_id, $currentpass): bool
1608 {
1609 $num = ilObjTest::lookupPassResultsUpdateTimestamp($active_id, $currentpass);
1610 return ((($currentpass > 0) && ($num == 0)) || $this->isTestFinished($active_id)) ? true : false;
1611 }
1612
1619 public function getAllQuestions($pass = null): array
1620 {
1621 if ($this->isRandomTest()) {
1622 $active_id = $this->getActiveIdOfUser($this->user->getId());
1623 if ($active_id === null) {
1624 return [];
1625 }
1626 $this->loadQuestions($active_id, $pass);
1627 if (count($this->questions) === 0) {
1628 return [];
1629 }
1630 if (is_null($pass)) {
1631 $pass = self::_getPass($active_id);
1632 }
1633 $result = $this->db->queryF(
1634 "SELECT qpl_questions.* FROM qpl_questions, tst_test_rnd_qst WHERE tst_test_rnd_qst.question_fi = qpl_questions.question_id AND tst_test_rnd_qst.active_fi = %s AND tst_test_rnd_qst.pass = %s AND " . $this->db->in('qpl_questions.question_id', $this->questions, false, 'integer'),
1635 ['integer','integer'],
1636 [$active_id, $pass]
1637 );
1638 } else {
1639 if (count($this->questions) === 0) {
1640 return [];
1641 }
1642 $result = $this->db->query("SELECT qpl_questions.* FROM qpl_questions, tst_test_question WHERE tst_test_question.question_fi = qpl_questions.question_id AND " . $this->db->in('qpl_questions.question_id', $this->questions, false, 'integer'));
1643 }
1644 $result_array = [];
1645 while ($row = $this->db->fetchAssoc($result)) {
1646 $result_array[$row["question_id"]] = $row;
1647 }
1648 return $result_array;
1649 }
1650
1659 public function getActiveIdOfUser($user_id = "", $anonymous_id = ""): ?int
1660 {
1661 if (!$user_id) {
1662 $user_id = $this->user->getId();
1663 }
1664
1665 $tst_access_code = ilSession::get('tst_access_code');
1666 if (is_array($tst_access_code) &&
1667 $this->user->getId() === ANONYMOUS_USER_ID &&
1668 isset($tst_access_code[$this->getTestId()]) &&
1669 $tst_access_code[$this->getTestId()] !== '') {
1670 $result = $this->db->queryF(
1671 'SELECT active_id FROM tst_active WHERE user_fi = %s AND test_fi = %s AND anonymous_id = %s',
1672 ['integer', 'integer', 'text'],
1673 [$user_id, $this->test_id, $tst_access_code[$this->getTestId()]]
1674 );
1675 } elseif ((string) $anonymous_id !== '') {
1676 $result = $this->db->queryF(
1677 'SELECT active_id FROM tst_active WHERE user_fi = %s AND test_fi = %s AND anonymous_id = %s',
1678 ['integer', 'integer', 'text'],
1679 [$user_id, $this->test_id, $anonymous_id]
1680 );
1681 } else {
1682 if ((int) $user_id === ANONYMOUS_USER_ID) {
1683 return null;
1684 }
1685 $result = $this->db->queryF(
1686 'SELECT active_id FROM tst_active WHERE user_fi = %s AND test_fi = %s',
1687 ['integer', 'integer'],
1688 [$user_id, $this->test_id]
1689 );
1690 }
1691
1692 if ($result->numRows()) {
1693 $row = $this->db->fetchAssoc($result);
1694 return (int) $row['active_id'];
1695 }
1696
1697 return null;
1698 }
1699
1700 public static function _getActiveIdOfUser($user_id = "", $test_id = "")
1701 {
1702 global $DIC;
1703 $ilDB = $DIC['ilDB'];
1704 $ilUser = $DIC['ilUser'];
1705
1706 if (!$user_id) {
1707 $user_id = $ilUser->id;
1708 }
1709 if (!$test_id) {
1710 return "";
1711 }
1712 $result = $ilDB->queryF(
1713 "SELECT tst_active.active_id FROM tst_active WHERE user_fi = %s AND test_fi = %s",
1714 ['integer', 'integer'],
1715 [$user_id, $test_id]
1716 );
1717 if ($result->numRows()) {
1718 $row = $ilDB->fetchAssoc($result);
1719 return $row["active_id"];
1720 } else {
1721 return "";
1722 }
1723 }
1724
1731 public function pcArrayShuffle($array): array
1732 {
1733 $keys = array_keys($array);
1734 shuffle($keys);
1735 $result = [];
1736 foreach ($keys as $key) {
1737 $result[$key] = $array[$key];
1738 }
1739 return $result;
1740 }
1741
1748 public function getTestResult(
1749 int $active_id,
1750 ?int $attempt = null,
1751 bool $ordered_sequence = false,
1752 bool $consider_hidden_questions = true,
1753 bool $consider_optional_questions = true
1754 ): array {
1755 $test_result = $this->test_result_repository->getTestResult($active_id);
1756
1757 if ($test_result === null) {
1758 $test_result = $this->test_result_repository->updateTestResultCache($active_id);
1759 }
1760
1761 if ($attempt === null) {
1762 $attempt = $test_result->getAttempt();
1763 }
1764
1765 $test_sequence_factory = new ilTestSequenceFactory($this, $this->db, $this->questionrepository);
1766 $test_sequence = $test_sequence_factory->getSequenceByActiveIdAndPass($active_id, $attempt);
1767
1768 $test_sequence->setConsiderHiddenQuestionsEnabled($consider_hidden_questions);
1769 $test_sequence->setConsiderOptionalQuestionsEnabled($consider_optional_questions);
1770
1771 $test_sequence->loadFromDb();
1772 $test_sequence->loadQuestions();
1773
1774 if ($ordered_sequence) {
1775 $sequence = $test_sequence->getOrderedSequenceQuestions();
1776 } else {
1777 $sequence = $test_sequence->getUserSequenceQuestions();
1778 }
1779
1780 $arr_results = [];
1781
1782 $query = "
1783 SELECT
1784 tst_test_result.question_fi,
1785 tst_test_result.points reached,
1786 tst_test_result.answered answered,
1787 tst_manual_fb.finalized_evaluation finalized_evaluation
1788
1789 FROM tst_test_result
1790
1791 LEFT JOIN tst_solutions
1792 ON tst_solutions.active_fi = tst_test_result.active_fi
1793 AND tst_solutions.question_fi = tst_test_result.question_fi
1794
1795 LEFT JOIN tst_manual_fb
1796 ON tst_test_result.active_fi = tst_manual_fb.active_fi
1797 AND tst_test_result.question_fi = tst_manual_fb.question_fi
1798
1799 WHERE tst_test_result.active_fi = %s
1800 AND tst_test_result.pass = %s
1801 ";
1802
1803 $solutionresult = $this->db->queryF(
1804 $query,
1805 ['integer', 'integer'],
1806 [$active_id, $attempt]
1807 );
1808
1809 while ($row = $this->db->fetchAssoc($solutionresult)) {
1810 $arr_results[ $row['question_fi'] ] = $row;
1811 }
1812
1813 $result = $this->db->query(
1814 'SELECT qpl_questions.*, qpl_qst_type.type_tag, qpl_sol_sug.question_fi has_sug_sol' . PHP_EOL
1815 . 'FROM qpl_qst_type, qpl_questions' . PHP_EOL
1816 . 'LEFT JOIN qpl_sol_sug' . PHP_EOL
1817 . 'ON qpl_sol_sug.question_fi = qpl_questions.question_id' . PHP_EOL
1818 . 'WHERE qpl_qst_type.question_type_id = qpl_questions.question_type_fi' . PHP_EOL
1819 . 'AND ' . $this->db->in('qpl_questions.question_id', $sequence, false, 'integer')
1820 );
1821
1822 $unordered = [];
1823 $key = 1;
1824 while ($row = $this->db->fetchAssoc($result)) {
1825 if (!isset($arr_results[ $row['question_id'] ])) {
1826 $percentvalue = 0.0;
1827 } else {
1828 $percentvalue = (
1829 $row['points'] ? $arr_results[$row['question_id']]['reached'] / $row['points'] : 0
1830 );
1831 }
1832 if ($percentvalue < 0) {
1833 $percentvalue = 0.0;
1834 }
1835
1836 $data = [
1837 'nr' => $key,
1838 'title' => ilLegacyFormElementsUtil::prepareFormOutput($row['title']),
1839 'max' => round($row['points'], 2),
1840 'reached' => round($arr_results[$row['question_id']]['reached'] ?? 0, 2),
1841 'percent' => sprintf('%2.2f ', ($percentvalue) * 100) . '%',
1842 'solution' => ($row['has_sug_sol']) ? assQuestion::_getSuggestedSolutionOutput($row['question_id']) : '',
1843 'type' => $row['type_tag'],
1844 'qid' => $row['question_id'],
1845 'original_id' => $row['original_id'],
1846 'workedthrough' => isset($arr_results[$row['question_id']]) ? 1 : 0,
1847 'answered' => $arr_results[$row['question_id']]['answered'] ?? 0,
1848 'finalized_evaluation' => $arr_results[$row['question_id']]['finalized_evaluation'] ?? 0,
1849 ];
1850
1851 $unordered[ $row['question_id'] ] = $data;
1852 $key++;
1853 }
1854
1855 $pass_max = 0;
1856 $pass_reached = 0;
1857
1858 $found = [];
1859 foreach ($sequence as $qid) {
1860 // building pass point sums based on prepared data
1861 // for question that exists in users qst sequence
1862 $pass_max += round($unordered[$qid]['max'], 2);
1863 $pass_reached += round($unordered[$qid]['reached'], 2);
1864 $found[] = $unordered[$qid];
1865 }
1866
1867 if ($this->getScoreCutting() == 1) {
1868 if ($pass_reached < 0) {
1869 $pass_reached = 0;
1870 }
1871 }
1872
1873 $found['pass']['total_max_points'] = $pass_max;
1874 $found['pass']['total_reached_points'] = $pass_reached;
1875 $found['pass']['percent'] = ($pass_max > 0) ? $pass_reached / $pass_max : 0;
1876 $found['pass']['num_workedthrough'] = count($arr_results);
1877 $found['pass']['num_questions_total'] = count($unordered);
1878
1879 $found['test']['total_max_points'] = $test_result->getMaxPoints();
1880 $found['test']['total_reached_points'] = $test_result->getReachedPoints();
1881 $found['test']['result_pass'] = $attempt;
1882 $found['test']['result_tstamp'] = $test_result->getTimestamp();
1883 $found['test']['passed'] = $test_result->isPassed();
1884
1885 return $found;
1886 }
1887
1894 public function evalTotalPersons(): int
1895 {
1896 $result = $this->db->queryF(
1897 'SELECT COUNT(active_id) total FROM tst_active WHERE test_fi = %s',
1898 ['integer'],
1899 [$this->getTestId()]
1900 );
1901 $row = $this->db->fetchAssoc($result);
1902 return $row['total'];
1903 }
1904
1912 {
1913 $result = $this->db->queryF(
1914 "SELECT tst_times.* FROM tst_active, tst_times WHERE tst_active.test_fi = %s AND tst_active.active_id = tst_times.active_fi AND tst_active.user_fi = %s",
1915 ['integer','integer'],
1916 [$this->getTestId(), $user_id]
1917 );
1918 $time = 0;
1919 while ($row = $this->db->fetchAssoc($result)) {
1920 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["started"], $matches);
1921 $epoch_1 = mktime(
1922 (int) $matches[4],
1923 (int) $matches[5],
1924 (int) $matches[6],
1925 (int) $matches[2],
1926 (int) $matches[3],
1927 (int) $matches[1]
1928 );
1929 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["finished"], $matches);
1930 $epoch_2 = mktime(
1931 (int) $matches[4],
1932 (int) $matches[5],
1933 (int) $matches[6],
1934 (int) $matches[2],
1935 (int) $matches[3],
1936 (int) $matches[1]
1937 );
1938 $time += ($epoch_2 - $epoch_1);
1939 }
1940 return $time;
1941 }
1942
1950 {
1951 return $this->_getCompleteWorkingTimeOfParticipants($this->getTestId());
1952 }
1953
1961 public function &_getCompleteWorkingTimeOfParticipants($test_id): array
1962 {
1963 $result = $this->db->queryF(
1964 "SELECT tst_times.* FROM tst_active, tst_times WHERE tst_active.test_fi = %s AND tst_active.active_id = tst_times.active_fi ORDER BY tst_times.active_fi, tst_times.started",
1965 ['integer'],
1966 [$test_id]
1967 );
1968 $time = 0;
1969 $times = [];
1970 while ($row = $this->db->fetchAssoc($result)) {
1971 if (!array_key_exists($row["active_fi"], $times)) {
1972 $times[$row["active_fi"]] = 0;
1973 }
1974 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["started"], $matches);
1975 $epoch_1 = mktime(
1976 (int) $matches[4],
1977 (int) $matches[5],
1978 (int) $matches[6],
1979 (int) $matches[2],
1980 (int) $matches[3],
1981 (int) $matches[1]
1982 );
1983 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["finished"], $matches);
1984 $epoch_2 = mktime(
1985 (int) $matches[4],
1986 (int) $matches[5],
1987 (int) $matches[6],
1988 (int) $matches[2],
1989 (int) $matches[3],
1990 (int) $matches[1]
1991 );
1992 $times[$row["active_fi"]] += ($epoch_2 - $epoch_1);
1993 }
1994 return $times;
1995 }
1996
2003 public function getCompleteWorkingTimeOfParticipant($active_id): int
2004 {
2005 $result = $this->db->queryF(
2006 "SELECT tst_times.* FROM tst_active, tst_times WHERE tst_active.test_fi = %s AND tst_active.active_id = tst_times.active_fi AND tst_active.active_id = %s ORDER BY tst_times.active_fi, tst_times.started",
2007 ['integer','integer'],
2008 [$this->getTestId(), $active_id]
2009 );
2010 $time = 0;
2011 while ($row = $this->db->fetchAssoc($result)) {
2012 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["started"], $matches);
2013 $epoch_1 = mktime(
2014 (int) $matches[4],
2015 (int) $matches[5],
2016 (int) $matches[6],
2017 (int) $matches[2],
2018 (int) $matches[3],
2019 (int) $matches[1]
2020 );
2021 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["finished"], $matches);
2022 $epoch_2 = mktime(
2023 (int) $matches[4],
2024 (int) $matches[5],
2025 (int) $matches[6],
2026 (int) $matches[2],
2027 (int) $matches[3],
2028 (int) $matches[1]
2029 );
2030 $time += ($epoch_2 - $epoch_1);
2031 }
2032 return $time;
2033 }
2034
2038 public function getWorkingTimeOfParticipantForPass(int $active_id, int $pass): int
2039 {
2040 return $this->test_result_repository->fetchWorkingTime($active_id, $pass);
2041 }
2042
2046 public function evalStatistical($active_id): array
2047 {
2048 $pass = ilObjTest::_getResultPass($active_id);
2049 $test_result = &$this->getTestResult($active_id, $pass);
2050 $result = $this->db->queryF(
2051 "SELECT tst_times.* FROM tst_active, tst_times WHERE tst_active.active_id = %s AND tst_active.active_id = tst_times.active_fi",
2052 ['integer'],
2053 [$active_id]
2054 );
2055 $times = [];
2056 $first_visit = 0;
2057 $last_visit = 0;
2058 while ($row = $this->db->fetchObject($result)) {
2059 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row->started, $matches);
2060 $epoch_1 = mktime(
2061 (int) $matches[4],
2062 (int) $matches[5],
2063 (int) $matches[6],
2064 (int) $matches[2],
2065 (int) $matches[3],
2066 (int) $matches[1]
2067 );
2068 if (!$first_visit) {
2069 $first_visit = $epoch_1;
2070 }
2071 if ($epoch_1 < $first_visit) {
2072 $first_visit = $epoch_1;
2073 }
2074 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row->finished, $matches);
2075 $epoch_2 = mktime(
2076 (int) $matches[4],
2077 (int) $matches[5],
2078 (int) $matches[6],
2079 (int) $matches[2],
2080 (int) $matches[3],
2081 (int) $matches[1]
2082 );
2083 if (!$last_visit) {
2084 $last_visit = $epoch_2;
2085 }
2086 if ($epoch_2 > $last_visit) {
2087 $last_visit = $epoch_2;
2088 }
2089 $times[$row->active_fi] += ($epoch_2 - $epoch_1);
2090 }
2091 $max_time = 0;
2092 foreach ($times as $key => $value) {
2093 $max_time += $value;
2094 }
2095 if ((!$test_result["test"]["total_reached_points"]) or (!$test_result["test"]["total_max_points"])) {
2096 $percentage = 0.0;
2097 } else {
2098 $percentage = ($test_result["test"]["total_reached_points"] / $test_result["test"]["total_max_points"]) * 100.0;
2099 if ($percentage < 0) {
2100 $percentage = 0.0;
2101 }
2102 }
2103 $mark_obj = $this->getMarkSchema()->getMatchingMark($percentage);
2104 $first_date = getdate($first_visit);
2105 $last_date = getdate($last_visit);
2106 $qworkedthrough = 0;
2107 foreach ($test_result as $key => $value) {
2108 if (preg_match("/\d+/", $key)) {
2109 $qworkedthrough += $value["workedthrough"];
2110 }
2111 }
2112 if (!$qworkedthrough) {
2113 $atimeofwork = 0;
2114 } else {
2115 $atimeofwork = $max_time / $qworkedthrough;
2116 }
2117
2118 $result_mark = "";
2119 $passed = "";
2120
2121 if ($mark_obj !== null) {
2122 $result_mark = $mark_obj->getShortName();
2123
2124 if ($mark_obj->getPassed()) {
2125 $passed = 1;
2126 } else {
2127 $passed = 0;
2128 }
2129 }
2130 $percent_worked_through = 0;
2131 if (count($this->questions)) {
2132 $percent_worked_through = $qworkedthrough / count($this->questions);
2133 }
2134 $result_array = [
2135 "qworkedthrough" => $qworkedthrough,
2136 "qmax" => count($this->questions),
2137 "pworkedthrough" => $percent_worked_through,
2138 "timeofwork" => $max_time,
2139 "atimeofwork" => $atimeofwork,
2140 "firstvisit" => $first_date,
2141 "lastvisit" => $last_date,
2142 "resultspoints" => $test_result["test"]["total_reached_points"],
2143 "maxpoints" => $test_result["test"]["total_max_points"],
2144 "resultsmarks" => $result_mark,
2145 "passed" => $passed,
2146 "distancemedian" => "0"
2147 ];
2148 foreach ($test_result as $key => $value) {
2149 if (preg_match("/\d+/", $key)) {
2150 $result_array[$key] = $value;
2151 }
2152 }
2153 return $result_array;
2154 }
2155
2163 public function getTotalPointsPassedArray(): array
2164 {
2165 $totalpoints_array = [];
2166 $all_users = $this->evalTotalParticipantsArray();
2167 foreach ($all_users as $active_id => $user_name) {
2168 $test_result = &$this->getTestResult($active_id);
2169 $reached = $test_result["test"]["total_reached_points"];
2170 $total = $test_result["test"]["total_max_points"];
2171 $percentage = $total != 0 ? $reached / $total : 0;
2172 $mark = $this->getMarkSchema()->getMatchingMark($percentage * 100.0);
2173
2174 if ($mark !== null && $mark->getPassed()) {
2175 array_push($totalpoints_array, $test_result["test"]["total_reached_points"]);
2176 }
2177 }
2178 return $totalpoints_array;
2179 }
2180
2186 public function getParticipants(): array
2187 {
2188 $result = $this->db->queryF(
2189 "SELECT tst_active.active_id, usr_data.usr_id, usr_data.firstname, usr_data.lastname, usr_data.title, usr_data.login FROM tst_active LEFT JOIN usr_data ON tst_active.user_fi = usr_data.usr_id WHERE tst_active.test_fi = %s ORDER BY usr_data.lastname ASC",
2190 ['integer'],
2191 [$this->getTestId()]
2192 );
2193 $persons_array = [];
2194 while ($row = $this->db->fetchAssoc($result)) {
2195 $name = $this->lng->txt("anonymous");
2196 $fullname = $this->lng->txt("anonymous");
2197 $login = "";
2198 if (!$this->getAnonymity()) {
2199 if (strlen($row["firstname"] . $row["lastname"] . $row["title"]) == 0) {
2200 $name = $this->lng->txt("deleted_user");
2201 $fullname = $this->lng->txt("deleted_user");
2202 $login = $this->lng->txt("unknown");
2203 } else {
2204 $login = $row["login"];
2205 if ($row["usr_id"] == ANONYMOUS_USER_ID) {
2206 $name = $this->lng->txt("anonymous");
2207 $fullname = $this->lng->txt("anonymous");
2208 } else {
2209 $name = trim($row["lastname"] . ", " . $row["firstname"] . " " . $row["title"]);
2210 $fullname = trim($row["title"] . " " . $row["firstname"] . " " . $row["lastname"]);
2211 }
2212 }
2213 }
2214 $persons_array[$row["active_id"]] = [
2215 "name" => $name,
2216 "fullname" => $fullname,
2217 "login" => $login
2218 ];
2219 }
2220 return $persons_array;
2221 }
2222
2223 public function evalTotalPersonsArray(string $name_sort_order = 'asc'): array
2224 {
2225 $result = $this->db->queryF(
2226 "SELECT tst_active.user_fi, tst_active.active_id, usr_data.firstname, usr_data.lastname, usr_data.title FROM tst_active LEFT JOIN usr_data ON tst_active.user_fi = usr_data.usr_id WHERE tst_active.test_fi = %s ORDER BY usr_data.lastname " . strtoupper($name_sort_order),
2227 ['integer'],
2228 [$this->getTestId()]
2229 );
2230 $persons_array = [];
2231 while ($row = $this->db->fetchAssoc($result)) {
2232 if ($this->getAccessFilteredParticipantList() && !$this->getAccessFilteredParticipantList()->isActiveIdInList($row["active_id"])) {
2233 continue;
2234 }
2235
2236 if ($this->getAnonymity()) {
2237 $persons_array[$row["active_id"]] = $this->lng->txt("anonymous");
2238 } else {
2239 if (strlen($row["firstname"] . $row["lastname"] . $row["title"]) == 0) {
2240 $persons_array[$row["active_id"]] = $this->lng->txt("deleted_user");
2241 } else {
2242 if ($row["user_fi"] == ANONYMOUS_USER_ID) {
2243 $persons_array[$row["active_id"]] = $row["lastname"];
2244 } else {
2245 $persons_array[$row["active_id"]] = trim($row["lastname"] . ", " . $row["firstname"] . " " . $row["title"]);
2246 }
2247 }
2248 }
2249 }
2250 return $persons_array;
2251 }
2252
2253 public function evalTotalParticipantsArray(string $name_sort_order = 'asc'): array
2254 {
2255 $result = $this->db->queryF(
2256 'SELECT tst_active.user_fi, tst_active.active_id, usr_data.login, '
2257 . 'usr_data.firstname, usr_data.lastname, usr_data.title FROM tst_active '
2258 . 'LEFT JOIN usr_data ON tst_active.user_fi = usr_data.usr_id '
2259 . 'WHERE tst_active.test_fi = %s '
2260 . 'ORDER BY usr_data.lastname ' . strtoupper($name_sort_order),
2261 ['integer'],
2262 [$this->getTestId()]
2263 );
2264 $persons_array = [];
2265 while ($row = $this->db->fetchAssoc($result)) {
2266 if ($this->getAnonymity()) {
2267 $persons_array[$row['active_id']] = ['name' => $this->lng->txt("anonymous")];
2268 } else {
2269 if (strlen($row['firstname'] . $row['lastname'] . $row["title"]) == 0) {
2270 $persons_array[$row['active_id']] = ['name' => $this->lng->txt('deleted_user')];
2271 } else {
2272 if ($row['user_fi'] == ANONYMOUS_USER_ID) {
2273 $persons_array[$row['active_id']] = ['name' => $row['lastname']];
2274 } else {
2275 $persons_array[$row['active_id']] = [
2276 'name' => trim($row['lastname'] . ', ' . $row['firstname']
2277 . ' ' . $row['title']),
2278 'login' => $row['login']
2279 ];
2280 }
2281 }
2282 }
2283 }
2284 return $persons_array;
2285 }
2286
2287 public function getQuestionsOfTest(int $active_id): array
2288 {
2289 if ($this->isRandomTest()) {
2290 $this->db->setLimit($this->getQuestionCount(), 0);
2291 $result = $this->db->queryF(
2292 'SELECT tst_test_rnd_qst.sequence, tst_test_rnd_qst.question_fi, '
2293 . 'tst_test_rnd_qst.pass, qpl_questions.points '
2294 . 'FROM tst_test_rnd_qst, qpl_questions '
2295 . 'WHERE tst_test_rnd_qst.question_fi = qpl_questions.question_id '
2296 . 'AND tst_test_rnd_qst.active_fi = %s ORDER BY tst_test_rnd_qst.sequence',
2297 ['integer'],
2298 [$active_id]
2299 );
2300 } else {
2301 $result = $this->db->queryF(
2302 'SELECT tst_test_question.sequence, tst_test_question.question_fi, '
2303 . 'qpl_questions.points '
2304 . 'FROM tst_test_question, tst_active, qpl_questions '
2305 . 'WHERE tst_test_question.question_fi = qpl_questions.question_id '
2306 . 'AND tst_active.active_id = %s AND tst_active.test_fi = tst_test_question.test_fi',
2307 ['integer'],
2308 [$active_id]
2309 );
2310 }
2311 $qtest = [];
2312 if ($result->numRows()) {
2313 while ($row = $this->db->fetchAssoc($result)) {
2314 array_push($qtest, $row);
2315 }
2316 }
2317 return $qtest;
2318 }
2319
2320 public function getQuestionsOfPass(int $active_id, int $pass): array
2321 {
2322 if ($this->isRandomTest()) {
2323 $this->db->setLimit($this->getQuestionCount(), 0);
2324 $result = $this->db->queryF(
2325 'SELECT tst_test_rnd_qst.sequence, tst_test_rnd_qst.question_fi, '
2326 . 'qpl_questions.points '
2327 . 'FROM tst_test_rnd_qst, qpl_questions '
2328 . 'WHERE tst_test_rnd_qst.question_fi = qpl_questions.question_id '
2329 . 'AND tst_test_rnd_qst.active_fi = %s AND tst_test_rnd_qst.pass = %s '
2330 . 'ORDER BY tst_test_rnd_qst.sequence',
2331 ['integer', 'integer'],
2332 [$active_id, $pass]
2333 );
2334 } else {
2335 $result = $this->db->queryF(
2336 'SELECT tst_test_question.sequence, tst_test_question.question_fi, '
2337 . 'qpl_questions.points '
2338 . 'FROM tst_test_question, tst_active, qpl_questions '
2339 . 'WHERE tst_test_question.question_fi = qpl_questions.question_id '
2340 . 'AND tst_active.active_id = %s AND tst_active.test_fi = tst_test_question.test_fi',
2341 ['integer'],
2342 [$active_id]
2343 );
2344 }
2345 $qpass = [];
2346 if ($result->numRows()) {
2347 while ($row = $this->db->fetchAssoc($result)) {
2348 array_push($qpass, $row);
2349 }
2350 }
2351 return $qpass;
2352 }
2353
2355 {
2356 return $this->access_filtered_participant_list;
2357 }
2358
2359 public function setAccessFilteredParticipantList(ilTestParticipantList $access_filtered_participant_list): void
2360 {
2361 $this->access_filtered_participant_list = $access_filtered_participant_list;
2362 }
2363
2365 {
2366 $list = new ilTestParticipantList($this, $this->user, $this->lng, $this->db);
2367 $list->initializeFromDbRows($this->getTestParticipants());
2368
2369 return $list->getAccessFilteredList(
2370 $this->participant_access_filter->getAccessStatisticsUserFilter($this->getRefId())
2371 );
2372 }
2373
2377 public function getAnonOnlyParticipantIds(): array
2378 {
2379 $list = new ilTestParticipantList($this, $this->user, $this->lng, $this->db);
2380 $list->initializeFromDbRows($this->getTestParticipants());
2381 if ($this->getAnonymity()) {
2382 return $list->getAllUserIds();
2383 }
2384 return $list->getAccessFilteredList(
2385 $this->participant_access_filter->getAnonOnlyParticipantsUserFilter($this->getRefId())
2386 )->getAllUserIds();
2387 }
2388
2390 {
2391 return (new ilTestEvaluationFactory($this->db, $this))
2392 ->getEvaluationData();
2393 }
2394
2395 public function getQuestionCountAndPointsForPassOfParticipant(int $active_id, int $pass): array
2396 {
2397 $question_set_type = $this->lookupQuestionSetTypeByActiveId($active_id);
2398
2399 switch ($question_set_type) {
2401 $res = $this->db->queryF(
2402 "
2403 SELECT tst_test_rnd_qst.pass,
2404 COUNT(tst_test_rnd_qst.question_fi) qcount,
2405 SUM(qpl_questions.points) qsum
2406
2407 FROM tst_test_rnd_qst,
2408 qpl_questions
2409
2410 WHERE tst_test_rnd_qst.question_fi = qpl_questions.question_id
2411 AND tst_test_rnd_qst.active_fi = %s
2412 AND pass = %s
2413
2414 GROUP BY tst_test_rnd_qst.active_fi,
2415 tst_test_rnd_qst.pass
2416 ",
2417 ['integer', 'integer'],
2418 [$active_id, $pass]
2419 );
2420 break;
2421
2423 $res = $this->db->queryF(
2424 "
2425 SELECT COUNT(tst_test_question.question_fi) qcount,
2426 SUM(qpl_questions.points) qsum
2427
2428 FROM tst_test_question,
2429 qpl_questions,
2430 tst_active
2431
2432 WHERE tst_test_question.question_fi = qpl_questions.question_id
2433 AND tst_test_question.test_fi = tst_active.test_fi
2434 AND tst_active.active_id = %s
2435
2436 GROUP BY tst_test_question.test_fi
2437 ",
2438 ['integer'],
2439 [$active_id]
2440 );
2441 break;
2442
2443 default:
2444 throw new ilTestException("not supported question set type: $question_set_type");
2445 }
2446
2447 $row = $this->db->fetchAssoc($res);
2448
2449 if (is_array($row)) {
2450 return ["count" => $row["qcount"], "points" => $row["qsum"]];
2451 }
2452
2453 return ["count" => 0, "points" => 0];
2454 }
2455
2456 public function getCompleteEvaluationData($filterby = '', $filtertext = ''): ilTestEvaluationData
2457 {
2458 $data = $this->getUnfilteredEvaluationData();
2459 $data->setFilter($filterby, $filtertext);
2460 return $data;
2461 }
2462
2474 public function buildName(
2475 ?int $user_id,
2476 ?string $firstname,
2477 ?string $lastname
2478 ): string {
2479 if ($user_id === null
2480 || $firstname . $lastname === '') {
2481 return $this->lng->txt('deleted_user');
2482 }
2483
2484 if ($this->getAnonymity()) {
2485 return $this->lng->txt('anonymous');
2486 }
2487
2488 if ($user_id == ANONYMOUS_USER_ID) {
2489 return $lastname;
2490 }
2491
2492 return trim($lastname . ', ' . $firstname);
2493 }
2494
2495 public function evalTotalStartedAverageTime(?array $active_ids_to_filter = null): int
2496 {
2497 $query = "SELECT tst_times.* FROM tst_active, tst_times WHERE tst_active.test_fi = %s AND tst_active.active_id = tst_times.active_fi";
2498
2499 if ($active_ids_to_filter !== null && $active_ids_to_filter !== []) {
2500 $query .= " AND " . $this->db->in('active_id', $active_ids_to_filter, false, 'integer');
2501 }
2502
2503 $result = $this->db->queryF($query, ['integer'], [$this->getTestId()]);
2504 $times = [];
2505 while ($row = $this->db->fetchObject($result)) {
2506 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row->started, $matches);
2507 $epoch_1 = mktime(
2508 (int) $matches[4],
2509 (int) $matches[5],
2510 (int) $matches[6],
2511 (int) $matches[2],
2512 (int) $matches[3],
2513 (int) $matches[1]
2514 );
2515 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row->finished, $matches);
2516 $epoch_2 = mktime(
2517 (int) $matches[4],
2518 (int) $matches[5],
2519 (int) $matches[6],
2520 (int) $matches[2],
2521 (int) $matches[3],
2522 (int) $matches[1]
2523 );
2524 if (isset($times[$row->active_fi])) {
2525 $times[$row->active_fi] += ($epoch_2 - $epoch_1);
2526 } else {
2527 $times[$row->active_fi] = ($epoch_2 - $epoch_1);
2528 }
2529 }
2530 $max_time = 0;
2531 $counter = 0;
2532 foreach ($times as $value) {
2533 $max_time += $value;
2534 $counter++;
2535 }
2536 if ($counter === 0) {
2537 return 0;
2538 }
2539 return (int) round($max_time / $counter);
2540 }
2541
2549 bool $use_object_id = false,
2550 ?bool $equal_points = false,
2551 bool $could_be_offline = false,
2552 bool $show_path = false,
2553 bool $with_questioncount = false,
2554 string $permission = 'read'
2555 ): array {
2556 return ilObjQuestionPool::_getAvailableQuestionpools(
2557 $use_object_id,
2558 $equal_points ?? false,
2559 $could_be_offline,
2560 $show_path,
2561 $with_questioncount,
2562 $permission
2563 );
2564 }
2565
2572 public function getImagePath(): string
2573 {
2574 return CLIENT_WEB_DIR . "/assessment/" . $this->getId() . "/images/";
2575 }
2576
2583 public function getImagePathWeb()
2584 {
2585 $webdir = ilFileUtils::removeTrailingPathSeparators(CLIENT_WEB_DIR) . "/assessment/" . $this->getId() . "/images/";
2586 return str_replace(
2587 ilFileUtils::removeTrailingPathSeparators(ILIAS_ABSOLUTE_PATH),
2589 $webdir
2590 );
2591 }
2592
2601 public function createQuestionGUI($question_type, $question_id = -1): ?assQuestionGUI
2602 {
2603 if ((!$question_type) and ($question_id > 0)) {
2604 $question_type = $this->getQuestionType($question_id);
2605 }
2606
2607 if (!strlen($question_type)) {
2608 return null;
2609 }
2610
2611 if ($question_id > 0) {
2612 $question_gui = assQuestion::instantiateQuestionGUI($question_id);
2613 } else {
2614 $question_type_gui = $question_type . 'GUI';
2615 $question_gui = new $question_type_gui();
2616 }
2617
2618 return $question_gui;
2619 }
2620
2627 public static function _instanciateQuestion($question_id): ?assQuestion
2628 {
2629 if (strcmp((string) $question_id, "") !== 0) {
2630 return assQuestion::instantiateQuestion((int) $question_id);
2631 }
2632
2633 return null;
2634 }
2635
2640 public function moveQuestions(array $move_questions, int $target_index, int $insert_mode): void
2641 {
2642 $this->questions = array_values($this->questions);
2643 $array_pos = array_search($target_index, $this->questions);
2644 if ($insert_mode == 0) {
2645 $part1 = array_slice($this->questions, 0, $array_pos);
2646 $part2 = array_slice($this->questions, $array_pos);
2647 } elseif ($insert_mode == 1) {
2648 $part1 = array_slice($this->questions, 0, $array_pos + 1);
2649 $part2 = array_slice($this->questions, $array_pos + 1);
2650 }
2651 foreach ($move_questions as $question_id) {
2652 if (!(array_search($question_id, $part1) === false)) {
2653 unset($part1[array_search($question_id, $part1)]);
2654 }
2655 if (!(array_search($question_id, $part2) === false)) {
2656 unset($part2[array_search($question_id, $part2)]);
2657 }
2658 }
2659 $part1 = array_values($part1);
2660 $part2 = array_values($part2);
2661 $new_array = array_values(array_merge($part1, $move_questions, $part2));
2662 $this->questions = [];
2663 $counter = 1;
2664 foreach ($new_array as $question_id) {
2665 $this->questions[$counter] = $question_id;
2666 $counter++;
2667 }
2668 $this->saveQuestionsToDb();
2669
2670 if ($this->logger->isLoggingEnabled()) {
2671 $this->logger->logTestAdministrationInteraction(
2672 $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
2673 $this->getRefId(),
2674 $this->user->getId(),
2675 TestAdministrationInteractionTypes::QUESTION_MOVED,
2676 [
2677 AdditionalInformationGenerator::KEY_QUESTION_ORDER => $this->questions
2678 ]
2679 )
2680 );
2681 }
2682 }
2683
2684
2692 public function startingTimeReached(): bool
2693 {
2694 if ($this->isStartingTimeEnabled() && $this->getStartingTime() != 0) {
2695 $now = time();
2696 if ($now < $this->getStartingTime()) {
2697 return false;
2698 }
2699 }
2700 return true;
2701 }
2702
2710 public function endingTimeReached(): bool
2711 {
2712 if ($this->isEndingTimeEnabled() && $this->getEndingTime() != 0) {
2713 $now = time();
2714 if ($now > $this->getEndingTime()) {
2715 return true;
2716 }
2717 }
2718 return false;
2719 }
2720
2726 public function getAvailableQuestions($arr_filter, $completeonly = 0): array
2727 {
2728 $available_pools = array_keys(ilObjQuestionPool::_getAvailableQuestionpools(true, false, false, false, false));
2729 $available = "";
2730 if (count($available_pools)) {
2731 $available = " AND " . $this->db->in('qpl_questions.obj_fi', $available_pools, false, 'integer');
2732 } else {
2733 return [];
2734 }
2735 if ($completeonly) {
2736 $available .= " AND qpl_questions.complete = " . $this->db->quote("1", 'text');
2737 }
2738
2739 $where = "";
2740 if (is_array($arr_filter)) {
2741 if (array_key_exists('title', $arr_filter) && strlen($arr_filter['title'])) {
2742 $where .= " AND " . $this->db->like('qpl_questions.title', 'text', "%%" . $arr_filter['title'] . "%%");
2743 }
2744 if (array_key_exists('description', $arr_filter) && strlen($arr_filter['description'])) {
2745 $where .= " AND " . $this->db->like('qpl_questions.description', 'text', "%%" . $arr_filter['description'] . "%%");
2746 }
2747 if (array_key_exists('author', $arr_filter) && strlen($arr_filter['author'])) {
2748 $where .= " AND " . $this->db->like('qpl_questions.author', 'text', "%%" . $arr_filter['author'] . "%%");
2749 }
2750 if (array_key_exists('type', $arr_filter) && strlen($arr_filter['type'])) {
2751 $where .= " AND qpl_qst_type.type_tag = " . $this->db->quote($arr_filter['type'], 'text');
2752 }
2753 if (array_key_exists('qpl', $arr_filter) && strlen($arr_filter['qpl'])) {
2754 $where .= " AND " . $this->db->like('object_data.title', 'text', "%%" . $arr_filter['qpl'] . "%%");
2755 }
2756 }
2757
2758 $original_ids = &$this->getExistingQuestions();
2759 $original_clause = " qpl_questions.original_id IS NULL";
2760 if (count($original_ids)) {
2761 $original_clause = " qpl_questions.original_id IS NULL AND " . $this->db->in('qpl_questions.question_id', $original_ids, true, 'integer');
2762 }
2763
2764 $query_result = $this->db->query("
2765 SELECT qpl_questions.*, qpl_questions.tstamp,
2766 qpl_qst_type.type_tag, qpl_qst_type.plugin, qpl_qst_type.plugin_name,
2767 object_data.title parent_title
2768 FROM qpl_questions, qpl_qst_type, object_data
2769 WHERE $original_clause $available
2770 AND object_data.obj_id = qpl_questions.obj_fi
2771 AND qpl_questions.tstamp > 0
2772 AND qpl_questions.question_type_fi = qpl_qst_type.question_type_id
2773 $where
2774 ");
2775 $rows = [];
2776
2777 if ($query_result->numRows()) {
2778 while ($row = $this->db->fetchAssoc($query_result)) {
2780
2781 if (!$row['plugin']) {
2782 $row[ 'ttype' ] = $this->lng->txt($row[ "type_tag" ]);
2783
2784 $rows[] = $row;
2785 continue;
2786 }
2787
2788 $plugin = $this->component_repository->getPluginByName($row['plugin_name']);
2789 if (!$plugin->isActive()) {
2790 continue;
2791 }
2792
2793 $pl = $this->component_factory->getPlugin($plugin->getId());
2794 $row[ 'ttype' ] = $pl->getQuestionTypeTranslation();
2795
2796 $rows[] = $row;
2797 }
2798 }
2799 return $rows;
2800 }
2801
2806 public function fromXML(ilQTIAssessment $assessment, array $mappings): void
2807 {
2808 if (($importdir = ilSession::get('path_to_container_import_file')) === null) {
2809 $importdir = $this->buildImportDirectoryFromImportFile(ilSession::get('path_to_import_file'));
2810 }
2811 ilSession::clear('path_to_container_import_file');
2812 ilSession::clear('import_mob_xhtml');
2813
2814 $this->saveToDb(true);
2815
2816 $main_settings = $this->getMainSettings();
2817 $general_settings = $main_settings->getGeneralSettings();
2818 $introduction_settings = $main_settings->getIntroductionSettings();
2819 $access_settings = $main_settings->getAccessSettings();
2820 $test_behaviour_settings = $main_settings->getTestBehaviourSettings();
2821 $question_behaviour_settings = $main_settings->getQuestionBehaviourSettings();
2822 $participant_functionality_settings = $main_settings->getParticipantFunctionalitySettings();
2823 $finishing_settings = $main_settings->getFinishingSettings();
2824 $additional_settings = $main_settings->getAdditionalSettings();
2825
2826 $introduction_settings = $introduction_settings->withIntroductionEnabled(false);
2827 foreach ($assessment->objectives as $objectives) {
2828 foreach ($objectives->materials as $material) {
2829 $introduction_settings = $this->addIntroductionToSettingsFromImport(
2830 $introduction_settings,
2831 $this->qtiMaterialToArray($material),
2832 $importdir,
2833 $mappings
2834 );
2835 }
2836 }
2837
2838 if ($assessment->getPresentationMaterial()
2839 && $assessment->getPresentationMaterial()->getFlowMat(0)
2840 && $assessment->getPresentationMaterial()->getFlowMat(0)->getMaterial(0)) {
2841 $finishing_settings = $this->addConcludingRemarksToSettingsFromImport(
2842 $finishing_settings,
2843 $this->qtiMaterialToArray(
2844 $assessment->getPresentationMaterial()->getFlowMat(0)->getMaterial(0)
2845 ),
2846 $importdir,
2847 $mappings
2848 );
2849 }
2850
2851 $score_settings = $this->getScoreSettings();
2852 $scoring_settings = $score_settings->getScoringSettings();
2853 $gamification_settings = $score_settings->getGamificationSettings();
2854 $result_summary_settings = $score_settings->getResultSummarySettings();
2855 $result_details_settings = $score_settings->getResultDetailsSettings();
2856
2857 $mark_steps = [];
2858 foreach ($assessment->qtimetadata as $metadata) {
2859 switch ($metadata["label"]) {
2860 case "solution_details":
2861 $result_details_settings = $result_details_settings->withShowPassDetails((bool) $metadata["entry"]);
2862 break;
2863 case "show_solution_list_comparison":
2864 $result_details_settings = $result_details_settings->withShowSolutionListComparison((bool) $metadata["entry"]);
2865 break;
2866 case "print_bs_with_res":
2867 $result_details_settings = $result_details_settings->withShowSolutionListComparison((bool) $metadata["entry"]);
2868 break;
2869 case "author":
2870 $this->saveAuthorToMetadata($metadata["entry"]);
2871 break;
2872 case "nr_of_tries":
2873 $test_behaviour_settings = $test_behaviour_settings->withNumberOfTries((int) $metadata["entry"]);
2874 break;
2875 case 'block_after_passed':
2876 $test_behaviour_settings = $test_behaviour_settings->withBlockAfterPassedEnabled((bool) $metadata['entry']);
2877 break;
2878 case "pass_waiting":
2879 $test_behaviour_settings = $test_behaviour_settings->withPassWaiting($metadata["entry"]);
2880 break;
2881 case "kiosk":
2882 $test_behaviour_settings = $test_behaviour_settings->withKioskMode((int) $metadata["entry"]);
2883 break;
2884 case 'show_introduction':
2885 $introduction_settings = $introduction_settings->withIntroductionEnabled((bool) $metadata['entry']);
2886 break;
2887 case "showfinalstatement":
2888 case 'show_concluding_remarks':
2889 $finishing_settings = $finishing_settings->withConcludingRemarksEnabled((bool) $metadata["entry"]);
2890 break;
2891 case 'exam_conditions':
2892 $introduction_settings = $introduction_settings->withExamConditionsCheckboxEnabled($metadata['entry'] === '1');
2893 break;
2894 case "highscore_enabled":
2895 $gamification_settings = $gamification_settings->withHighscoreEnabled((bool) $metadata["entry"]);
2896 break;
2897 case "highscore_anon":
2898 $gamification_settings = $gamification_settings->withHighscoreAnon((bool) $metadata["entry"]);
2899 break;
2900 case "highscore_achieved_ts":
2901 $gamification_settings = $gamification_settings->withHighscoreAchievedTS((bool) $metadata["entry"]);
2902 break;
2903 case "highscore_score":
2904 $gamification_settings = $gamification_settings->withHighscoreScore((bool) $metadata["entry"]);
2905 break;
2906 case "highscore_percentage":
2907 $gamification_settings = $gamification_settings->withHighscorePercentage((bool) $metadata["entry"]);
2908 break;
2909 case "highscore_wtime":
2910 $gamification_settings = $gamification_settings->withHighscoreWTime((bool) $metadata["entry"]);
2911 break;
2912 case "highscore_own_table":
2913 $gamification_settings = $gamification_settings->withHighscoreOwnTable((bool) $metadata["entry"]);
2914 break;
2915 case "highscore_top_table":
2916 $gamification_settings = $gamification_settings->withHighscoreTopTable((bool) $metadata["entry"]);
2917 break;
2918 case "highscore_top_num":
2919 $gamification_settings = $gamification_settings->withHighscoreTopNum((int) $metadata["entry"]);
2920 break;
2921 case "use_previous_answers":
2922 $participant_functionality_settings = $participant_functionality_settings->withUsePreviousAnswerAllowed((bool) $metadata["entry"]);
2923 break;
2924 case 'question_list_enabled':
2925 $participant_functionality_settings = $participant_functionality_settings->withQuestionListEnabled((bool) $metadata['entry']);
2926 // no break
2927 case "title_output":
2928 $question_behaviour_settings = $question_behaviour_settings->withQuestionTitleOutputMode((int) $metadata["entry"]);
2929 break;
2930 case "question_set_type":
2931 if ($metadata['entry'] === self::QUESTION_SET_TYPE_RANDOM) {
2932 $this->questions = [];
2933 }
2934 $general_settings = $general_settings->withQuestionSetType($metadata["entry"]);
2935 break;
2936 case "anonymity":
2937 $general_settings = $general_settings->withAnonymity((bool) $metadata["entry"]);
2938 break;
2939 case "results_presentation":
2940 $result_details_settings = $result_details_settings->withResultsPresentation((int) $metadata["entry"]);
2941 break;
2942 case "reset_processing_time":
2943 $test_behaviour_settings = $test_behaviour_settings->withResetProcessingTime($metadata["entry"] === '1');
2944 break;
2945 case "answer_feedback_points":
2946 $question_behaviour_settings = $question_behaviour_settings->withInstantFeedbackPointsEnabled((bool) $metadata["entry"]);
2947 break;
2948 case "answer_feedback":
2949 $question_behaviour_settings = $question_behaviour_settings->withInstantFeedbackGenericEnabled((bool) $metadata["entry"]);
2950 break;
2951 case 'instant_feedback_specific':
2952 $question_behaviour_settings = $question_behaviour_settings->withInstantFeedbackSpecificEnabled((bool) $metadata['entry']);
2953 break;
2954 case "instant_verification":
2955 $question_behaviour_settings = $question_behaviour_settings->withInstantFeedbackSolutionEnabled((bool) $metadata["entry"]);
2956 break;
2957 case "force_instant_feedback":
2958 $question_behaviour_settings = $question_behaviour_settings->withForceInstantFeedbackOnNextQuestion((bool) $metadata["entry"]);
2959 break;
2960 case "follow_qst_answer_fixation":
2961 $question_behaviour_settings = $question_behaviour_settings->withLockAnswerOnNextQuestionEnabled((bool) $metadata["entry"]);
2962 break;
2963 case "instant_feedback_answer_fixation":
2964 $question_behaviour_settings = $question_behaviour_settings->withLockAnswerOnInstantFeedbackEnabled((bool) $metadata["entry"]);
2965 break;
2966 case "show_cancel":
2967 case "suspend_test_allowed":
2968 $participant_functionality_settings = $participant_functionality_settings->withSuspendTestAllowed((bool) $metadata["entry"]);
2969 break;
2970 case "sequence_settings":
2971 $participant_functionality_settings = $participant_functionality_settings->withPostponedQuestionsMoveToEnd((bool) $metadata["entry"]);
2972 break;
2973 case "show_marker":
2974 $participant_functionality_settings = $participant_functionality_settings->withQuestionMarkingEnabled((bool) $metadata["entry"]);
2975 break;
2976 case "fixed_participants":
2977 $access_settings = $access_settings->withFixedParticipants((bool) $metadata["entry"]);
2978 break;
2979 case "score_reporting":
2980 if ($metadata['entry'] !== null) {
2981 $result_summary_settings = $result_summary_settings->withScoreReporting(
2982 ScoreReportingTypes::tryFrom((int) $metadata['entry']) ?? ScoreReportingTypes::SCORE_REPORTING_DISABLED
2983 );
2984 }
2985 break;
2986 case "shuffle_questions":
2987 $question_behaviour_settings = $question_behaviour_settings->withShuffleQuestions((bool) $metadata["entry"]);
2988 break;
2989 case "count_system":
2990 $scoring_settings = $scoring_settings->withCountSystem((int) $metadata["entry"]);
2991 break;
2992 case "mailnotification":
2993 $finishing_settings = $finishing_settings->withMailNotificationContentType((int) $metadata["entry"]);
2994 break;
2995 case "mailnottype":
2996 $finishing_settings = $finishing_settings->withAlwaysSendMailNotification((bool) $metadata["entry"]);
2997 break;
2998 case "exportsettings":
2999 $result_details_settings = $result_details_settings->withExportSettings((int) $metadata["entry"]);
3000 break;
3001 case "score_cutting":
3002 $scoring_settings = $scoring_settings->withScoreCutting((int) $metadata["entry"]);
3003 break;
3004 case "password":
3005 $access_settings = $access_settings->withPasswordEnabled(
3006 $metadata["entry"] !== null && $metadata["entry"] !== ''
3007 )->withPassword($metadata["entry"]);
3008 break;
3009 case 'ip_range_from':
3010 if ($metadata['entry'] !== '') {
3011 $access_settings = $access_settings->withIpRangeFrom($metadata['entry']);
3012 }
3013 break;
3014 case 'ip_range_to':
3015 if ($metadata['entry'] !== '') {
3016 $access_settings = $access_settings->withIpRangeTo($metadata['entry']);
3017 }
3018 break;
3019 case "pass_scoring":
3020 $scoring_settings = $scoring_settings->withPassScoring((int) $metadata["entry"]);
3021 break;
3022 case 'pass_deletion_allowed':
3023 $result_summary_settings = $result_summary_settings->withPassDeletionAllowed((bool) $metadata["entry"]);
3024 break;
3025 case "usr_pass_overview_mode":
3026 $participant_functionality_settings = $participant_functionality_settings->withUsrPassOverviewMode((int) $metadata["entry"]);
3027 break;
3028 case "question_list":
3029 $participant_functionality_settings = $participant_functionality_settings->withQuestionListEnabled((bool) $metadata["entry"]);
3030 break;
3031
3032 case "reporting_date":
3033 $reporting_date = $this->buildDateTimeImmutableFromPeriod($metadata['entry']);
3034 if ($reporting_date !== null) {
3035 $result_summary_settings = $result_summary_settings->withReportingDate($reporting_date);
3036 }
3037 break;
3038 case 'enable_processing_time':
3039 $test_behaviour_settings = $test_behaviour_settings->withProcessingTimeEnabled((bool) $metadata['entry']);
3040 break;
3041 case "processing_time":
3042 $test_behaviour_settings = $test_behaviour_settings->withProcessingTime($metadata['entry']);
3043 break;
3044 case "starting_time":
3045 $starting_time = $this->buildDateTimeImmutableFromPeriod($metadata['entry']);
3046 if ($starting_time !== null) {
3047 $access_settings = $access_settings->withStartTime($starting_time)
3048 ->withStartTimeEnabled(true);
3049 }
3050 break;
3051 case "ending_time":
3052 $ending_time = $this->buildDateTimeImmutableFromPeriod($metadata['entry']);
3053 if ($ending_time !== null) {
3054 $access_settings = $access_settings->withEndTime($ending_time)
3055 ->withStartTimeEnabled(true);
3056 }
3057 break;
3058 case "enable_examview":
3059 $finishing_settings = $finishing_settings->withShowAnswerOverview((bool) $metadata["entry"]);
3060 break;
3061 case 'redirection_mode':
3062 $finishing_settings = $finishing_settings->withRedirectionMode(
3063 RedirectionModes::tryFrom($metadata['entry'] ?? 0) ?? RedirectionModes::NONE
3064 );
3065 break;
3066 case 'redirection_url':
3067 $finishing_settings = $finishing_settings->withRedirectionUrl($metadata['entry']);
3068 break;
3069 case 'examid_in_test_pass':
3070 $test_behaviour_settings = $test_behaviour_settings->withExamIdInTestAttemptEnabled((bool) $metadata['entry']);
3071 break;
3072 case 'examid_in_test_res':
3073 $result_details_settings = $result_details_settings->withShowExamIdInTestResults((bool) $metadata["entry"]);
3074 break;
3075 case 'skill_service':
3076 $additional_settings = $additional_settings->withSkillsServiceEnabled((bool) $metadata['entry']);
3077 break;
3078 case 'show_grading_status':
3079 $result_summary_settings = $result_summary_settings->withShowGradingStatusEnabled((bool) $metadata["entry"]);
3080 break;
3081 case 'show_grading_mark':
3082 $result_summary_settings = $result_summary_settings->withShowGradingMarkEnabled((bool) $metadata["entry"]);
3083 break;
3084 case 'autosave':
3085 $question_behaviour_settings = $question_behaviour_settings->withAutosaveEnabled((bool) $metadata['entry']);
3086 break;
3087 case 'autosave_ival':
3088 $question_behaviour_settings = $question_behaviour_settings->withAutosaveInterval((int) $metadata['entry']);
3089 break;
3090 case 'show_summary':
3091 $participant_functionality_settings = $participant_functionality_settings->withQuestionListEnabled(($metadata['entry'] & 1) > 0)
3092 ->withUsrPassOverviewMode((int) $metadata['entry']);
3093
3094 // no break
3095 case 'hide_info_tab':
3096 $additional_settings = $additional_settings->withHideInfoTab($metadata['entry'] === '1');
3097 }
3098 if (preg_match("/mark_step_\d+/", $metadata["label"])) {
3099 $xmlmark = $metadata["entry"];
3100 preg_match("/<short>(.*?)<\/short>/", $xmlmark, $matches);
3101 $mark_short = $matches[1];
3102 preg_match("/<official>(.*?)<\/official>/", $xmlmark, $matches);
3103 $mark_official = $matches[1];
3104 preg_match("/<percentage>(.*?)<\/percentage>/", $xmlmark, $matches);
3105 $mark_percentage = (float) $matches[1];
3106 preg_match("/<passed>(.*?)<\/passed>/", $xmlmark, $matches);
3107 $mark_passed = (bool) $matches[1];
3108 $mark_steps[] = new Mark($mark_short, $mark_official, $mark_percentage, $mark_passed);
3109 }
3110 }
3111 $this->mark_schema = $this->getMarkSchema()->withMarkSteps($mark_steps);
3112 $this->saveToDb();
3113 $this->getObjectProperties()->storePropertyTitleAndDescription(
3114 $this->getObjectProperties()->getPropertyTitleAndDescription()
3115 ->withTitle($assessment->getTitle())
3116 ->withDescription($assessment->getComment())
3117 );
3118 $this->addToNewsOnOnline(false, $this->getObjectProperties()->getPropertyIsOnline()->getIsOnline());
3119 $main_settings = $main_settings
3120 ->withGeneralSettings($general_settings)
3121 ->withIntroductionSettings($introduction_settings)
3122 ->withAccessSettings($access_settings)
3123 ->withParticipantFunctionalitySettings($participant_functionality_settings)
3124 ->withTestBehaviourSettings($test_behaviour_settings)
3125 ->withQuestionBehaviourSettings($question_behaviour_settings)
3126 ->withFinishingSettings($finishing_settings)
3127 ->withAdditionalSettings($additional_settings);
3128 $this->getMainSettingsRepository()->store($main_settings);
3129 $this->main_settings = $main_settings;
3130
3131 $score_settings = $score_settings
3132 ->withGamificationSettings($gamification_settings)
3133 ->withScoringSettings($scoring_settings)
3134 ->withResultDetailsSettings($result_details_settings)
3135 ->withResultSummarySettings($result_summary_settings);
3136 $this->getScoreSettingsRepository()->store($score_settings);
3137 $this->score_settings = $score_settings;
3138 $this->loadFromDb();
3139 }
3140
3142 SettingsIntroduction $settings,
3143 array $material,
3144 string $importdir,
3145 array $mappings
3147 $text = $material['text'];
3148 $mobs = $material['mobs'];
3149 if (str_starts_with($text, '<PageObject>')) {
3150 $text = $this->replaceMobsInPageImports(
3151 $text,
3152 $mappings['components/ILIAS/MediaObjects']['mob'] ?? []
3153 );
3154 $text = $this->replaceFilesInPageImports(
3155 $text,
3156 $mappings['components/ILIAS/File']['file'] ?? []
3157 );
3158 $page_object = new ilTestPage();
3159 $page_object->setParentId($this->getId());
3160 $page_object->setXMLContent($text);
3161 $new_page_id = $page_object->createPageWithNextId();
3162 return $settings->withIntroductionPageId($new_page_id);
3163 }
3164
3165 $text = $this->retrieveMobsFromLegacyImports($text, $mobs, $importdir);
3166
3167 return new SettingsIntroduction(
3168 $settings->getTestId(),
3169 $text !== '',
3170 $text
3171 );
3172 }
3173
3175 SettingsFinishing $settings,
3176 array $material,
3177 string $importdir,
3178 array $mappings
3180 $file_to_import = ilSession::get('path_to_import_file');
3181 $text = $material['text'];
3182 $mobs = $material['mobs'];
3183 if (str_starts_with($text, '<PageObject>')) {
3184 $text = $this->replaceMobsInPageImports(
3185 $text,
3186 $mappings['components/ILIAS/MediaObjects']['mob'] ?? []
3187 );
3188 $text = $this->replaceFilesInPageImports(
3189 $text,
3190 $mappings['components/ILIAS/File']['file'] ?? []
3191 );
3192 $page_object = new ilTestPage();
3193 $page_object->setParentId($this->getId());
3194 $page_object->setXMLContent($text);
3195 $new_page_id = $page_object->createPageWithNextId();
3196 return $settings->withConcludingRemarksPageId($new_page_id);
3197 }
3198
3199 $text = $this->retrieveMobsFromLegacyImports($text, $mobs, $importdir);
3200
3201 return new SettingsFinishing(
3202 $settings->getTestId(),
3203 $settings->getShowAnswerOverview(),
3204 strlen($text) > 0,
3205 $text,
3206 null,
3207 $settings->getRedirectionMode(),
3208 $settings->getRedirectionUrl(),
3209 $settings->getMailNotificationContentType(),
3211 );
3212 }
3213
3214 private function replaceMobsInPageImports(string $text, array $mappings): string
3215 {
3216 preg_match_all('/il_(\d+)_mob_(\d+)/', $text, $matches);
3217 foreach ($matches[0] as $index => $match) {
3218 if (empty($mappings[$matches[2][$index]])) {
3219 continue;
3220 }
3221 $text = str_replace($match, "il__mob_{$mappings[$matches[2][$index]]}", $text);
3222 ilObjMediaObject::_saveUsage((int) $mappings[$matches[2][$index]], 'tst', $this->getId());
3223 }
3224 return $text;
3225 }
3226
3227 private function replaceFilesInPageImports(string $text, array $mappings): string
3228 {
3229 preg_match_all('/il_(\d+)_file_(\d+)/', $text, $matches);
3230 foreach ($matches[0] as $index => $match) {
3231 if (empty($mappings[$matches[2][$index]])) {
3232 continue;
3233 }
3234 $text = str_replace($match, "il__file_{$mappings[$matches[2][$index]]}", $text);
3235 }
3236 return $text;
3237 }
3238
3239 private function retrieveMobsFromLegacyImports(string $text, array $mobs, string $importdir): string
3240 {
3241 foreach ($mobs as $mob) {
3242 $importfile = $importdir . DIRECTORY_SEPARATOR . $mob['uri'];
3243 if (file_exists($importfile)) {
3244 $media_object = ilObjMediaObject::_saveTempFileAsMediaObject(basename($importfile), $importfile, false);
3245 ilObjMediaObject::_saveUsage($media_object->getId(), 'tst:html', $this->getId());
3247 str_replace(
3248 'src="' . $mob['mob'] . '"',
3249 'src="' . 'il_' . IL_INST_ID . '_mob_' . $media_object->getId() . '"',
3250 $text
3251 ),
3252 1
3253 );
3254 }
3255 }
3256 return $text;
3257 }
3258
3264 public function toXML(): string
3265 {
3266 $main_settings = $this->getMainSettings();
3267 $a_xml_writer = new ilXmlWriter();
3268 // set xml header
3269 $a_xml_writer->xmlHeader();
3270 $a_xml_writer->xmlSetDtdDef("<!DOCTYPE questestinterop SYSTEM \"ims_qtiasiv1p2p1.dtd\">");
3271 $a_xml_writer->xmlStartTag("questestinterop");
3272
3273 $attrs = [
3274 "ident" => "il_" . IL_INST_ID . "_tst_" . $this->getTestId(),
3275 "title" => $this->getTitle()
3276 ];
3277 $a_xml_writer->xmlStartTag("assessment", $attrs);
3278 $a_xml_writer->xmlElement("qticomment", null, $this->getDescription());
3279
3280 if ($main_settings->getTestBehaviourSettings()->getProcessingTimeEnabled()) {
3281 $a_xml_writer->xmlElement(
3282 "duration",
3283 null,
3284 $this->getProcessingTimeForXML()
3285 );
3286 }
3287
3288 $a_xml_writer->xmlStartTag("qtimetadata");
3289 $a_xml_writer->xmlStartTag("qtimetadatafield");
3290 $a_xml_writer->xmlElement("fieldlabel", null, "ILIAS_VERSION");
3291 $a_xml_writer->xmlElement("fieldentry", null, ILIAS_VERSION);
3292 $a_xml_writer->xmlEndTag("qtimetadatafield");
3293
3294 $a_xml_writer->xmlStartTag("qtimetadatafield");
3295 $a_xml_writer->xmlElement("fieldlabel", null, "anonymity");
3296 $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $main_settings->getGeneralSettings()->getAnonymity()));
3297 $a_xml_writer->xmlEndTag("qtimetadatafield");
3298
3299 $a_xml_writer->xmlStartTag("qtimetadatafield");
3300 $a_xml_writer->xmlElement("fieldlabel", null, "question_set_type");
3301 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getGeneralSettings()->getQuestionSetType());
3302 $a_xml_writer->xmlEndTag("qtimetadatafield");
3303
3304 $a_xml_writer->xmlStartTag("qtimetadatafield");
3305 $a_xml_writer->xmlElement("fieldlabel", null, "sequence_settings");
3306 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getParticipantFunctionalitySettings()->getPostponedQuestionsMoveToEnd());
3307 $a_xml_writer->xmlEndTag("qtimetadatafield");
3308
3309 $a_xml_writer->xmlStartTag("qtimetadatafield");
3310 $a_xml_writer->xmlElement("fieldlabel", null, "author");
3311 $a_xml_writer->xmlElement("fieldentry", null, $this->getAuthor());
3312 $a_xml_writer->xmlEndTag("qtimetadatafield");
3313
3314 $a_xml_writer->xmlStartTag("qtimetadatafield");
3315 $a_xml_writer->xmlElement("fieldlabel", null, "reset_processing_time");
3316 $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getTestBehaviourSettings()->getResetProcessingTime());
3317 $a_xml_writer->xmlEndTag("qtimetadatafield");
3318
3319 $a_xml_writer->xmlStartTag("qtimetadatafield");
3320 $a_xml_writer->xmlElement("fieldlabel", null, "count_system");
3321 $a_xml_writer->xmlElement("fieldentry", null, $this->getCountSystem());
3322 $a_xml_writer->xmlEndTag("qtimetadatafield");
3323
3324 $a_xml_writer->xmlStartTag("qtimetadatafield");
3325 $a_xml_writer->xmlElement("fieldlabel", null, "score_cutting");
3326 $a_xml_writer->xmlElement("fieldentry", null, $this->getScoreCutting());
3327 $a_xml_writer->xmlEndTag("qtimetadatafield");
3328
3329 $a_xml_writer->xmlStartTag("qtimetadatafield");
3330 $a_xml_writer->xmlElement("fieldlabel", null, "password");
3331 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getAccessSettings()->getPassword() ?? '');
3332 $a_xml_writer->xmlEndTag("qtimetadatafield");
3333
3334 $a_xml_writer->xmlStartTag("qtimetadatafield");
3335 $a_xml_writer->xmlElement("fieldlabel", null, "ip_range_from");
3336 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getAccessSettings()->getIpRangeFrom());
3337 $a_xml_writer->xmlEndTag("qtimetadatafield");
3338
3339 $a_xml_writer->xmlStartTag("qtimetadatafield");
3340 $a_xml_writer->xmlElement("fieldlabel", null, "ip_range_to");
3341 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getAccessSettings()->getIpRangeTo());
3342 $a_xml_writer->xmlEndTag("qtimetadatafield");
3343
3344 $a_xml_writer->xmlStartTag("qtimetadatafield");
3345 $a_xml_writer->xmlElement("fieldlabel", null, "pass_scoring");
3346 $a_xml_writer->xmlElement("fieldentry", null, $this->getPassScoring());
3347 $a_xml_writer->xmlEndTag("qtimetadatafield");
3348
3349 $a_xml_writer->xmlStartTag('qtimetadatafield');
3350 $a_xml_writer->xmlElement('fieldlabel', null, 'pass_deletion_allowed');
3351 $a_xml_writer->xmlElement('fieldentry', null, (int) $this->isPassDeletionAllowed());
3352 $a_xml_writer->xmlEndTag('qtimetadatafield');
3353
3354 if ($this->getScoreSettings()->getResultSummarySettings()->getReportingDate() !== null) {
3355 $a_xml_writer->xmlStartTag("qtimetadatafield");
3356 $a_xml_writer->xmlElement("fieldlabel", null, "reporting_date");
3357 $a_xml_writer->xmlElement(
3358 "fieldentry",
3359 null,
3360 $this->buildIso8601PeriodForExportCompatibility(
3361 $this->getScoreSettings()->getResultSummarySettings()->getReportingDate(),
3362 ),
3363 );
3364 $a_xml_writer->xmlEndTag("qtimetadatafield");
3365 }
3366
3367 $a_xml_writer->xmlStartTag("qtimetadatafield");
3368 $a_xml_writer->xmlElement("fieldlabel", null, "nr_of_tries");
3369 $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $main_settings->getTestBehaviourSettings()->getNumberOfTries()));
3370 $a_xml_writer->xmlEndTag("qtimetadatafield");
3371
3372 $a_xml_writer->xmlStartTag('qtimetadatafield');
3373 $a_xml_writer->xmlElement('fieldlabel', null, 'block_after_passed');
3374 $a_xml_writer->xmlElement('fieldentry', null, (int) $main_settings->getTestBehaviourSettings()->getBlockAfterPassedEnabled());
3375 $a_xml_writer->xmlEndTag('qtimetadatafield');
3376
3377 $a_xml_writer->xmlStartTag("qtimetadatafield");
3378 $a_xml_writer->xmlElement("fieldlabel", null, "pass_waiting");
3379 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getTestBehaviourSettings()->getPassWaiting());
3380 $a_xml_writer->xmlEndTag("qtimetadatafield");
3381
3382 $a_xml_writer->xmlStartTag("qtimetadatafield");
3383 $a_xml_writer->xmlElement("fieldlabel", null, "kiosk");
3384 $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $main_settings->getTestBehaviourSettings()->getKioskMode()));
3385 $a_xml_writer->xmlEndTag("qtimetadatafield");
3386
3387 $a_xml_writer->xmlStartTag('qtimetadatafield');
3388 $a_xml_writer->xmlElement("fieldlabel", null, "redirection_mode");
3389 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getFinishingSettings()->getRedirectionMode()->value);
3390 $a_xml_writer->xmlEndTag("qtimetadatafield");
3391
3392 $a_xml_writer->xmlStartTag('qtimetadatafield');
3393 $a_xml_writer->xmlElement("fieldlabel", null, "redirection_url");
3394 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getFinishingSettings()->getRedirectionUrl());
3395 $a_xml_writer->xmlEndTag("qtimetadatafield");
3396
3397 $a_xml_writer->xmlStartTag("qtimetadatafield");
3398 $a_xml_writer->xmlElement("fieldlabel", null, "use_previous_answers");
3399 $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getParticipantFunctionalitySettings()->getUsePreviousAnswerAllowed());
3400 $a_xml_writer->xmlEndTag("qtimetadatafield");
3401
3402 $a_xml_writer->xmlStartTag('qtimetadatafield');
3403 $a_xml_writer->xmlElement('fieldlabel', null, 'question_list_enabled');
3404 $a_xml_writer->xmlElement('fieldentry', null, (int) $main_settings->getParticipantFunctionalitySettings()->getQuestionListEnabled());
3405 $a_xml_writer->xmlEndTag('qtimetadatafield');
3406
3407 $a_xml_writer->xmlStartTag("qtimetadatafield");
3408 $a_xml_writer->xmlElement("fieldlabel", null, "title_output");
3409 $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $main_settings->getQuestionBehaviourSettings()->getQuestionTitleOutputMode()));
3410 $a_xml_writer->xmlEndTag("qtimetadatafield");
3411
3412 $a_xml_writer->xmlStartTag("qtimetadatafield");
3413 $a_xml_writer->xmlElement("fieldlabel", null, "results_presentation");
3414 $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $this->getScoreSettings()->getResultDetailsSettings()->getResultsPresentation()));
3415 $a_xml_writer->xmlEndTag("qtimetadatafield");
3416
3417 $a_xml_writer->xmlStartTag("qtimetadatafield");
3418 $a_xml_writer->xmlElement("fieldlabel", null, "examid_in_test_pass");
3419 $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $main_settings->getTestBehaviourSettings()->getExamIdInTestAttemptEnabled()));
3420 $a_xml_writer->xmlEndTag("qtimetadatafield");
3421
3422 $a_xml_writer->xmlStartTag("qtimetadatafield");
3423 $a_xml_writer->xmlElement("fieldlabel", null, "examid_in_test_res");
3424 $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $this->getScoreSettings()->getResultDetailsSettings()->getShowExamIdInTestResults()));
3425 $a_xml_writer->xmlEndTag("qtimetadatafield");
3426
3427 $a_xml_writer->xmlStartTag("qtimetadatafield");
3428 $a_xml_writer->xmlElement("fieldlabel", null, "usr_pass_overview_mode");
3429 $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $main_settings->getParticipantFunctionalitySettings()->getUsrPassOverviewMode()));
3430 $a_xml_writer->xmlEndTag("qtimetadatafield");
3431
3432 $a_xml_writer->xmlStartTag("qtimetadatafield");
3433 $a_xml_writer->xmlElement("fieldlabel", null, "score_reporting");
3434 $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $this->getScoreSettings()->getResultSummarySettings()->getScoreReporting()->value));
3435 $a_xml_writer->xmlEndTag("qtimetadatafield");
3436
3437 $a_xml_writer->xmlStartTag("qtimetadatafield");
3438 $a_xml_writer->xmlElement("fieldlabel", null, "show_solution_list_comparison");
3439 $a_xml_writer->xmlElement("fieldentry", null, (int) $this->score_settings->getResultDetailsSettings()->getShowSolutionListComparison());
3440 $a_xml_writer->xmlEndTag("qtimetadatafield");
3441
3442 $a_xml_writer->xmlStartTag("qtimetadatafield");
3443 $a_xml_writer->xmlElement("fieldlabel", null, "instant_verification");
3444 $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackSolutionEnabled()));
3445 $a_xml_writer->xmlEndTag("qtimetadatafield");
3446
3447 $a_xml_writer->xmlStartTag("qtimetadatafield");
3448 $a_xml_writer->xmlElement("fieldlabel", null, "answer_feedback");
3449 $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackGenericEnabled()));
3450 $a_xml_writer->xmlEndTag("qtimetadatafield");
3451
3452 $a_xml_writer->xmlStartTag("qtimetadatafield");
3453 $a_xml_writer->xmlElement("fieldlabel", null, "instant_feedback_specific");
3454 $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackSpecificEnabled()));
3455 $a_xml_writer->xmlEndTag("qtimetadatafield");
3456
3457 $a_xml_writer->xmlStartTag("qtimetadatafield");
3458 $a_xml_writer->xmlElement("fieldlabel", null, "answer_feedback_points");
3459 $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackPointsEnabled()));
3460 $a_xml_writer->xmlEndTag("qtimetadatafield");
3461
3462 $a_xml_writer->xmlStartTag("qtimetadatafield");
3463 $a_xml_writer->xmlElement("fieldlabel", null, "follow_qst_answer_fixation");
3464 $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getQuestionBehaviourSettings()->getLockAnswerOnNextQuestionEnabled());
3465 $a_xml_writer->xmlEndTag("qtimetadatafield");
3466
3467 $a_xml_writer->xmlStartTag("qtimetadatafield");
3468 $a_xml_writer->xmlElement("fieldlabel", null, "instant_feedback_answer_fixation");
3469 $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getQuestionBehaviourSettings()->getLockAnswerOnInstantFeedbackEnabled());
3470 $a_xml_writer->xmlEndTag("qtimetadatafield");
3471
3472 $a_xml_writer->xmlStartTag("qtimetadatafield");
3473 $a_xml_writer->xmlElement("fieldlabel", null, "force_instant_feedback");
3474 $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getQuestionBehaviourSettings()->getForceInstantFeedbackOnNextQuestion());
3475 $a_xml_writer->xmlEndTag("qtimetadatafield");
3476
3477 $highscore_metadata = [
3478 'highscore_enabled' => ['value' => $this->getHighscoreEnabled()],
3479 'highscore_anon' => ['value' => $this->getHighscoreAnon()],
3480 'highscore_achieved_ts' => ['value' => $this->getHighscoreAchievedTS()],
3481 'highscore_score' => ['value' => $this->getHighscoreScore()],
3482 'highscore_percentage' => ['value' => $this->getHighscorePercentage()],
3483 'highscore_wtime' => ['value' => $this->getHighscoreWTime()],
3484 'highscore_own_table' => ['value' => $this->getHighscoreOwnTable()],
3485 'highscore_top_table' => ['value' => $this->getHighscoreTopTable()],
3486 'highscore_top_num' => ['value' => $this->getHighscoreTopNum()],
3487 ];
3488 foreach ($highscore_metadata as $label => $data) {
3489 $a_xml_writer->xmlStartTag("qtimetadatafield");
3490 $a_xml_writer->xmlElement("fieldlabel", null, $label);
3491 $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $data['value']));
3492 $a_xml_writer->xmlEndTag("qtimetadatafield");
3493 }
3494
3495 $a_xml_writer->xmlStartTag("qtimetadatafield");
3496 $a_xml_writer->xmlElement("fieldlabel", null, "suspend_test_allowed");
3497 $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getParticipantFunctionalitySettings()->getSuspendTestAllowed()));
3498 $a_xml_writer->xmlEndTag("qtimetadatafield");
3499
3500 $a_xml_writer->xmlStartTag("qtimetadatafield");
3501 $a_xml_writer->xmlElement("fieldlabel", null, "show_marker");
3502 $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getParticipantFunctionalitySettings()->getQuestionMarkingEnabled()));
3503 $a_xml_writer->xmlEndTag("qtimetadatafield");
3504
3505 $a_xml_writer->xmlStartTag("qtimetadatafield");
3506 $a_xml_writer->xmlElement("fieldlabel", null, "fixed_participants");
3507 $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getAccessSettings()->getFixedParticipants()));
3508 $a_xml_writer->xmlEndTag("qtimetadatafield");
3509
3510 $a_xml_writer->xmlStartTag("qtimetadatafield");
3511 $a_xml_writer->xmlElement("fieldlabel", null, "show_introduction");
3512 $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getIntroductionSettings()->getIntroductionEnabled()));
3513 $a_xml_writer->xmlEndTag("qtimetadatafield");
3514
3515 $a_xml_writer->xmlStartTag("qtimetadatafield");
3516 $a_xml_writer->xmlElement("fieldlabel", null, 'exam_conditions');
3517 $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getIntroductionSettings()->getExamConditionsCheckboxEnabled()));
3518 $a_xml_writer->xmlEndTag("qtimetadatafield");
3519
3520 $a_xml_writer->xmlStartTag("qtimetadatafield");
3521 $a_xml_writer->xmlElement("fieldlabel", null, "show_concluding_remarks");
3522 $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getFinishingSettings()->getConcludingRemarksEnabled()));
3523 $a_xml_writer->xmlEndTag("qtimetadatafield");
3524
3525 $a_xml_writer->xmlStartTag("qtimetadatafield");
3526 $a_xml_writer->xmlElement("fieldlabel", null, "mailnotification");
3527 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getFinishingSettings()->getMailNotificationContentType());
3528 $a_xml_writer->xmlEndTag("qtimetadatafield");
3529
3530 $a_xml_writer->xmlStartTag("qtimetadatafield");
3531 $a_xml_writer->xmlElement("fieldlabel", null, "mailnottype");
3532 $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getFinishingSettings()->getAlwaysSendMailNotification());
3533 $a_xml_writer->xmlEndTag("qtimetadatafield");
3534
3535 $a_xml_writer->xmlStartTag("qtimetadatafield");
3536 $a_xml_writer->xmlElement("fieldlabel", null, "exportsettings");
3537 $a_xml_writer->xmlElement("fieldentry", null, $this->getExportSettings());
3538 $a_xml_writer->xmlEndTag("qtimetadatafield");
3539
3540 $a_xml_writer->xmlStartTag("qtimetadatafield");
3541 $a_xml_writer->xmlElement("fieldlabel", null, "shuffle_questions");
3542 $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getQuestionBehaviourSettings()->getShuffleQuestions()));
3543 $a_xml_writer->xmlEndTag("qtimetadatafield");
3544
3545 $a_xml_writer->xmlStartTag("qtimetadatafield");
3546 $a_xml_writer->xmlElement("fieldlabel", null, "processing_time");
3547 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getTestBehaviourSettings()->getProcessingTime());
3548 $a_xml_writer->xmlEndTag("qtimetadatafield");
3549
3550 $a_xml_writer->xmlStartTag("qtimetadatafield");
3551 $a_xml_writer->xmlElement("fieldlabel", null, "enable_examview");
3552 $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getFinishingSettings()->getShowAnswerOverview());
3553 $a_xml_writer->xmlEndTag("qtimetadatafield");
3554
3555 $a_xml_writer->xmlStartTag("qtimetadatafield");
3556 $a_xml_writer->xmlElement("fieldlabel", null, "skill_service");
3557 $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getAdditionalSettings()->getSkillsServiceEnabled());
3558 $a_xml_writer->xmlEndTag("qtimetadatafield");
3559
3560 if ($this->getInstantFeedbackSolution() == 1) {
3561 $attrs = [
3562 "solutionswitch" => "Yes"
3563 ];
3564 } else {
3565 $attrs = null;
3566 }
3567 $a_xml_writer->xmlElement("assessmentcontrol", $attrs, null);
3568
3569 $a_xml_writer->xmlStartTag("qtimetadatafield");
3570 $a_xml_writer->xmlElement("fieldlabel", null, "show_grading_status");
3571 $a_xml_writer->xmlElement("fieldentry", null, (int) $this->isShowGradingStatusEnabled());
3572 $a_xml_writer->xmlEndTag("qtimetadatafield");
3573
3574 $a_xml_writer->xmlStartTag("qtimetadatafield");
3575 $a_xml_writer->xmlElement("fieldlabel", null, "show_grading_mark");
3576 $a_xml_writer->xmlElement("fieldentry", null, (int) $this->isShowGradingMarkEnabled());
3577 $a_xml_writer->xmlEndTag("qtimetadatafield");
3578
3579 $a_xml_writer->xmlStartTag('qtimetadatafield');
3580 $a_xml_writer->xmlElement('fieldlabel', null, 'hide_info_tab');
3581 $a_xml_writer->xmlElement('fieldentry', null, (int) $this->getMainSettings()->getAdditionalSettings()->getHideInfoTab());
3582 $a_xml_writer->xmlEndTag("qtimetadatafield");
3583
3584 if ($this->getStartingTime() > 0) {
3585 $a_xml_writer->xmlStartTag("qtimetadatafield");
3586 $a_xml_writer->xmlElement("fieldlabel", null, "starting_time");
3587 $a_xml_writer->xmlElement(
3588 "fieldentry",
3589 null,
3590 $this->buildIso8601PeriodForExportCompatibility(
3591 (new DateTimeImmutable())->setTimestamp($this->getStartingTime()),
3592 ),
3593 );
3594 $a_xml_writer->xmlEndTag("qtimetadatafield");
3595 }
3596
3597 if ($this->getEndingTime() > 0) {
3598 $a_xml_writer->xmlStartTag("qtimetadatafield");
3599 $a_xml_writer->xmlElement("fieldlabel", null, "ending_time");
3600 $a_xml_writer->xmlElement(
3601 "fieldentry",
3602 null,
3603 $this->buildIso8601PeriodForExportCompatibility(
3604 (new DateTimeImmutable())->setTimestamp($this->getEndingTime()),
3605 ),
3606 );
3607 $a_xml_writer->xmlEndTag("qtimetadatafield");
3608 }
3609
3610 $a_xml_writer->xmlStartTag("qtimetadatafield");
3611 $a_xml_writer->xmlElement("fieldlabel", null, "autosave");
3612 $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getQuestionBehaviourSettings()->getAutosaveEnabled());
3613 $a_xml_writer->xmlEndTag("qtimetadatafield");
3614
3615 $a_xml_writer->xmlStartTag("qtimetadatafield");
3616 $a_xml_writer->xmlElement("fieldlabel", null, "autosave_ival");
3617 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getAutosaveInterval());
3618 $a_xml_writer->xmlEndTag("qtimetadatafield");
3619
3620 $a_xml_writer->xmlStartTag("qtimetadatafield");
3621 $a_xml_writer->xmlElement("fieldlabel", null, "instant_feedback_specific");
3622 $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackSpecificEnabled());
3623 $a_xml_writer->xmlEndTag("qtimetadatafield");
3624
3625 $a_xml_writer->xmlStartTag("qtimetadatafield");
3626 $a_xml_writer->xmlElement("fieldlabel", null, "instant_feedback_answer_fixation");
3627 $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getQuestionBehaviourSettings()->getLockAnswerOnInstantFeedbackEnabled());
3628 $a_xml_writer->xmlEndTag("qtimetadatafield");
3629
3630 $a_xml_writer->xmlStartTag("qtimetadatafield");
3631 $a_xml_writer->xmlElement("fieldlabel", null, "enable_processing_time");
3632 $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getTestBehaviourSettings()->getProcessingTimeEnabled());
3633 $a_xml_writer->xmlEndTag("qtimetadatafield");
3634
3635 foreach ($this->getMarkSchema()->getMarkSteps() as $index => $mark) {
3636 $a_xml_writer->xmlStartTag("qtimetadatafield");
3637 $a_xml_writer->xmlElement("fieldlabel", null, "mark_step_$index");
3638 $a_xml_writer->xmlElement("fieldentry", null, sprintf(
3639 "<short>%s</short><official>%s</official><percentage>%.2f</percentage><passed>%d</passed>",
3640 $mark->getShortName(),
3641 $mark->getOfficialName(),
3642 $mark->getMinimumLevel(),
3643 $mark->getPassed()
3644 ));
3645 $a_xml_writer->xmlEndTag("qtimetadatafield");
3646 }
3647 $a_xml_writer->xmlEndTag("qtimetadata");
3648
3649 $page_id = $main_settings->getIntroductionSettings()->getIntroductionPageId();
3650 $introduction = $page_id !== null
3651 ? (new ilTestPage($page_id))->getXMLContent()
3652 : ilRTE::_replaceMediaObjectImageSrc($this->getIntroduction(), 0);
3653
3654 $a_xml_writer->xmlStartTag("objectives");
3655 $this->addQTIMaterial($a_xml_writer, $page_id, $introduction);
3656 $a_xml_writer->xmlEndTag("objectives");
3657
3658 if ($this->getInstantFeedbackSolution() == 1) {
3659 $attrs = [
3660 "solutionswitch" => "Yes"
3661 ];
3662 } else {
3663 $attrs = null;
3664 }
3665 $a_xml_writer->xmlElement("assessmentcontrol", $attrs, null);
3666
3667 if (strlen($this->getFinalStatement())) {
3668 $page_id = $main_settings->getFinishingSettings()->getConcludingRemarksPageId();
3669 $concluding_remarks = $page_id !== null
3670 ? (new ilTestPage($page_id))->getXMLContent()
3671 : ilRTE::_replaceMediaObjectImageSrc($this->getFinalStatement());
3672 // add qti presentation_material
3673 $a_xml_writer->xmlStartTag("presentation_material");
3674 $a_xml_writer->xmlStartTag("flow_mat");
3675 $this->addQTIMaterial($a_xml_writer, $page_id, $concluding_remarks);
3676 $a_xml_writer->xmlEndTag("flow_mat");
3677 $a_xml_writer->xmlEndTag("presentation_material");
3678 }
3679
3680 $attrs = [
3681 "ident" => "1"
3682 ];
3683 $a_xml_writer->xmlElement("section", $attrs, null);
3684 $a_xml_writer->xmlEndTag("assessment");
3685 $a_xml_writer->xmlEndTag("questestinterop");
3686
3687 $xml = $a_xml_writer->xmlDumpMem(false);
3688 return $xml;
3689 }
3690
3691 protected function buildIso8601PeriodForExportCompatibility(DateTimeImmutable $date_time): string
3692 {
3693 return $date_time->setTimezone(new DateTimeZone('UTC'))->format('\PY\Yn\Mj\D\TG\Hi\Ms\S');
3694 }
3695
3696 protected function buildDateTimeImmutableFromPeriod(?string $period): ?DateTimeImmutable
3697 {
3698 if ($period === null) {
3699 return null;
3700 }
3701 if (preg_match("/P(\d+)Y(\d+)M(\d+)DT(\d+)H(\d+)M(\d+)S/", $period, $matches)) {
3702 return new DateTimeImmutable(
3703 sprintf(
3704 "%02d-%02d-%02d %02d:%02d:%02d",
3705 $matches[1],
3706 $matches[2],
3707 $matches[3],
3708 $matches[4],
3709 $matches[5],
3710 $matches[6]
3711 ),
3712 new \DateTimeZone('UTC')
3713 );
3714 }
3715 return null;
3716 }
3717
3724 public function exportPagesXML(&$a_xml_writer, $a_inst, $a_target_dir, &$expLog): void
3725 {
3726 $this->mob_ids = [];
3727
3728 // PageObjects
3729 $expLog->write(date("[y-m-d H:i:s] ") . "Start Export Page Objects");
3730 $this->bench->start("ContentObjectExport", "exportPageObjects");
3731 $this->exportXMLPageObjects($a_xml_writer, $a_inst, $expLog);
3732 $this->bench->stop("ContentObjectExport", "exportPageObjects");
3733 $expLog->write(date("[y-m-d H:i:s] ") . "Finished Export Page Objects");
3734
3735 // MediaObjects
3736 $expLog->write(date("[y-m-d H:i:s] ") . "Start Export Media Objects");
3737 $this->bench->start("ContentObjectExport", "exportMediaObjects");
3738 $this->exportXMLMediaObjects($a_xml_writer, $a_inst, $a_target_dir, $expLog);
3739 $this->bench->stop("ContentObjectExport", "exportMediaObjects");
3740 $expLog->write(date("[y-m-d H:i:s] ") . "Finished Export Media Objects");
3741
3742 // FileItems
3743 $expLog->write(date("[y-m-d H:i:s] ") . "Start Export File Items");
3744 $this->bench->start("ContentObjectExport", "exportFileItems");
3745 $this->exportFileItems($a_target_dir, $expLog);
3746 $this->bench->stop("ContentObjectExport", "exportFileItems");
3747 $expLog->write(date("[y-m-d H:i:s] ") . "Finished Export File Items");
3748 }
3749
3755 public function modifyExportIdentifier($a_tag, $a_param, $a_value)
3756 {
3757 if ($a_tag == "Identifier" && $a_param == "Entry") {
3758 $a_value = ilUtil::insertInstIntoID($a_value);
3759 }
3760
3761 return $a_value;
3762 }
3763
3764
3771 public function exportXMLPageObjects(&$a_xml_writer, $inst, &$expLog)
3772 {
3773 foreach ($this->questions as $question_id) {
3774 $this->bench->start("ContentObjectExport", "exportPageObject");
3775 $expLog->write(date("[y-m-d H:i:s] ") . "Page Object " . $question_id);
3776
3777 $attrs = [];
3778 $a_xml_writer->xmlStartTag("PageObject", $attrs);
3779
3780
3781 // export xml to writer object
3782 $this->bench->start("ContentObjectExport", "exportPageObject_XML");
3783 $page_object = new ilAssQuestionPage($question_id);
3784 $page_object->buildDom();
3785 $page_object->insertInstIntoIDs((string) $inst);
3786 $mob_ids = $page_object->collectMediaObjects(false);
3787 $file_ids = ilPCFileList::collectFileItems($page_object, $page_object->getDomDoc());
3788 $xml = $page_object->getXMLFromDom(false, false, false, "", true);
3789 $xml = str_replace("&", "&amp;", $xml);
3790 $a_xml_writer->appendXML($xml);
3791 $page_object->freeDom();
3792 unset($page_object);
3793
3794 $this->bench->stop("ContentObjectExport", "exportPageObject_XML");
3795
3796 // collect media objects
3797 $this->bench->start("ContentObjectExport", "exportPageObject_CollectMedia");
3798 //$mob_ids = $page_obj->getMediaObjectIDs();
3799 foreach ($mob_ids as $mob_id) {
3800 $this->mob_ids[$mob_id] = $mob_id;
3801 }
3802 $this->bench->stop("ContentObjectExport", "exportPageObject_CollectMedia");
3803
3804 // collect all file items
3805 $this->bench->start("ContentObjectExport", "exportPageObject_CollectFileItems");
3806 //$file_ids = $page_obj->getFileItemIds();
3807 foreach ($file_ids as $file_id) {
3808 $this->file_ids[$file_id] = $file_id;
3809 }
3810 $this->bench->stop("ContentObjectExport", "exportPageObject_CollectFileItems");
3811
3812 $a_xml_writer->xmlEndTag("PageObject");
3813 //unset($page_obj);
3814
3815 $this->bench->stop("ContentObjectExport", "exportPageObject");
3816 }
3817 }
3818
3822 public function exportXMLMediaObjects(&$a_xml_writer, $a_inst, $a_target_dir, &$expLog)
3823 {
3824 foreach ($this->mob_ids as $mob_id) {
3825 $expLog->write(date("[y-m-d H:i:s] ") . "Media Object " . $mob_id);
3826 if (ilObjMediaObject::_exists((int) $mob_id)) {
3827 $target_dir = $a_target_dir . DIRECTORY_SEPARATOR . 'objects'
3828 . DIRECTORY_SEPARATOR . 'il_' . IL_INST_ID . '_mob_' . $mob_id;
3829 ilFileUtils::createDirectory($target_dir);
3830 $media_obj = new ilObjMediaObject((int) $mob_id);
3831 $media_obj->exportXML($a_xml_writer, (int) $a_inst);
3832 foreach ($media_obj->getMediaItems() as $item) {
3833 $stream = $item->getLocationStream();
3834 file_put_contents($target_dir . DIRECTORY_SEPARATOR . $item->getLocation(), $stream);
3835 $stream->close();
3836 }
3837 unset($media_obj);
3838 }
3839 }
3840 }
3841
3846 public function exportFileItems($target_dir, &$expLog)
3847 {
3848 foreach ($this->file_ids as $file_id) {
3849 $expLog->write(date("[y-m-d H:i:s] ") . "File Item " . $file_id);
3850 $file_dir = $target_dir . '/objects/il_' . IL_INST_ID . '_file_' . $file_id;
3851 ilFileUtils::makeDir($file_dir);
3852 $file_obj = new ilObjFile((int) $file_id, false);
3853 $source_file = $file_obj->getFile($file_obj->getVersion());
3854 if (!is_file($source_file)) {
3855 $source_file = $file_obj->getFile();
3856 }
3857 if (is_file($source_file)) {
3858 copy($source_file, $file_dir . '/' . $file_obj->getFileName());
3859 }
3860 unset($file_obj);
3861 }
3862 }
3863
3868 public function getImportMapping(): array
3869 {
3870 return [];
3871 }
3872
3875 public function onMarkSchemaSaved(): void
3876 {
3877 $this->saveCompleteStatus($this->question_set_config_factory->getQuestionSetConfig());
3878
3879 if ($this->participantDataExist()) {
3880 $this->recalculateScores(true);
3881 }
3882 }
3883
3887 public function marksEditable(): bool
3888 {
3889 $total = $this->evalTotalPersons();
3890 $results_summary_settings = $this->getScoreSettings()->getResultSummarySettings();
3891 if ($total === 0
3892 || $results_summary_settings->getScoreReporting()->isReportingEnabled() === false) {
3893 return true;
3894 }
3895
3896 if ($results_summary_settings->getScoreReporting() === ScoreReportingTypes::SCORE_REPORTING_DATE) {
3897 return $results_summary_settings->getReportingDate()
3898 >= new DateTimeImmutable('now', new DateTimeZone('UTC'));
3899 }
3900
3901 return false;
3902 }
3903
3913 public function saveAuthorToMetadata($author = "")
3914 {
3915 $path_to_lifecycle = $this->lo_metadata->paths()->custom()->withNextStep('lifeCycle')->get();
3916 $path_to_authors = $this->lo_metadata->paths()->authors();
3917
3918 $reader = $this->lo_metadata->read($this->getId(), 0, $this->getType(), $path_to_lifecycle);
3919 if (!is_null($reader->allData($path_to_lifecycle)->current())) {
3920 return;
3921 }
3922
3923 if ($author === '') {
3924 $author = $this->user->getFullname();
3925 }
3926 $this->lo_metadata->manipulate($this->getId(), 0, $this->getType())
3927 ->prepareCreateOrUpdate($path_to_authors, $author)
3928 ->execute();
3929 }
3930
3934 protected function doCreateMetaData(): void
3935 {
3936 $this->saveAuthorToMetadata();
3937 }
3938
3946 public function getAuthor(): string
3947 {
3948 $path_to_authors = $this->lo_metadata->paths()->authors();
3949 $author_data = $this->lo_metadata->read($this->getId(), 0, $this->getType(), $path_to_authors)
3950 ->allData($path_to_authors);
3951
3952 return $this->lo_metadata->dataHelper()->makePresentableAsList(', ', ...$author_data);
3953 }
3954
3962 public static function _lookupAuthor($obj_id): string
3963 {
3964 global $DIC;
3965
3966 $lo_metadata = $DIC->learningObjectMetadata();
3967
3968 $path_to_authors = $lo_metadata->paths()->authors();
3969 $author_data = $lo_metadata->read($obj_id, 0, "tst", $path_to_authors)
3970 ->allData($path_to_authors);
3971
3972 return $lo_metadata->dataHelper()->makePresentableAsList(',', ...$author_data);
3973 }
3974
3981 public static function _getAvailableTests($use_object_id = false): array
3982 {
3983 global $DIC;
3984 $ilUser = $DIC['ilUser'];
3985
3986 $result_array = [];
3987 $tests = array_slice(
3988 array_reverse(
3989 ilUtil::_getObjectsByOperations("tst", "write", $ilUser->getId(), PHP_INT_MAX)
3990 ),
3991 0,
3992 10000
3993 );
3994
3995 if (count($tests)) {
3996 $titles = ilObject::_prepareCloneSelection($tests, "tst");
3997 foreach ($tests as $ref_id) {
3998 if ($use_object_id) {
4000 $result_array[$obj_id] = $titles[$ref_id];
4001 } else {
4002 $result_array[$ref_id] = $titles[$ref_id];
4003 }
4004 }
4005 }
4006 return $result_array;
4007 }
4008
4017 public function cloneObject(int $target_id, int $copy_id = 0, bool $omit_tree = false): ?ilObject
4018 {
4019 $this->loadFromDb();
4020
4021 $new_obj = parent::cloneObject($target_id, $copy_id, $omit_tree);
4022 $new_obj->setTmpCopyWizardCopyId($copy_id);
4023 $this->cloneMetaData($new_obj);
4024
4025 $new_obj->saveToDb();
4026 $new_obj->addToNewsOnOnline(false, $new_obj->getObjectProperties()->getPropertyIsOnline()->getIsOnline());
4027 $this->getMainSettingsRepository()->store(
4028 $this->getMainSettings()->withTestId($new_obj->getTestId())
4029 ->withIntroductionSettings(
4030 $this->getMainSettings()->getIntroductionSettings()->withIntroductionPageId(
4031 $this->cloneIntroduction()
4032 )->withTestId($new_obj->getTestId())
4033 )->withFinishingSettings(
4034 $this->getMainSettings()->getFinishingSettings()->withConcludingRemarksPageId(
4035 $this->cloneConcludingRemarks()
4036 )->withTestId($new_obj->getTestId())
4037 )
4038 );
4039 $this->getScoreSettingsRepository()->store(
4040 $this->getScoreSettings()->withTestId($new_obj->getTestId())
4041 );
4042 $this->marks_repository->storeMarkSchema(
4043 $this->getMarkSchema()->withTestId($new_obj->getTestId())
4044 );
4045
4046 $new_obj->setTemplate($this->getTemplate());
4047
4048 // clone certificate
4049 $pathFactory = new ilCertificatePathFactory();
4050 $templateRepository = new ilCertificateTemplateDatabaseRepository($this->db);
4051
4052 $cloneAction = new ilCertificateCloneAction(
4053 $this->db,
4054 $pathFactory,
4055 $templateRepository,
4057 );
4058
4059 $cloneAction->cloneCertificate($this, $new_obj);
4060
4061 $this->question_set_config_factory->getQuestionSetConfig()->cloneQuestionSetRelatedData($new_obj);
4062 $new_obj->saveQuestionsToDb();
4063
4064 $skillLevelThresholdList = new ilTestSkillLevelThresholdList($this->db);
4065 $skillLevelThresholdList->setTestId($this->getTestId());
4066 $skillLevelThresholdList->loadFromDb();
4067 $skillLevelThresholdList->cloneListForTest($new_obj->getTestId());
4068
4069 $obj_settings = new ilLPObjSettings($this->getId());
4070 $obj_settings->cloneSettings($new_obj->getId());
4071
4072 if ($new_obj->getTestLogger()->isLoggingEnabled()) {
4073 $new_obj->getTestLogger()->logTestAdministrationInteraction(
4074 $new_obj->getTestLogger()->getInteractionFactory()->buildTestAdministrationInteraction(
4075 $new_obj->getRefId(),
4076 $this->user->getId(),
4077 TestAdministrationInteractionTypes::NEW_TEST_CREATED,
4078 []
4079 )
4080 );
4081 }
4082
4083 return $new_obj;
4084 }
4085
4086 public function getQuestionCount(): int
4087 {
4088 $num = 0;
4089
4090 if ($this->isRandomTest()) {
4091 $questionSetConfig = new ilTestRandomQuestionSetConfig(
4092 $this->tree,
4093 $this->db,
4094 $this->lng,
4095 $this->logger,
4096 $this->component_repository,
4097 $this,
4098 $this->questionrepository
4099 );
4100
4101 $questionSetConfig->loadFromDb();
4102
4103 if ($questionSetConfig->isQuestionAmountConfigurationModePerPool()) {
4104 $sourcePoolDefinitionList = new ilTestRandomQuestionSetSourcePoolDefinitionList(
4105 $this->db,
4106 $this,
4108 );
4109
4110 $sourcePoolDefinitionList->loadDefinitions();
4111
4112 if (is_int($sourcePoolDefinitionList->getQuestionAmount())) {
4113 $num = $sourcePoolDefinitionList->getQuestionAmount();
4114 }
4115 } elseif (is_int($questionSetConfig->getQuestionAmountPerTest())) {
4116 $num = $questionSetConfig->getQuestionAmountPerTest();
4117 }
4118 } else {
4119 $this->loadQuestions();
4120 $num = count($this->questions);
4121 }
4122
4123 return $num;
4124 }
4125
4127 {
4128 if ($this->isRandomTest()) {
4129 return $this->getQuestionCount();
4130 }
4131 return count($this->questions);
4132 }
4133
4141 public static function _getObjectIDFromTestID($test_id)
4142 {
4143 global $DIC;
4144 $ilDB = $DIC['ilDB'];
4145 $object_id = false;
4146 $result = $ilDB->queryF(
4147 "SELECT obj_fi FROM tst_tests WHERE test_id = %s",
4148 ['integer'],
4149 [$test_id]
4150 );
4151 if ($result->numRows()) {
4152 $row = $ilDB->fetchAssoc($result);
4153 $object_id = $row["obj_fi"];
4154 }
4155 return $object_id;
4156 }
4157
4165 public static function _getObjectIDFromActiveID($active_id)
4166 {
4167 global $DIC;
4168 $ilDB = $DIC['ilDB'];
4169 $object_id = false;
4170 $result = $ilDB->queryF(
4171 "SELECT tst_tests.obj_fi FROM tst_tests, tst_active WHERE tst_tests.test_id = tst_active.test_fi AND tst_active.active_id = %s",
4172 ['integer'],
4173 [$active_id]
4174 );
4175 if ($result->numRows()) {
4176 $row = $ilDB->fetchAssoc($result);
4177 $object_id = $row["obj_fi"];
4178 }
4179 return $object_id;
4180 }
4181
4189 public static function _getTestIDFromObjectID($object_id)
4190 {
4191 global $DIC;
4192 $ilDB = $DIC['ilDB'];
4193 $test_id = false;
4194 $result = $ilDB->queryF(
4195 "SELECT test_id FROM tst_tests WHERE obj_fi = %s",
4196 ['integer'],
4197 [$object_id]
4198 );
4199 if ($result->numRows()) {
4200 $row = $ilDB->fetchAssoc($result);
4201 $test_id = $row["test_id"];
4202 }
4203 return $test_id;
4204 }
4205
4214 public function getTextAnswer($active_id, $question_id, $pass = null): string
4215 {
4216 if (($active_id) && ($question_id)) {
4217 if ($pass === null) {
4218 $pass = assQuestion::_getSolutionMaxPass($question_id, $active_id);
4219 }
4220 if ($pass === null) {
4221 return '';
4222 }
4223 $query = $this->db->queryF(
4224 "SELECT value1 FROM tst_solutions WHERE active_fi = %s AND question_fi = %s AND pass = %s",
4225 ['integer', 'integer', 'integer'],
4226 [$active_id, $question_id, $pass]
4227 );
4228 $result = $this->db->fetchAll($query);
4229 if (count($result) == 1) {
4230 return $result[0]["value1"];
4231 }
4232 }
4233 return '';
4234 }
4235
4243 public function getQuestiontext($question_id): string
4244 {
4245 $res = "";
4246 if ($question_id) {
4247 $result = $this->db->queryF(
4248 "SELECT question_text FROM qpl_questions WHERE question_id = %s",
4249 ['integer'],
4250 [$question_id]
4251 );
4252 if ($result->numRows() == 1) {
4253 $row = $this->db->fetchAssoc($result);
4254 $res = $row["question_text"];
4255 }
4256 }
4257 return $res;
4258 }
4259
4261 {
4262 $participant_list = new ilTestParticipantList($this, $this->user, $this->lng, $this->db);
4263 $participant_list->initializeFromDbRows($this->getTestParticipants());
4264
4265 return $participant_list;
4266 }
4267
4274 public function &getInvitedUsers(int $user_id = 0, $order = "login, lastname, firstname"): array
4275 {
4276 $result_array = [];
4277
4278 if ($this->getAnonymity()) {
4279 if ($user_id !== 0) {
4280 $result = $this->db->queryF(
4281 "SELECT tst_active.active_id, tst_active.tries, usr_id, %s login, %s lastname, %s firstname, " .
4282 "tst_active.submitted test_finished, matriculation, COALESCE(tst_active.last_finished_pass, -1) <> tst_active.last_started_pass unfinished_passes FROM usr_data, tst_invited_user " .
4283 "LEFT JOIN tst_active ON tst_active.user_fi = tst_invited_user.user_fi AND tst_active.test_fi = tst_invited_user.test_fi " .
4284 "WHERE tst_invited_user.test_fi = %s and tst_invited_user.user_fi=usr_data.usr_id AND usr_data.usr_id=%s " .
4285 "ORDER BY $order",
4286 ['text', 'text', 'text', 'integer', 'integer'],
4287 ['', $this->lng->txt('anonymous'), '', $this->getTestId(), $user_id]
4288 );
4289 } else {
4290 $result = $this->db->queryF(
4291 "SELECT tst_active.active_id, tst_active.tries, usr_id, %s login, %s lastname, %s firstname, " .
4292 "tst_active.submitted test_finished, matriculation, COALESCE(tst_active.last_finished_pass, -1) <> tst_active.last_started_pass unfinished_passes FROM usr_data, tst_invited_user " .
4293 "LEFT JOIN tst_active ON tst_active.user_fi = tst_invited_user.user_fi AND tst_active.test_fi = tst_invited_user.test_fi " .
4294 "WHERE tst_invited_user.test_fi = %s and tst_invited_user.user_fi=usr_data.usr_id " .
4295 "ORDER BY $order",
4296 ['text', 'text', 'text', 'integer'],
4297 ['', $this->lng->txt('anonymous'), '', $this->getTestId()]
4298 );
4299 }
4300 } else {
4301 if ($user_id !== 0) {
4302 $result = $this->db->queryF(
4303 "SELECT tst_active.active_id, tst_active.tries, usr_id, login, lastname, firstname, " .
4304 "tst_active.submitted test_finished, matriculation, COALESCE(tst_active.last_finished_pass, -1) <> tst_active.last_started_pass unfinished_passes FROM usr_data, tst_invited_user " .
4305 "LEFT JOIN tst_active ON tst_active.user_fi = tst_invited_user.user_fi AND tst_active.test_fi = tst_invited_user.test_fi " .
4306 "WHERE tst_invited_user.test_fi = %s and tst_invited_user.user_fi=usr_data.usr_id AND usr_data.usr_id=%s " .
4307 "ORDER BY $order",
4308 ['integer', 'integer'],
4309 [$this->getTestId(), $user_id]
4310 );
4311 } else {
4312 $result = $this->db->queryF(
4313 "SELECT tst_active.active_id, tst_active.tries, usr_id, login, lastname, firstname, " .
4314 "tst_active.submitted test_finished, matriculation, COALESCE(tst_active.last_finished_pass, -1) <> tst_active.last_started_pass unfinished_passes FROM usr_data, tst_invited_user " .
4315 "LEFT JOIN tst_active ON tst_active.user_fi = tst_invited_user.user_fi AND tst_active.test_fi = tst_invited_user.test_fi " .
4316 "WHERE tst_invited_user.test_fi = %s and tst_invited_user.user_fi=usr_data.usr_id " .
4317 "ORDER BY $order",
4318 ['integer'],
4319 [$this->getTestId()]
4320 );
4321 }
4322 }
4323 $result_array = [];
4324 while ($row = $this->db->fetchAssoc($result)) {
4325 $result_array[$row['usr_id']] = $row;
4326 }
4327 return $result_array;
4328 }
4329
4330 public function getTestParticipants(): array
4331 {
4332 if ($this->getMainSettings()->getGeneralSettings()->getAnonymity()) {
4333 $query = "
4334 SELECT tst_active.active_id,
4335 tst_active.tries,
4336 tst_active.user_fi usr_id,
4337 %s login,
4338 %s lastname,
4339 %s firstname,
4340 tst_active.submitted test_finished,
4341 usr_data.matriculation,
4342 usr_data.active,
4343 tst_active.lastindex,
4344 COALESCE(tst_active.last_finished_pass, -1) <> tst_active.last_started_pass unfinished_passes
4345 FROM tst_active
4346 LEFT JOIN usr_data
4347 ON tst_active.user_fi = usr_data.usr_id
4348 WHERE tst_active.test_fi = %s
4349 ORDER BY usr_data.lastname
4350 ";
4351 $result = $this->db->queryF(
4352 $query,
4353 ['text', 'text', 'text', 'integer'],
4354 ['', $this->lng->txt("anonymous"), "", $this->getTestId()]
4355 );
4356 } else {
4357 $query = "
4358 SELECT tst_active.active_id,
4359 tst_active.tries,
4360 tst_active.user_fi usr_id,
4361 usr_data.login,
4362 usr_data.lastname,
4363 usr_data.firstname,
4364 tst_active.submitted test_finished,
4365 usr_data.matriculation,
4366 usr_data.active,
4367 tst_active.lastindex,
4368 COALESCE(tst_active.last_finished_pass, -1) <> tst_active.last_started_pass unfinished_passes
4369 FROM tst_active
4370 LEFT JOIN usr_data
4371 ON tst_active.user_fi = usr_data.usr_id
4372 WHERE tst_active.test_fi = %s
4373 ORDER BY usr_data.lastname
4374 ";
4375 $result = $this->db->queryF(
4376 $query,
4377 ['integer'],
4378 [$this->getTestId()]
4379 );
4380 }
4381 $data = [];
4382 while ($row = $this->db->fetchAssoc($result)) {
4383 $data[$row['active_id']] = $row;
4384 }
4385 foreach ($data as $index => $participant) {
4386 if (strlen(trim($participant["firstname"] . $participant["lastname"])) == 0) {
4387 $data[$index]["lastname"] = $this->lng->txt("deleted_user");
4388 }
4389 }
4390 return $data;
4391 }
4392
4393 public function getTestParticipantsForManualScoring($filter = null): array
4394 {
4395 if (!$this->getGlobalSettings()->isManualScoringEnabled()) {
4396 return [];
4397 }
4398
4399 $filtered_participants = [];
4400 foreach ($this->getTestParticipants() as $active_id => $participant) {
4401 if ($participant['tries'] > 0) {
4402 switch ($filter) {
4403 case 4:
4404 if ($this->test_man_scoring_done_helper->isDone((int) $active_id)) {
4405 $filtered_participants[$active_id] = $participant;
4406 }
4407 break;
4408 case 5:
4409 if (!$this->test_man_scoring_done_helper->isDone((int) $active_id)) {
4410 $filtered_participants[$active_id] = $participant;
4411 }
4412 break;
4413 default:
4414 $filtered_participants[$active_id] = $participant;
4415 }
4416 }
4417 }
4418 return $filtered_participants;
4419 }
4420
4427 public function getUserData($ids): array
4428 {
4429 if (!is_array($ids) || count($ids) == 0) {
4430 return [];
4431 }
4432
4433 if ($this->getAnonymity()) {
4434 $result = $this->db->queryF(
4435 "SELECT usr_id, %s login, %s lastname, %s firstname, client_ip clientip FROM usr_data WHERE " . $this->db->in('usr_id', $ids, false, 'integer') . " ORDER BY login",
4436 ['text', 'text', 'text'],
4437 ["", $this->lng->txt("anonymous"), ""]
4438 );
4439 } else {
4440 $result = $this->db->query("SELECT usr_id, login, lastname, firstname, client_ip clientip FROM usr_data WHERE " . $this->db->in('usr_id', $ids, false, 'integer') . " ORDER BY login");
4441 }
4442
4443 $result_array = [];
4444 while ($row = $this->db->fetchAssoc($result)) {
4445 $result_array[$row["usr_id"]] = $row;
4446 }
4447 return $result_array;
4448 }
4449
4450 public function getGroupData($ids): array
4451 {
4452 if (!is_array($ids) || count($ids) == 0) {
4453 return [];
4454 }
4455 $result = [];
4456 foreach ($ids as $ref_id) {
4458 $result[$ref_id] = ["ref_id" => $ref_id, "title" => ilObject::_lookupTitle($obj_id), "description" => ilObject::_lookupDescription($obj_id)];
4459 }
4460 return $result;
4461 }
4462
4463 public function getRoleData($ids): array
4464 {
4465 if (!is_array($ids) || count($ids) == 0) {
4466 return [];
4467 }
4468 $result = [];
4469 foreach ($ids as $obj_id) {
4470 $result[$obj_id] = ["obj_id" => $obj_id, "title" => ilObject::_lookupTitle($obj_id), "description" => ilObject::_lookupDescription($obj_id)];
4471 }
4472 return $result;
4473 }
4474
4481 public function inviteUser($user_id, $client_ip = "")
4482 {
4483 $this->db->manipulateF(
4484 "DELETE FROM tst_invited_user WHERE test_fi = %s AND user_fi = %s",
4485 ['integer', 'integer'],
4486 [$this->getTestId(), $user_id]
4487 );
4488 $this->db->manipulateF(
4489 "INSERT INTO tst_invited_user (test_fi, user_fi, ip_range_from, ip_range_to, tstamp) VALUES (%s, %s, %s, %s, %s)",
4490 ['integer', 'integer', 'text', 'text', 'integer'],
4491 [$this->getTestId(), $user_id, (strlen($client_ip)) ? $client_ip : null, (strlen($client_ip)) ? $client_ip : null,time()]
4492 );
4493 }
4494
4500 public static function _getSolvedQuestions($active_id, $question_fi = null): array
4501 {
4502 global $DIC;
4503 $ilDB = $DIC['ilDB'];
4504 if (is_numeric($question_fi)) {
4505 $result = $ilDB->queryF(
4506 "SELECT question_fi, solved FROM tst_qst_solved WHERE active_fi = %s AND question_fi=%s",
4507 ['integer', 'integer'],
4508 [$active_id, $question_fi]
4509 );
4510 } else {
4511 $result = $ilDB->queryF(
4512 "SELECT question_fi, solved FROM tst_qst_solved WHERE active_fi = %s",
4513 ['integer'],
4514 [$active_id]
4515 );
4516 }
4517 $result_array = [];
4518 while ($row = $ilDB->fetchAssoc($result)) {
4519 $result_array[$row["question_fi"]] = $row;
4520 }
4521 return $result_array;
4522 }
4523
4524
4528 public function setQuestionSetSolved($value, $question_id, $user_id)
4529 {
4530 $active_id = $this->getActiveIdOfUser($user_id);
4531 $this->db->manipulateF(
4532 "DELETE FROM tst_qst_solved WHERE active_fi = %s AND question_fi = %s",
4533 ['integer', 'integer'],
4534 [$active_id, $question_id]
4535 );
4536 $this->db->manipulateF(
4537 "INSERT INTO tst_qst_solved (solved, question_fi, active_fi) VALUES (%s, %s, %s)",
4538 ['integer', 'integer', 'integer'],
4539 [$value, $question_id, $active_id]
4540 );
4541 }
4542
4546 public function isTestFinished($active_id): bool
4547 {
4548 $result = $this->db->queryF(
4549 "SELECT submitted FROM tst_active WHERE active_id=%s AND submitted=%s",
4550 ['integer', 'integer'],
4551 [$active_id, 1]
4552 );
4553 return $result->numRows() == 1;
4554 }
4555
4559 public function isActiveTestSubmitted($user_id = null): bool
4560 {
4561 if (!is_numeric($user_id)) {
4562 $user_id = $this->user->getId();
4563 }
4564
4565 $result = $this->db->queryF(
4566 "SELECT submitted FROM tst_active WHERE test_fi=%s AND user_fi=%s AND submitted=%s",
4567 ['integer', 'integer', 'integer'],
4568 [$this->getTestId(), $user_id, 1]
4569 );
4570 return $result->numRows() == 1;
4571 }
4572
4576 public function hasNrOfTriesRestriction(): bool
4577 {
4578 return $this->getNrOfTries() != 0;
4579 }
4580
4581
4587 public function isNrOfTriesReached($tries): bool
4588 {
4589 return $tries >= $this->getNrOfTries();
4590 }
4591
4592
4600 public function getAllTestResults($participants): array
4601 {
4602 $results = [];
4603 $row = [
4604 "user_id" => $this->lng->txt("user_id"),
4605 "matriculation" => $this->lng->txt("matriculation"),
4606 "lastname" => $this->lng->txt("lastname"),
4607 "firstname" => $this->lng->txt("firstname"),
4608 "login" => $this->lng->txt("login"),
4609 "reached_points" => $this->lng->txt("tst_reached_points"),
4610 "max_points" => $this->lng->txt("tst_maximum_points"),
4611 "percent_value" => $this->lng->txt("tst_percent_solved"),
4612 "mark" => $this->lng->txt("tst_mark"),
4613 "passed" => $this->lng->txt("tst_mark_passed"),
4614 ];
4615 $results[] = $row;
4616 if (count($participants)) {
4617 foreach ($participants as $active_id => $user_rec) {
4618 $mark = '';
4619 $row = [];
4620 $reached_points = 0;
4621 $max_points = 0;
4622 $pass = ilObjTest::_getResultPass($active_id);
4623 // abort if no valid pass can be found
4624 if (!is_int($pass)) {
4625 continue;
4626 }
4627 foreach ($this->questions as $value) {
4628 $question = ilObjTest::_instanciateQuestion($value);
4629 if (is_object($question)) {
4630 $max_points += $question->getMaximumPoints();
4631 $reached_points += $question->getReachedPoints($active_id, $pass);
4632 }
4633 }
4634 if ($max_points > 0) {
4635 $percentvalue = $reached_points / $max_points;
4636 if ($percentvalue < 0) {
4637 $percentvalue = 0.0;
4638 }
4639 } else {
4640 $percentvalue = 0;
4641 }
4642 $mark_obj = $this->getMarkSchema()->getMatchingMark($percentvalue * 100);
4643 $passed = "";
4644 if ($mark_obj !== null) {
4645 $mark = $mark_obj->getOfficialName();
4646 }
4647 if ($this->getAnonymity()) {
4648 $user_rec['firstname'] = "";
4649 $user_rec['lastname'] = $this->lng->txt("anonymous");
4650 }
4651 $results[] = [
4652 "user_id" => $user_rec['usr_id'],
4653 "matriculation" => $user_rec['matriculation'],
4654 "lastname" => $user_rec['lastname'],
4655 "firstname" => $user_rec['firstname'],
4656 "login" => $user_rec['login'],
4657 "reached_points" => $reached_points,
4658 "max_points" => $max_points,
4659 "percent_value" => $percentvalue,
4660 "mark" => $mark,
4661 "passed" => $user_rec['passed'] ? '1' : '0',
4662 ];
4663 }
4664 }
4665 return $results;
4666 }
4667
4676 public static function _getPass($active_id): int
4677 {
4678 global $DIC;
4679 $ilDB = $DIC['ilDB'];
4680 $result = $ilDB->queryF(
4681 "SELECT tries FROM tst_active WHERE active_id = %s",
4682 ['integer'],
4683 [$active_id]
4684 );
4685 if ($result->numRows()) {
4686 $row = $ilDB->fetchAssoc($result);
4687 return $row["tries"];
4688 } else {
4689 return 0;
4690 }
4691 }
4692
4702 public static function _getMaxPass($active_id): ?int
4703 {
4704 global $DIC;
4705 $ilDB = $DIC['ilDB'];
4706 $result = $ilDB->queryF(
4707 "SELECT MAX(pass) maxpass FROM tst_pass_result WHERE active_fi = %s",
4708 ['integer'],
4709 [$active_id]
4710 );
4711
4712 if ($result->numRows()) {
4713 $row = $ilDB->fetchAssoc($result);
4714 return $row["maxpass"];
4715 }
4716
4717 return null;
4718 }
4719
4725 public static function _getBestPass($active_id): ?int
4726 {
4727 global $DIC;
4728 $ilDB = $DIC['ilDB'];
4729
4730 $result = $ilDB->queryF(
4731 "SELECT * FROM tst_pass_result WHERE active_fi = %s",
4732 ['integer'],
4733 [$active_id]
4734 );
4735
4736 if (!$result->numRows()) {
4737 return null;
4738 }
4739
4740 $bestrow = null;
4741 $bestfactor = 0.0;
4742 while ($row = $ilDB->fetchAssoc($result)) {
4743 if ($row["maxpoints"] > 0.0) {
4744 $factor = (float) ($row["points"] / $row["maxpoints"]);
4745 } else {
4746 $factor = 0.0;
4747 }
4748 if ($factor === 0.0 && $bestfactor === 0.0
4749 || $factor > $bestfactor) {
4750 $bestrow = $row;
4751 $bestfactor = $factor;
4752 }
4753 }
4754
4755 if (is_array($bestrow)) {
4756 return $bestrow["pass"];
4757 }
4758
4759 return null;
4760 }
4761
4770 public static function _getResultPass($active_id): ?int
4771 {
4772 $counted_pass = null;
4773 if (ilObjTest::_getPassScoring($active_id) == self::SCORE_BEST_PASS) {
4774 $counted_pass = ilObjTest::_getBestPass($active_id);
4775 } else {
4776 $counted_pass = ilObjTest::_getMaxPass($active_id);
4777 }
4778 return $counted_pass;
4779 }
4780
4790 public function getAnsweredQuestionCount($active_id, $pass = null): int
4791 {
4792 if ($this->isRandomTest()) {
4793 $this->loadQuestions($active_id, $pass);
4794 }
4795 $workedthrough = 0;
4796 foreach ($this->questions as $value) {
4797 if ($this->questionrepository->lookupResultRecordExist($active_id, $value, $pass)) {
4798 $workedthrough += 1;
4799 }
4800 }
4801 return $workedthrough;
4802 }
4803
4810 public static function lookupPassResultsUpdateTimestamp($active_id, $pass): int
4811 {
4812 global $DIC;
4813 $ilDB = $DIC['ilDB'];
4814
4815 if (is_null($pass)) {
4816 $pass = 0;
4817 }
4818
4819 $query = "
4820 SELECT tst_pass_result.tstamp pass_res_tstamp,
4821 tst_test_result.tstamp quest_res_tstamp
4822
4823 FROM tst_pass_result
4824
4825 LEFT JOIN tst_test_result
4826 ON tst_test_result.active_fi = tst_pass_result.active_fi
4827 AND tst_test_result.pass = tst_pass_result.pass
4828
4829 WHERE tst_pass_result.active_fi = %s
4830 AND tst_pass_result.pass = %s
4831
4832 ORDER BY tst_test_result.tstamp DESC
4833 ";
4834
4835 $result = $ilDB->queryF(
4836 $query,
4837 ['integer', 'integer'],
4838 [$active_id, $pass]
4839 );
4840
4841 while ($row = $ilDB->fetchAssoc($result)) {
4842 if ($row['quest_res_tstamp']) {
4843 return $row['quest_res_tstamp'];
4844 }
4845
4846 return $row['pass_res_tstamp'];
4847 }
4848
4849 return 0;
4850 }
4851
4860 public function isExecutable($test_session, $user_id, $allow_pass_increase = false): array
4861 {
4862 $result = [
4863 "executable" => true,
4864 "errormessage" => ""
4865 ];
4866
4867 if (!$this->getObjectProperties()->getPropertyIsOnline()->getIsOnline()) {
4868 $result["executable"] = false;
4869 $result["errormessage"] = $this->lng->txt('autosave_failed') . ': ' . $this->lng->txt('offline');
4870 return $result;
4871 }
4872
4873 if (!$this->startingTimeReached()) {
4874 $result["executable"] = false;
4875 $result["errormessage"] = sprintf($this->lng->txt("detail_starting_time_not_reached"), ilDatePresentation::formatDate(new ilDateTime($this->getStartingTime(), IL_CAL_UNIX)));
4876 return $result;
4877 }
4878 if ($this->endingTimeReached()) {
4879 $result["executable"] = false;
4880 $result["errormessage"] = sprintf($this->lng->txt("detail_ending_time_reached"), ilDatePresentation::formatDate(new ilDateTime($this->getEndingTime(), IL_CAL_UNIX)));
4881 return $result;
4882 }
4883
4884 $active_id = $this->getActiveIdOfUser($user_id);
4885
4886 if ($this->getEnableProcessingTime()
4887 && $active_id > 0
4888 && ($starting_time = $this->getStartingTimeOfUser($active_id)) !== false
4889 && $this->isMaxProcessingTimeReached($starting_time, $active_id)) {
4890 $result["executable"] = false;
4891 $result["errormessage"] = $this->lng->txt("detail_max_processing_time_reached");
4892 return $result;
4893 }
4894
4895 $testPassesSelector = new ilTestPassesSelector($this->db, $this);
4896 $testPassesSelector->setActiveId($active_id);
4897 $testPassesSelector->setLastFinishedPass($test_session->getLastFinishedPass());
4898
4899 if ($this->hasNrOfTriesRestriction() && ($active_id > 0)) {
4900 $closedPasses = $testPassesSelector->getClosedPasses();
4901
4902 if (count($closedPasses) >= $this->getNrOfTries()) {
4903 $result["executable"] = false;
4904 $result["errormessage"] = $this->lng->txt("maximum_nr_of_tries_reached");
4905 return $result;
4906 }
4907
4908 if ($this->isBlockPassesAfterPassedEnabled() && !$testPassesSelector->openPassExists()) {
4909 if ($this->test_result_repository->isPassed($user_id, $this->getId())) {
4910 $result['executable'] = false;
4911 $result['errormessage'] = $this->lng->txt("tst_addit_passes_blocked_after_passed_msg");
4912 return $result;
4913 }
4914 }
4915 }
4916
4917 $next_pass_allowed_timestamp = 0;
4918 if (!$this->isNextPassAllowed($testPassesSelector, $next_pass_allowed_timestamp)) {
4919 $date = ilDatePresentation::formatDate(new ilDateTime($next_pass_allowed_timestamp, IL_CAL_UNIX));
4920
4921 $result['executable'] = false;
4922 $result['errormessage'] = sprintf($this->lng->txt('wait_for_next_pass_hint_msg'), $date);
4923 return $result;
4924 }
4925 return $result;
4926 }
4927
4928 public function isNextPassAllowed(ilTestPassesSelector $testPassesSelector, int &$next_pass_allowed_timestamp): bool
4929 {
4930 $waiting_between_passes = $this->getMainSettings()->getTestBehaviourSettings()->getPassWaiting();
4931 $last_finished_pass_timestamp = $testPassesSelector->getLastFinishedPassTimestamp();
4932
4933 if (
4934 $this->getMainSettings()->getTestBehaviourSettings()->getPassWaitingEnabled()
4935 && ($waiting_between_passes !== '')
4936 && ($testPassesSelector->getLastFinishedPass() !== null)
4937 && ($last_finished_pass_timestamp !== null)
4938 ) {
4939 $time_values = explode(':', $waiting_between_passes);
4940 $next_pass_allowed_timestamp = strtotime('+ ' . $time_values[0] . ' Days + ' . $time_values[1] . ' Hours' . $time_values[2] . ' Minutes', $last_finished_pass_timestamp);
4941 return (time() > $next_pass_allowed_timestamp);
4942 }
4943
4944 return true;
4945 }
4946
4947 public function canShowTestResults(ilTestSession $test_session): bool
4948 {
4949 $pass_selector = new ilTestPassesSelector($this->db, $this);
4950
4951 $pass_selector->setActiveId($test_session->getActiveId());
4952 $pass_selector->setLastFinishedPass($test_session->getLastFinishedPass());
4953
4954 return $pass_selector->hasReportablePasses();
4955 }
4956
4957 public function hasAnyTestResult(ilTestSession $test_session): bool
4958 {
4959 $pass_selector = new ilTestPassesSelector($this->db, $this);
4960
4961 $pass_selector->setActiveId($test_session->getActiveId());
4962 $pass_selector->setLastFinishedPass($test_session->getLastFinishedPass());
4963
4964 return $pass_selector->hasExistingPasses();
4965 }
4966
4974 public function getStartingTimeOfUser($active_id, $pass = null)
4975 {
4976 if ($active_id < 1) {
4977 return false;
4978 }
4979 if ($pass === null) {
4980 $pass = ($this->getResetProcessingTime()) ? self::_getPass($active_id) : 0;
4981 }
4982 $result = $this->db->queryF(
4983 "SELECT tst_times.started FROM tst_times WHERE tst_times.active_fi = %s AND tst_times.pass = %s ORDER BY tst_times.started",
4984 ['integer', 'integer'],
4985 [$active_id, $pass]
4986 );
4987 if ($result->numRows()) {
4988 $row = $this->db->fetchAssoc($result);
4989 if (preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["started"], $matches)) {
4990 return mktime(
4991 (int) $matches[4],
4992 (int) $matches[5],
4993 (int) $matches[6],
4994 (int) $matches[2],
4995 (int) $matches[3],
4996 (int) $matches[1]
4997 );
4998 } else {
4999 return time();
5000 }
5001 } else {
5002 return time();
5003 }
5004 }
5005
5012 public function isMaxProcessingTimeReached(int $starting_time, int $active_id): bool
5013 {
5014 if (!$this->getEnableProcessingTime()) {
5015 return false;
5016 }
5017
5018 $processing_time = $this->getProcessingTimeInSeconds($active_id);
5019 $now = time();
5020 if ($now > ($starting_time + $processing_time)) {
5021 return true;
5022 }
5023
5024 return false;
5025 }
5026
5027 public function getTestQuestions(): array
5028 {
5029 $tags_trafo = $this->refinery->string()->stripTags();
5030
5031 $query = "
5032 SELECT questions.*,
5033 questtypes.type_tag,
5034 tstquest.sequence,
5035 origquest.obj_fi orig_obj_fi
5036
5037 FROM qpl_questions questions
5038
5039 INNER JOIN qpl_qst_type questtypes
5040 ON questtypes.question_type_id = questions.question_type_fi
5041
5042 INNER JOIN tst_test_question tstquest
5043 ON tstquest.question_fi = questions.question_id
5044
5045 LEFT JOIN qpl_questions origquest
5046 ON origquest.question_id = questions.original_id
5047
5048 WHERE tstquest.test_fi = %s
5049
5050 ORDER BY tstquest.sequence
5051 ";
5052
5053 $query_result = $this->db->queryF(
5054 $query,
5055 ['integer'],
5056 [$this->getTestId()]
5057 );
5058
5059 $questions = [];
5060
5061 while ($row = $this->db->fetchAssoc($query_result)) {
5062 $row['title'] = $tags_trafo->transform($row['title']);
5063 $row['description'] = $tags_trafo->transform($row['description'] !== '' && $row['description'] !== null ? $row['description'] : '&nbsp;');
5064 $row['author'] = $tags_trafo->transform($row['author']);
5065
5066 $questions[] = $row;
5067 }
5068
5069 return $questions;
5070 }
5071
5072 public function isTestQuestion(int $question_id): bool
5073 {
5074 foreach ($this->getTestQuestions() as $questionData) {
5075 if ($questionData['question_id'] != $question_id) {
5076 continue;
5077 }
5078
5079 return true;
5080 }
5081
5082 return false;
5083 }
5084
5085 public function checkQuestionParent(int $question_id): bool
5086 {
5087 $row = $this->db->fetchAssoc($this->db->queryF(
5088 "SELECT COUNT(question_id) cnt FROM qpl_questions WHERE question_id = %s AND obj_fi = %s",
5089 ['integer', 'integer'],
5090 [$question_id, $this->getId()]
5091 ));
5092
5093 return (bool) $row['cnt'];
5094 }
5095
5096 public function getFixedQuestionSetTotalPoints(): float
5097 {
5098 $points = 0;
5099
5100 foreach ($this->getTestQuestions() as $question_data) {
5101 $points += $question_data['points'];
5102 }
5103
5104 return $points;
5105 }
5106
5110 public function getPotentialRandomTestQuestions(): array
5111 {
5112 $query = "
5113 SELECT questions.*,
5114 questtypes.type_tag,
5115 origquest.obj_fi orig_obj_fi
5116
5117 FROM qpl_questions questions
5118
5119 INNER JOIN qpl_qst_type questtypes
5120 ON questtypes.question_type_id = questions.question_type_fi
5121
5122 INNER JOIN tst_rnd_cpy tstquest
5123 ON tstquest.qst_fi = questions.question_id
5124
5125 LEFT JOIN qpl_questions origquest
5126 ON origquest.question_id = questions.original_id
5127
5128 WHERE tstquest.tst_fi = %s
5129 ";
5130
5131 $query_result = $this->db->queryF(
5132 $query,
5133 ['integer'],
5134 [$this->getTestId()]
5135 );
5136
5137 return $this->db->fetchAll($query_result);
5138 }
5139
5140 public function getShuffleQuestions(): bool
5141 {
5142 return $this->getMainSettings()->getQuestionBehaviourSettings()->getShuffleQuestions();
5143 }
5144
5157 {
5158 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getUsrPassOverviewMode();
5159 }
5160
5161 public function getListOfQuestions(): bool
5162 {
5163 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getQuestionListEnabled();
5164 }
5165
5166 public function getUsrPassOverviewEnabled(): bool
5167 {
5168 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getUsrPassOverviewEnabled();
5169 }
5170
5171 public function getListOfQuestionsStart(): bool
5172 {
5173 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getShownQuestionListAtBeginning();
5174 }
5175
5176 public function getListOfQuestionsEnd(): bool
5177 {
5178 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getShownQuestionListAtEnd();
5179 }
5180
5181 public function getListOfQuestionsDescription(): bool
5182 {
5183 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getShowDescriptionInQuestionList();
5184 }
5185
5189 public function getShowPassDetails(): bool
5190 {
5191 return $this->getScoreSettings()->getResultDetailsSettings()->getShowPassDetails();
5192 }
5193
5197 public function getShowSolutionPrintview(): bool
5198 {
5199 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionPrintview();
5200 }
5204 public function canShowSolutionPrintview($user_id = null): bool
5205 {
5206 return $this->getShowSolutionPrintview();
5207 }
5208
5212 public function getShowSolutionFeedback(): bool
5213 {
5214 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionFeedback();
5215 }
5216
5220 public function getShowSolutionAnswersOnly(): bool
5221 {
5222 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionAnswersOnly();
5223 }
5224
5228 public function getShowSolutionSignature(): bool
5229 {
5230 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionSignature();
5231 }
5232
5236 public function getShowSolutionSuggested(): bool
5237 {
5238 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionSuggested();
5239 }
5240
5245 public function getShowSolutionListComparison(): bool
5246 {
5247 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionListComparison();
5248 }
5249
5250 public function getShowSolutionListOwnAnswers(): bool
5251 {
5252 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionListOwnAnswers();
5253 }
5254
5258 public static function _getUserIdFromActiveId(int $active_id): int
5259 {
5260 global $DIC;
5261 $ilDB = $DIC['ilDB'];
5262 $result = $ilDB->queryF(
5263 "SELECT user_fi FROM tst_active WHERE active_id = %s",
5264 ['integer'],
5265 [$active_id]
5266 );
5267 if ($result->numRows()) {
5268 $row = $ilDB->fetchAssoc($result);
5269 return $row["user_fi"];
5270 } else {
5271 return -1;
5272 }
5273 }
5274
5275 public function _getLastAccess(int $active_id): string
5276 {
5277 $result = $this->db->queryF(
5278 "SELECT finished FROM tst_times WHERE active_fi = %s ORDER BY finished DESC",
5279 ['integer'],
5280 [$active_id]
5281 );
5282 if ($result->numRows()) {
5283 $row = $this->db->fetchAssoc($result);
5284 return $row["finished"];
5285 }
5286 return "";
5287 }
5288
5289 public static function lookupLastTestPassAccess(int $active_id, int $pass_index): ?int
5290 {
5292 global $DIC;
5293 $ilDB = $DIC['ilDB'];
5294
5295 $query = "
5296 SELECT MAX(tst_times.tstamp) as last_pass_access
5297 FROM tst_times
5298 WHERE active_fi = %s
5299 AND pass = %s
5300 ";
5301
5302 $res = $ilDB->queryF(
5303 $query,
5304 ['integer', 'integer'],
5305 [$active_id, $pass_index]
5306 );
5307
5308 while ($row = $ilDB->fetchAssoc($res)) {
5309 return $row['last_pass_access'];
5310 }
5311
5312 return null;
5313 }
5314
5322 public function isHTML($a_text): bool
5323 {
5324 if (preg_match("/<[^>]*?>/", $a_text)) {
5325 return true;
5326 } else {
5327 return false;
5328 }
5329 }
5330
5337 public function qtiMaterialToArray($a_material): array
5338 {
5339 $result = '';
5340 $mobs = [];
5341 for ($i = 0; $i < $a_material->getMaterialCount(); $i++) {
5342 $material = $a_material->getMaterial($i);
5343 if ($material['type'] === 'mattext') {
5344 $result .= $material['material']->getContent();
5345 }
5346 if ($material['type'] === 'matimage') {
5347 $matimage = $material['material'];
5348 if (preg_match('/(il_([0-9]+)_mob_([0-9]+))/', $matimage->getLabel(), $matches)) {
5349 $mobs[] = [
5350 'mob' => $matimage->getLabel(),
5351 'uri' => $matimage->getUri()
5352 ];
5353 }
5354 }
5355 }
5356
5357 $decoded_result = base64_decode($result);
5358 if (str_starts_with($decoded_result, '<PageObject>')) {
5359 $result = $decoded_result;
5360 }
5361
5362 $this->logger->info(print_r(ilSession::get('import_mob_xhtml'), true));
5363 return [
5364 'text' => $result,
5365 'mobs' => $mobs
5366 ];
5367 }
5368
5369 public function addQTIMaterial(ilXmlWriter &$xml_writer, ?int $page_id, string $material = ''): void
5370 {
5371 $xml_writer->xmlStartTag('material');
5372 $attrs = [
5373 'texttype' => 'text/plain'
5374 ];
5375 $file_ids = [];
5376 $mobs = [];
5377 if ($page_id !== null) {
5378 $attrs['texttype'] = 'text/xml';
5379 $mobs = ilObjMediaObject::_getMobsOfObject('tst:pg', $page_id);
5380 $page_object = new ilTestPage($page_id);
5381 $page_object->buildDom();
5382 $page_object->insertInstIntoIDs((string) IL_INST_ID);
5383 $material = base64_encode($page_object->getXMLFromDom());
5384 $file_ids = ilPCFileList::collectFileItems($page_object, $page_object->getDomDoc());
5385 foreach ($file_ids as $file_id) {
5386 $this->file_ids[] = (int) $file_id;
5387 };
5388 $mob_string = 'il_' . IL_INST_ID . '_mob_';
5389 } elseif ($this->isHTML($material)) {
5390 $attrs['texttype'] = 'text/xhtml';
5391 $mobs = ilObjMediaObject::_getMobsOfObject('tst:html', $this->getId());
5392 $mob_string = 'mm_';
5393 }
5394
5395 $xml_writer->xmlElement('mattext', $attrs, $material);
5396 foreach ($mobs as $mob) {
5397 $mob_id_string = (string) $mob;
5398 $moblabel = 'il_' . IL_INST_ID . '_mob_' . $mob_id_string;
5399 if (strpos($material, $mob_string . $mob_id_string) !== false) {
5400 if (ilObjMediaObject::_exists($mob)) {
5401 $mob_obj = new ilObjMediaObject($mob);
5402 $imgattrs = [
5403 'label' => $moblabel,
5404 'uri' => 'objects/' . 'il_' . IL_INST_ID . '_mob_' . $mob_id_string . '/' . $mob_obj->getTitle()
5405 ];
5406 }
5407 $xml_writer->xmlElement('matimage', $imgattrs, null);
5408 }
5409 }
5410 $xml_writer->xmlEndTag('material');
5411 }
5412
5419 public function prepareTextareaOutput($txt_output, $prepare_for_latex_output = false, $omitNl2BrWhenTextArea = false)
5420 {
5421 if ($txt_output == null) {
5422 $txt_output = '';
5423 }
5425 $txt_output,
5426 $prepare_for_latex_output,
5427 $omitNl2BrWhenTextArea
5428 );
5429 }
5430
5431 public function getAnonymity(): bool
5432 {
5433 return $this->getMainSettings()->getGeneralSettings()->getAnonymity();
5434 }
5435
5436
5437 public static function _lookupAnonymity($a_obj_id): int
5438 {
5439 global $DIC;
5440 $ilDB = $DIC['ilDB'];
5441
5442 $result = $ilDB->queryF(
5443 "SELECT anonymity FROM tst_tests WHERE obj_fi = %s",
5444 ['integer'],
5445 [$a_obj_id]
5446 );
5447 while ($row = $ilDB->fetchAssoc($result)) {
5448 return (int) $row['anonymity'];
5449 }
5450 return 0;
5451 }
5452
5453 public function getShowCancel(): bool
5454 {
5455 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getSuspendTestAllowed();
5456 }
5457
5458 public function getShowMarker(): bool
5459 {
5460 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getQuestionMarkingEnabled();
5461 }
5462
5463 public function getFixedParticipants(): bool
5464 {
5465 return $this->getMainSettings()->getAccessSettings()->getFixedParticipants();
5466 }
5467
5468 public function lookupQuestionSetTypeByActiveId(int $active_id): ?string
5469 {
5470 $query = "
5471 SELECT tst_tests.question_set_type
5472 FROM tst_active
5473 INNER JOIN tst_tests
5474 ON tst_active.test_fi = tst_tests.test_id
5475 WHERE tst_active.active_id = %s
5476 ";
5477
5478 $res = $this->db->queryF($query, ['integer'], [$active_id]);
5479
5480 while ($row = $this->db->fetchAssoc($res)) {
5481 return $row['question_set_type'];
5482 }
5483
5484 return null;
5485 }
5486
5497 public function userLookupFullName($user_id, $overwrite_anonymity = false, $sorted_order = false, $suffix = ""): string
5498 {
5499 if ($this->getAnonymity() && !$overwrite_anonymity) {
5500 return $this->lng->txt("anonymous") . $suffix;
5501 } else {
5503 if (strlen($uname["firstname"] . $uname["lastname"]) == 0) {
5504 $uname["firstname"] = $this->lng->txt("deleted_user");
5505 }
5506 if ($sorted_order) {
5507 return trim($uname["lastname"] . ", " . $uname["firstname"]) . $suffix;
5508 } else {
5509 return trim($uname["firstname"] . " " . $uname["lastname"]) . $suffix;
5510 }
5511 }
5512 }
5513
5519 public function getAvailableDefaults(): array
5520 {
5521 $result = $this->db->queryF(
5522 "SELECT * FROM tst_test_defaults WHERE user_fi = %s ORDER BY name ASC",
5523 ['integer'],
5524 [$this->user->getId()]
5525 );
5526 $defaults = [];
5527 while ($row = $this->db->fetchAssoc($result)) {
5528 $defaults[$row["test_defaults_id"]] = $row;
5529 }
5530 return $defaults;
5531 }
5532
5533 public function getTestDefaults($test_defaults_id): ?array
5534 {
5535 $result = $this->db->queryF(
5536 "SELECT * FROM tst_test_defaults WHERE test_defaults_id = %s",
5537 ['integer'],
5538 [$test_defaults_id]
5539 );
5540 if ($result->numRows() == 1) {
5541 $row = $this->db->fetchAssoc($result);
5542 return $row;
5543 } else {
5544 return null;
5545 }
5546 }
5547
5548 public function deleteDefaults($test_default_id)
5549 {
5550 $this->db->manipulateF(
5551 "DELETE FROM tst_test_defaults WHERE test_defaults_id = %s",
5552 ['integer'],
5553 [$test_default_id]
5554 );
5555 }
5556
5563 public function addDefaults($a_name)
5564 {
5565 $main_settings = $this->getMainSettings();
5566 $score_settings = $this->getScoreSettings();
5567 $testsettings = [
5568 'questionSetType' => $main_settings->getGeneralSettings()->getQuestionSetType(),
5569 'Anonymity' => (int) $main_settings->getGeneralSettings()->getAnonymity(),
5570
5571 'IntroEnabled' => (int) $main_settings->getIntroductionSettings()->getIntroductionEnabled(),
5572 'ExamConditionsCheckboxEnabled' => (int) $main_settings->getIntroductionSettings()->getExamConditionsCheckboxEnabled(),
5573
5574 'StartingTimeEnabled' => (int) $main_settings->getAccessSettings()->getStartTimeEnabled(),
5575 'StartingTime' => $main_settings->getAccessSettings()->getStartTime(),
5576 'EndingTimeEnabled' => (int) $main_settings->getAccessSettings()->getEndTimeEnabled(),
5577 'EndingTime' => $main_settings->getAccessSettings()->getEndTime(),
5578 'password_enabled' => (int) $main_settings->getAccessSettings()->getPasswordEnabled(),
5579 'password' => $main_settings->getAccessSettings()->getPassword(),
5580 'fixed_participants' => (int) $main_settings->getAccessSettings()->getFixedParticipants(),
5581
5582 'NrOfTries' => $main_settings->getTestBehaviourSettings()->getNumberOfTries(),
5583 'BlockAfterPassed' => (int) $main_settings->getTestBehaviourSettings()->getBlockAfterPassedEnabled(),
5584 'pass_waiting' => $main_settings->getTestBehaviourSettings()->getPassWaiting(),
5585 'EnableProcessingTime' => (int) $main_settings->getTestBehaviourSettings()->getProcessingTimeEnabled(),
5586 'ProcessingTime' => $main_settings->getTestBehaviourSettings()->getProcessingTime(),
5587 'ResetProcessingTime' => $main_settings->getTestBehaviourSettings()->getResetProcessingTime(),
5588 'Kiosk' => $main_settings->getTestBehaviourSettings()->getKioskMode(),
5589 'examid_in_test_pass' => (int) $main_settings->getTestBehaviourSettings()->getExamIdInTestAttemptEnabled(),
5590
5591 'TitleOutput' => $main_settings->getQuestionBehaviourSettings()->getQuestionTitleOutputMode(),
5592 'autosave' => (int) $main_settings->getQuestionBehaviourSettings()->getAutosaveEnabled(),
5593 'autosave_ival' => $main_settings->getQuestionBehaviourSettings()->getAutosaveInterval(),
5594 'Shuffle' => (int) $main_settings->getQuestionBehaviourSettings()->getShuffleQuestions(),
5595 'AnswerFeedbackPoints' => (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackPointsEnabled(),
5596 'AnswerFeedback' => (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackGenericEnabled(),
5597 'SpecificAnswerFeedback' => (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackSpecificEnabled(),
5598 'InstantFeedbackSolution' => (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackSolutionEnabled(),
5599 'force_inst_fb' => (int) $main_settings->getQuestionBehaviourSettings()->getForceInstantFeedbackOnNextQuestion(),
5600 'follow_qst_answer_fixation' => (int) $main_settings->getQuestionBehaviourSettings()->getLockAnswerOnNextQuestionEnabled(),
5601 'inst_fb_answer_fixation' => (int) $main_settings->getQuestionBehaviourSettings()->getLockAnswerOnInstantFeedbackEnabled(),
5602
5603 'use_previous_answers' => (int) $main_settings->getParticipantFunctionalitySettings()->getUsePreviousAnswerAllowed(),
5604 'ShowCancel' => (int) $main_settings->getParticipantFunctionalitySettings()->getSuspendTestAllowed(),
5605 'SequenceSettings' => (int) $main_settings->getParticipantFunctionalitySettings()->getPostponedQuestionsMoveToEnd(),
5606 'ListOfQuestionsSettings' => $main_settings->getParticipantFunctionalitySettings()->getUsrPassOverviewMode(),
5607 'ShowMarker' => (int) $main_settings->getParticipantFunctionalitySettings()->getQuestionMarkingEnabled(),
5608
5609 'enable_examview' => $main_settings->getFinishingSettings()->getShowAnswerOverview(),
5610 'ShowFinalStatement' => (int) $main_settings->getFinishingSettings()->getConcludingRemarksEnabled(),
5611 'redirection_mode' => $main_settings->getFinishingSettings()->getRedirectionMode()->value,
5612 'redirection_url' => $main_settings->getFinishingSettings()->getRedirectionUrl(),
5613 'mailnotification' => $main_settings->getFinishingSettings()->getMailNotificationContentType(),
5614 'mailnottype' => (int) $main_settings->getFinishingSettings()->getAlwaysSendMailNotification(),
5615
5616 'skill_service' => (int) $main_settings->getAdditionalSettings()->getSkillsServiceEnabled(),
5617
5618 'PassScoring' => $score_settings->getScoringSettings()->getPassScoring(),
5619 'ScoreCutting' => $score_settings->getScoringSettings()->getScoreCutting(),
5620 'CountSystem' => $score_settings->getScoringSettings()->getCountSystem(),
5621
5622 'ScoreReporting' => $score_settings->getResultSummarySettings()->getScoreReporting()->value,
5623 'ReportingDate' => $score_settings->getResultSummarySettings()->getReportingDate(),
5624 'pass_deletion_allowed' => (int) $score_settings->getResultSummarySettings()->getPassDeletionAllowed(),
5625 'show_grading_status' => (int) $score_settings->getResultSummarySettings()->getShowGradingStatusEnabled(),
5626 'show_grading_mark' => (int) $score_settings->getResultSummarySettings()->getShowGradingMarkEnabled(),
5627
5628 'ResultsPresentation' => $score_settings->getResultDetailsSettings()->getResultsPresentation(),
5629 'show_solution_list_comparison' => (int) $score_settings->getResultDetailsSettings()->getShowSolutionListComparison(),
5630 'examid_in_test_res' => (int) $score_settings->getResultDetailsSettings()->getShowExamIdInTestResults(),
5631
5632 'highscore_enabled' => (int) $score_settings->getGamificationSettings()->getHighscoreEnabled(),
5633 'highscore_anon' => (int) $score_settings->getGamificationSettings()->getHighscoreAnon(),
5634 'highscore_achieved_ts' => $score_settings->getGamificationSettings()->getHighscoreAchievedTS(),
5635 'highscore_score' => $score_settings->getGamificationSettings()->getHighscoreScore(),
5636 'highscore_percentage' => $score_settings->getGamificationSettings()->getHighscorePercentage(),
5637 'highscore_wtime' => $score_settings->getGamificationSettings()->getHighscoreWTime(),
5638 'highscore_own_table' => $score_settings->getGamificationSettings()->getHighscoreOwnTable(),
5639 'highscore_top_table' => $score_settings->getGamificationSettings()->getHighscoreTopTable(),
5640 'highscore_top_num' => $score_settings->getGamificationSettings()->getHighscoreTopNum(),
5641
5642 'HideInfoTab' => (int) $main_settings->getAdditionalSettings()->getHideInfoTab(),
5643 ];
5644
5645 $marks = array_map(
5646 fn(Mark $v): array => [
5647 'short_name' => $v->getShortName(),
5648 'official_name' => $v->getOfficialName(),
5649 'minimum_level' => $v->getMinimumLevel(),
5650 'passed' => $v->getPassed()
5651 ],
5652 $this->getMarkSchema()->getMarkSteps()
5653 );
5654
5655 $next_id = $this->db->nextId('tst_test_defaults');
5656 $this->db->insert(
5657 'tst_test_defaults',
5658 [
5659 'test_defaults_id' => ['integer', $next_id],
5660 'name' => ['text', $a_name],
5661 'user_fi' => ['integer', $this->user->getId()],
5662 'defaults' => ['clob', serialize($testsettings)],
5663 'marks' => ['clob', json_encode($marks)],
5664 'tstamp' => ['integer', time()]
5665 ]
5666 );
5667 }
5668
5669 public function applyDefaults(array $test_defaults): string
5670 {
5671 $testsettings = unserialize($test_defaults['defaults'], ['allowed_classes' => [DateTimeImmutable::class]]);
5672 $activation_starting_time = is_numeric($testsettings['activation_starting_time'] ?? false)
5673 ? (int) $testsettings['activation_starting_time']
5674 : null;
5675 $activation_ending_time = is_numeric($testsettings['activation_ending_time'] ?? false)
5676 ? (int) $testsettings['activation_ending_time']
5677 : null;
5678 $unserialized_marks = json_decode($test_defaults['marks'], true);
5679
5680 $info = '';
5681 if (is_array($unserialized_marks)
5682 && is_array($unserialized_marks[0])) {
5683 $this->mark_schema = $this->getMarkSchema()->withMarkSteps(
5684 array_map(
5685 fn(array $v): Mark => new Mark(
5686 $v['short_name'],
5687 $v['official_name'],
5688 $v['minimum_level'],
5689 $v['passed']
5690 ),
5691 $unserialized_marks
5692 )
5693 );
5694 } else {
5695 $info = 'old_mark_default_not_applied';
5696 }
5697
5698 $main_settings = $this->getMainSettings();
5699
5700 $general_settings = $main_settings->getGeneralSettings();
5701 $introduction_settings = $main_settings->getIntroductionSettings();
5702 $access_settings = $main_settings->getAccessSettings();
5703 $test_behavior_settings = $main_settings->getTestBehaviourSettings();
5704 $question_behavior_settings = $main_settings->getQuestionBehaviourSettings();
5705 $participant_functionality_settings = $main_settings->getParticipantFunctionalitySettings();
5706 $finishing_settings = $main_settings->getFinishingSettings();
5707 $additional_settings = $main_settings->getAdditionalSettings();
5708
5709 $main_settings = $main_settings
5711 $general_settings
5712 ->withQuestionSetType(
5713 $testsettings['questionSetType'] ?? $general_settings->getQuestionSetType()
5714 )->withAnonymity(
5715 $testsettings['Anonymity'] ?? $general_settings->getAnonymity()
5716 )
5717 )->withIntroductionSettings(
5718 $introduction_settings
5719 ->withIntroductionEnabled(
5720 $testsettings['IntroEnabled'] ?? $introduction_settings->getIntroductionEnabled()
5721 )->withExamConditionsCheckboxEnabled(
5722 $testsettings['ExamConditionsCheckboxEnabled'] ?? $introduction_settings->getExamConditionsCheckboxEnabled()
5723 )
5724 )->withAccessSettings(
5725 $access_settings
5726 ->withStartTimeEnabled(
5727 $testsettings['StartingTimeEnabled'] ?? $access_settings->getStartTimeEnabled()
5728 )->withStartTime(
5729 $this->convertTimeToDateTimeImmutableIfNecessary(
5730 $testsettings['StartingTime'] ?? $access_settings->getStartTime()
5731 )
5732 )->withEndTimeEnabled(
5733 $testsettings['EndingTimeEnabled'] ?? $access_settings->getEndTimeEnabled()
5734 )->withEndTime(
5735 $this->convertTimeToDateTimeImmutableIfNecessary(
5736 $testsettings['EndingTime'] ?? $access_settings->getEndTime()
5737 )
5738 )->withPasswordEnabled(
5739 $testsettings['password_enabled'] ?? $access_settings->getPasswordEnabled()
5740 )->withPassword(
5741 $testsettings['password'] ?? $access_settings->getPassword()
5742 )->withFixedParticipants(
5743 $testsettings['fixed_participants'] ?? $access_settings->getFixedParticipants()
5744 )
5745 )->withTestBehaviourSettings(
5746 $test_behavior_settings
5747 ->withNumberOfTries(
5748 $testsettings['NrOfTries'] ?? $test_behavior_settings->getNumberOfTries()
5749 )->withBlockAfterPassedEnabled(
5750 $testsettings['BlockAfterPassed'] ?? $test_behavior_settings->getBlockAfterPassedEnabled()
5751 )->withPassWaiting(
5752 $testsettings['pass_waiting'] ?? $test_behavior_settings->getPassWaiting()
5753 )->withKioskMode(
5754 $testsettings['Kiosk'] ?? $test_behavior_settings->getKioskMode()
5755 )->withProcessingTimeEnabled(
5756 $testsettings['EnableProcessingTime'] ?? $test_behavior_settings->getProcessingTimeEnabled()
5757 )->withProcessingTime(
5758 $testsettings['ProcessingTime'] ?? $test_behavior_settings->getProcessingTime()
5759 )->withResetProcessingTime(
5760 $testsettings['ResetProcessingTime'] ?? $test_behavior_settings->getResetProcessingTime()
5761 )->withExamIdInTestAttemptEnabled(
5762 $testsettings['examid_in_test_pass'] ?? $test_behavior_settings->getExamIdInTestAttemptEnabled()
5763 )
5764 )->withQuestionBehaviourSettings(
5765 $question_behavior_settings
5766 ->withQuestionTitleOutputMode(
5767 $testsettings['TitleOutput'] ?? $question_behavior_settings->getQuestionTitleOutputMode()
5768 )->withAutosaveEnabled(
5769 $testsettings['autosave'] ?? $question_behavior_settings->getAutosaveEnabled()
5770 )->withAutosaveInterval(
5771 $testsettings['autosave_ival'] ?? $question_behavior_settings->getAutosaveInterval()
5772 )->withShuffleQuestions(
5773 $testsettings['Shuffle'] ?? $question_behavior_settings->getShuffleQuestions()
5774 )->withInstantFeedbackPointsEnabled(
5775 $testsettings['AnswerFeedbackPoints'] ?? $question_behavior_settings->getInstantFeedbackPointsEnabled()
5776 )->withInstantFeedbackGenericEnabled(
5777 $testsettings['AnswerFeedback'] ?? $question_behavior_settings->getInstantFeedbackGenericEnabled()
5778 )->withInstantFeedbackSpecificEnabled(
5779 $testsettings['SpecificAnswerFeedback'] ?? $question_behavior_settings->getInstantFeedbackSpecificEnabled()
5780 )->withInstantFeedbackSolutionEnabled(
5781 $testsettings['InstantFeedbackSolution'] ?? $question_behavior_settings->getInstantFeedbackSolutionEnabled()
5782 )->withForceInstantFeedbackOnNextQuestion(
5783 $testsettings['force_inst_fb'] ?? $question_behavior_settings->getForceInstantFeedbackOnNextQuestion()
5784 )->withLockAnswerOnInstantFeedbackEnabled(
5785 $testsettings['inst_fb_answer_fixation'] ?? $question_behavior_settings->getLockAnswerOnInstantFeedbackEnabled()
5786 )->withLockAnswerOnNextQuestionEnabled(
5787 $testsettings['follow_qst_answer_fixation'] ?? $question_behavior_settings->getLockAnswerOnNextQuestionEnabled()
5788 )
5789 )->withParticipantFunctionalitySettings(
5790 $participant_functionality_settings
5791 ->withUsePreviousAnswerAllowed(
5792 $testsettings['use_previous_answers'] ?? $participant_functionality_settings->getUsePreviousAnswerAllowed()
5793 )->withSuspendTestAllowed(
5794 $testsettings['ShowCancel'] ?? $participant_functionality_settings->getSuspendTestAllowed()
5795 )->withPostponedQuestionsMoveToEnd(
5796 $testsettings['SequenceSettings'] ?? $participant_functionality_settings->getPostponedQuestionsMoveToEnd()
5797 )->withUsrPassOverviewMode(
5798 $testsettings['ListOfQuestionsSettings'] ?? $participant_functionality_settings->getUsrPassOverviewMode()
5799 )->withQuestionMarkingEnabled(
5800 $testsettings['ShowMarker'] ?? $participant_functionality_settings->getQuestionMarkingEnabled()
5801 )
5802 )->withFinishingSettings(
5803 $finishing_settings
5804 ->withShowAnswerOverview(
5805 $testsettings['enable_examview'] ?? $finishing_settings->getShowAnswerOverview()
5806 )->withConcludingRemarksEnabled(
5807 $testsettings['ShowFinalStatement'] ?? $finishing_settings->getConcludingRemarksEnabled()
5808 )->withRedirectionMode(
5809 RedirectionModes::tryFrom($testsettings['redirection_mode'] ?? 0) ?? $finishing_settings->getRedirectionMode()
5810 )->withRedirectionUrl(
5811 $testsettings['redirection_url'] ?? $finishing_settings->getRedirectionUrl()
5812 )->withMailNotificationContentType(
5813 $testsettings['mailnotification'] ?? $finishing_settings->getMailNotificationContentType()
5814 )->withAlwaysSendMailNotification(
5815 $testsettings['mailnottype'] ?? $finishing_settings->getAlwaysSendMailNotification()
5816 )
5817 )->withAdditionalSettings(
5818 $additional_settings
5819 ->withSkillsServiceEnabled(
5820 $testsettings['skill_service'] ?? $additional_settings->getSkillsServiceEnabled()
5821 )->withHideInfoTab(
5822 $testsettings['HideInfoTab'] ?? $additional_settings->getHideInfoTab()
5823 )
5824 );
5825
5826 $this->getMainSettingsRepository()->store($main_settings);
5827
5828 $score_reporting = ScoreReportingTypes::SCORE_REPORTING_DISABLED;
5829 if ($testsettings['ScoreReporting'] !== null) {
5830 $score_reporting = ScoreReportingTypes::tryFrom($testsettings['ScoreReporting'])
5831 ?? ScoreReportingTypes::SCORE_REPORTING_DISABLED;
5832 }
5833
5834 $reporting_date = $testsettings['ReportingDate'];
5835 if (is_string($reporting_date)) {
5836 $reporting_date = new DateTimeImmutable($testsettings['ReportingDate'], new DateTimeZone('UTC'));
5837 }
5838
5839 $score_settings = $this->getScoreSettings();
5840
5841 $scoring_settings = $score_settings->getScoringSettings();
5842 $result_summary_settings = $score_settings->getResultSummarySettings();
5843 $result_details_settings = $score_settings->getResultDetailsSettings();
5844 $gamification_settings = $score_settings->getGamificationSettings();
5845
5846 $score_settings = $score_settings
5848 $scoring_settings
5849 ->withPassScoring(
5850 $testsettings['PassScoring'] ?? $scoring_settings->getPassScoring()
5851 )->withScoreCutting(
5852 $testsettings['ScoreCutting'] ?? $scoring_settings->getScoreCutting()
5853 )->withCountSystem(
5854 $testsettings['CountSystem'] ?? $scoring_settings->getCountSystem()
5855 )
5856 )->withResultSummarySettings(
5857 $result_summary_settings
5858 ->withPassDeletionAllowed(
5859 $testsettings['pass_deletion_allowed'] ?? $result_summary_settings->getPassDeletionAllowed()
5860 )->withShowGradingStatusEnabled(
5861 $testsettings['show_grading_status'] ?? $result_summary_settings->getShowGradingStatusEnabled()
5862 )->withShowGradingMarkEnabled(
5863 $testsettings['show_grading_mark'] ?? $result_summary_settings->getShowGradingMarkEnabled()
5864 )->withScoreReporting(
5865 $score_reporting
5866 )->withReportingDate(
5867 $reporting_date
5868 )
5869 )->withResultDetailsSettings(
5870 $result_details_settings
5871 ->withResultsPresentation(
5872 $testsettings['ResultsPresentation'] ?? $result_details_settings->getResultsPresentation()
5873 )->withShowSolutionListComparison(
5874 $testsettings['show_solution_list_comparison'] ?? $result_details_settings->getShowSolutionListComparison()
5875 )->withShowExamIdInTestResults(
5876 $testsettings['examid_in_test_res'] ?? $result_details_settings->getShowExamIdInTestResults()
5877 )
5878 )->withGamificationSettings(
5879 $gamification_settings
5880 ->withHighscoreEnabled(
5881 $testsettings['highscore_enabled'] ?? $gamification_settings->getHighscoreEnabled()
5882 )->withHighscoreAnon(
5883 $testsettings['highscore_anon'] ?? $gamification_settings->getHighscoreAnon()
5884 )->withHighscoreAchievedTS(
5885 $testsettings['highscore_achieved_ts'] ?? $gamification_settings->getHighscoreAchievedTS()
5886 )->withHighscoreScore(
5887 $testsettings['highscore_score'] ?? $gamification_settings->getHighscoreScore()
5888 )->withHighscorePercentage(
5889 $testsettings['highscore_percentage'] ?? $gamification_settings->getHighscorePercentage()
5890 )->withHighscoreWTime(
5891 $testsettings['highscore_wtime'] ?? $gamification_settings->getHighscoreWTime()
5892 )->withHighscoreOwnTable(
5893 $testsettings['highscore_own_table'] ?? $gamification_settings->getHighscoreOwnTable()
5894 )->withHighscoreTopTable(
5895 $testsettings['highscore_top_table'] ?? $gamification_settings->getHighscoreTopTable()
5896 )->withHighscoreTopNum(
5897 $testsettings['highscore_top_num'] ?? $gamification_settings->getHighscoreTopNum()
5898 )
5899 )
5900 ;
5901 $this->getScoreSettingsRepository()->store($score_settings);
5902 $this->saveToDb();
5903
5904 return $info;
5905 }
5906
5908 DateTimeImmutable|int|null $date_time
5909 ): ?DateTimeImmutable {
5910 if ($date_time === null || $date_time instanceof DateTimeImmutable) {
5911 return $date_time;
5912 }
5913
5914 return DateTimeImmutable::createFromFormat('U', (string) $date_time);
5915 }
5916
5924 public function processPrintoutput2FO($print_output): string
5925 {
5926 if (extension_loaded("tidy")) {
5927 $config = [
5928 "indent" => false,
5929 "output-xml" => true,
5930 "numeric-entities" => true
5931 ];
5932 $tidy = new tidy();
5933 $tidy->parseString($print_output, $config, 'utf8');
5934 $tidy->cleanRepair();
5935 $print_output = tidy_get_output($tidy);
5936 $print_output = preg_replace("/^.*?(<html)/", "\\1", $print_output);
5937 } else {
5938 $print_output = str_replace("&nbsp;", "&#160;", $print_output);
5939 $print_output = str_replace("&otimes;", "X", $print_output);
5940 }
5941 $xsl = file_get_contents("./components/ILIAS/Test/xml/question2fo.xsl");
5942
5943 // additional font support
5944
5945 $xsl = str_replace(
5946 'font-family="Helvetica, unifont"',
5947 'font-family="' . $this->settings->get('rpc_pdf_font', 'Helvetica, unifont') . '"',
5948 $xsl
5949 );
5950
5951 $args = [ '/_xml' => $print_output, '/_xsl' => $xsl ];
5952 $xh = xslt_create();
5953 $params = [];
5954 $output = xslt_process($xh, "arg:/_xml", "arg:/_xsl", null, $args, $params);
5955 xslt_error($xh);
5956 xslt_free($xh);
5957 return $output;
5958 }
5959
5966 public function deliverPDFfromHTML($content, $title = null)
5967 {
5968 $content = preg_replace("/href=\".*?\"/", "", $content);
5969 $printbody = new ilTemplate("tpl.il_as_tst_print_body.html", true, true, "components/ILIAS/Test");
5970 $printbody->setVariable("TITLE", ilLegacyFormElementsUtil::prepareFormOutput($this->getTitle()));
5971 $printbody->setVariable("ADM_CONTENT", $content);
5972 $printbody->setCurrentBlock("css_file");
5973 $printbody->setVariable("CSS_FILE", ilUtil::getStyleSheetLocation("filesystem", "delos.css"));
5974 $printbody->parseCurrentBlock();
5975 $printoutput = $printbody->get();
5976 $html = str_replace("href=\"./", "href=\"" . ILIAS_HTTP_PATH . "/", $printoutput);
5977 $html = preg_replace("/<div id=\"dontprint\">.*?<\\/div>/ims", "", $html);
5978 if (extension_loaded("tidy")) {
5979 $config = [
5980 "indent" => false,
5981 "output-xml" => true,
5982 "numeric-entities" => true
5983 ];
5984 $tidy = new tidy();
5985 $tidy->parseString($html, $config, 'utf8');
5986 $tidy->cleanRepair();
5987 $html = tidy_get_output($tidy);
5988 $html = preg_replace("/^.*?(<html)/", "\\1", $html);
5989 } else {
5990 $html = str_replace("&nbsp;", "&#160;", $html);
5991 $html = str_replace("&otimes;", "X", $html);
5992 }
5993 $html = preg_replace("/src=\".\\//ims", "src=\"" . ILIAS_HTTP_PATH . "/", $html);
5994 $this->deliverPDFfromFO($this->processPrintoutput2FO($html), $title);
5995 }
5996
6002 public function deliverPDFfromFO($fo, $title = null): bool
6003 {
6004 $fo_file = ilFileUtils::ilTempnam() . ".fo";
6005 $fp = fopen($fo_file, "w");
6006 fwrite($fp, $fo);
6007 fclose($fp);
6008
6009 try {
6010 $pdf_base64 = ilRpcClientFactory::factory('RPCTransformationHandler')->ilFO2PDF($fo);
6011 $filename = (strlen($title)) ? $title : $this->getTitle();
6014 $pdf_base64->scalar,
6016 "application/pdf"
6017 );
6018 return true;
6019 } catch (Exception $e) {
6020 $this->logger->info(__METHOD__ . ': ' . $e->getMessage());
6021 return false;
6022 }
6023 }
6024
6034 public static function getManualFeedback(int $active_id, int $question_id, ?int $pass): string
6035 {
6036 if ($pass === null) {
6037 return '';
6038 }
6039 $feedback = '';
6040 $row = self::getSingleManualFeedback((int) $active_id, (int) $question_id, (int) $pass);
6041
6042 if ($row !== [] && ($row['finalized_evaluation'] || \ilTestService::isManScoringDone((int) $active_id))) {
6043 $feedback = $row['feedback'] ?? '';
6044 }
6045
6046 return $feedback;
6047 }
6048
6049 public static function getSingleManualFeedback(int $active_id, int $question_id, int $pass): array
6050 {
6051 global $DIC;
6052 $ilDB = $DIC['ilDB'];
6053 $row = [];
6054 $result = $ilDB->queryF(
6055 "SELECT * FROM tst_manual_fb WHERE active_fi = %s AND question_fi = %s AND pass = %s",
6056 ['integer', 'integer', 'integer'],
6057 [$active_id, $question_id, $pass]
6058 );
6059
6060 if ($ilDB->numRows($result) === 1) {
6061 $row = $ilDB->fetchAssoc($result);
6062 $row['feedback'] = ilRTE::_replaceMediaObjectImageSrc($row['feedback'] ?? '', 1);
6063 } elseif ($ilDB->numRows($result) > 1) {
6064 $DIC->logger()->root()->warning(
6065 "WARNING: Multiple feedback entries on tst_manual_fb for " .
6066 "active_fi = $active_id , question_fi = $question_id and pass = $pass"
6067 );
6068 }
6069
6070 return $row;
6071 }
6072
6080 public function getCompleteManualFeedback(int $question_id): array
6081 {
6082 global $DIC;
6083 $ilDB = $DIC['ilDB'];
6084
6085 $feedback = [];
6086 $result = $ilDB->queryF(
6087 "SELECT * FROM tst_manual_fb WHERE question_fi = %s",
6088 ['integer'],
6089 [$question_id]
6090 );
6091
6092 while ($row = $ilDB->fetchAssoc($result)) {
6093 $active = $row['active_fi'];
6094 $pass = $row['pass'];
6095 $question = $row['question_fi'];
6096
6097 $row['feedback'] = ilRTE::_replaceMediaObjectImageSrc($row['feedback'] ?? '', 1);
6098
6099 $feedback[$active][$pass][$question] = $row;
6100 }
6101
6102 return $feedback;
6103 }
6104
6105 public function saveManualFeedback(
6106 int $active_id,
6107 int $question_id,
6108 int $pass,
6109 ?string $feedback,
6110 bool $finalized = false
6111 ): void {
6112 $feedback_old = self::getSingleManualFeedback($active_id, $question_id, $pass);
6113 $this->db->manipulateF(
6114 'DELETE FROM tst_manual_fb WHERE active_fi = %s AND question_fi = %s AND pass = %s',
6115 ['integer', 'integer', 'integer'],
6116 [$active_id, $question_id, $pass]
6117 );
6118
6119 $this->insertManualFeedback($active_id, $question_id, $pass, $feedback, $finalized, $feedback_old);
6120
6121 }
6122
6123 private function insertManualFeedback(
6124 int $active_id,
6125 int $question_id,
6126 int $pass,
6127 ?string $feedback,
6128 bool $finalized,
6129 array $feedback_old
6130 ): void {
6131 $next_id = $this->db->nextId('tst_manual_fb');
6132 $user = $this->user->getId();
6133 $finalized_time = time();
6134
6135 $update_default = [
6136 'manual_feedback_id' => [ 'integer', $next_id],
6137 'active_fi' => [ 'integer', $active_id],
6138 'question_fi' => [ 'integer', $question_id],
6139 'pass' => [ 'integer', $pass],
6140 'feedback' => [ 'clob', $feedback ? ilRTE::_replaceMediaObjectImageSrc($feedback, 0) : null],
6141 'tstamp' => [ 'integer', time()]
6142 ];
6143
6144 if ($feedback_old !== [] && (int) $feedback_old['finalized_evaluation'] === 1) {
6145 $user = $feedback_old['finalized_by_usr_id'];
6146 $finalized_time = $feedback_old['finalized_tstamp'];
6147 }
6148
6149 if ($finalized === false) {
6150 $update_default['finalized_evaluation'] = ['integer', 0];
6151 $update_default['finalized_by_usr_id'] = ['integer', 0];
6152 $update_default['finalized_tstamp'] = ['integer', 0];
6153 } elseif ($finalized === true) {
6154 $update_default['finalized_evaluation'] = ['integer', 1];
6155 $update_default['finalized_by_usr_id'] = ['integer', $user];
6156 $update_default['finalized_tstamp'] = ['integer', $finalized_time];
6157 }
6158
6159 $this->db->insert('tst_manual_fb', $update_default);
6160
6161 if ($this->logger->isLoggingEnabled()) {
6162 $this->logger->logScoringInteraction(
6163 $this->logger->getInteractionFactory()->buildScoringInteraction(
6164 $this->getRefId(),
6165 $question_id,
6166 $this->user->getId(),
6167 self::_getUserIdFromActiveId($active_id),
6168 TestScoringInteractionTypes::QUESTION_GRADED,
6169 [
6170 AdditionalInformationGenerator::KEY_EVAL_FINALIZED => $this->logger
6171 ->getAdditionalInformationGenerator()->getTrueFalseTagForBool($finalized),
6172 AdditionalInformationGenerator::KEY_FEEDBACK => $feedback ? ilRTE::_replaceMediaObjectImageSrc($feedback, 0) : ''
6173 ]
6174 )
6175 );
6176 }
6177 }
6178
6186 public function getJavaScriptOutput(): bool
6187 {
6188 return true;
6189 }
6190
6191 public function &createTestSequence($active_id, $pass, $shuffle)
6192 {
6193 $this->test_sequence = new ilTestSequence($active_id, $pass, $this->isRandomTest(), $this->questionrepository);
6194 }
6195
6201 public function setTestId($a_id)
6202 {
6203 $this->test_id = $a_id;
6204 }
6205
6213 public function getDetailedTestResults($participants): array
6214 {
6215 $results = [];
6216 if (count($participants)) {
6217 foreach ($participants as $active_id => $user_rec) {
6218 $row = [];
6219 $reached_points = 0;
6220 $max_points = 0;
6221 $pass = ilObjTest::_getResultPass($active_id);
6222 foreach ($this->questions as $value) {
6223 $question = ilObjTest::_instanciateQuestion($value);
6224 if (is_object($question)) {
6225 $max_points += $question->getMaximumPoints();
6226 $reached_points += $question->getReachedPoints($active_id, $pass);
6227 if ($max_points > 0) {
6228 $percentvalue = $reached_points / $max_points;
6229 if ($percentvalue < 0) {
6230 $percentvalue = 0.0;
6231 }
6232 } else {
6233 $percentvalue = 0;
6234 }
6235 if ($this->getAnonymity()) {
6236 $user_rec['firstname'] = "";
6237 $user_rec['lastname'] = $this->lng->txt("anonymous");
6238 }
6239 $results[] = [
6240 "user_id" => $user_rec['usr_id'],
6241 "matriculation" => $user_rec['matriculation'],
6242 "lastname" => $user_rec['lastname'],
6243 "firstname" => $user_rec['firstname'],
6244 "login" => $user_rec['login'],
6245 "question_id" => $question->getId(),
6246 "question_title" => $question->getTitle(),
6247 "reached_points" => $reached_points,
6248 "max_points" => $max_points,
6249 "passed" => $user_rec['passed'] ? '1' : '0',
6250 ];
6251 }
6252 }
6253 }
6254 }
6255 return $results;
6256 }
6257
6261 public static function _lookupTestObjIdForQuestionId(int $q_id): ?int
6262 {
6263 global $DIC;
6264 $ilDB = $DIC['ilDB'];
6265
6266 $result = $ilDB->queryF(
6267 'SELECT t.obj_fi obj_id FROM tst_test_question q, tst_tests t WHERE q.test_fi = t.test_id AND q.question_fi = %s',
6268 ['integer'],
6269 [$q_id]
6270 );
6271 $rec = $ilDB->fetchAssoc($result);
6272 return $rec['obj_id'] ?? null;
6273 }
6274
6281 public function isPluginActive($a_pname): bool
6282 {
6283 if (!$this->component_repository->getComponentByTypeAndName(
6285 'TestQuestionPool'
6286 )->getPluginSlotById('qst')->hasPluginName($a_pname)) {
6287 return false;
6288 }
6289
6290 return $this->component_repository
6291 ->getComponentByTypeAndName(
6293 'TestQuestionPool'
6294 )
6295 ->getPluginSlotById(
6296 'qst'
6297 )
6298 ->getPluginByName(
6299 $a_pname
6300 )->isActive();
6301 }
6302
6306 public function getParticipantsForTestAndQuestion($test_id, $question_id): array
6307 {
6308 $query = "
6309 SELECT tst_test_result.active_fi, tst_test_result.question_fi, tst_test_result.pass
6310 FROM tst_test_result
6311 INNER JOIN tst_active ON tst_active.active_id = tst_test_result.active_fi AND tst_active.test_fi = %s
6312 INNER JOIN qpl_questions ON qpl_questions.question_id = tst_test_result.question_fi
6313 LEFT JOIN usr_data ON usr_data.usr_id = tst_active.user_fi
6314 WHERE tst_test_result.question_fi = %s
6315 ORDER BY usr_data.lastname ASC, usr_data.firstname ASC
6316 ";
6317
6318 $result = $this->db->queryF(
6319 $query,
6320 ['integer', 'integer'],
6321 [$test_id, $question_id]
6322 );
6323 $foundusers = [];
6325 while ($row = $this->db->fetchAssoc($result)) {
6326 if ($this->getAccessFilteredParticipantList() && !$this->getAccessFilteredParticipantList()->isActiveIdInList($row["active_fi"])) {
6327 continue;
6328 }
6329
6330 if (!array_key_exists($row["active_fi"], $foundusers)) {
6331 $foundusers[$row["active_fi"]] = [];
6332 }
6333 array_push($foundusers[$row["active_fi"]], ["pass" => $row["pass"], "qid" => $row["question_fi"]]);
6334 }
6335 return $foundusers;
6336 }
6337
6338 public function getAggregatedResultsData(): array
6339 {
6340 $data = $this->getCompleteEvaluationData();
6341 $found_participants = $data->getParticipants();
6342 $results = ['overview' => [], 'questions' => []];
6343 if ($found_participants !== []) {
6344 $results['overview']['tst_stat_result_mark_median'] = $data->getStatistics()->getEvaluationDataOfMedianUser()?->getMark()?->getShortName() ?? '';
6345 $results['overview']['tst_stat_result_rank_median'] = $data->getStatistics()->rankMedian();
6346 $results['overview']['tst_stat_result_total_participants'] = $data->getStatistics()->count();
6347 $results['overview']['tst_stat_result_median'] = $data->getStatistics()->median();
6348 $results['overview']['tst_eval_total_persons'] = count($found_participants);
6349 $total_finished = $data->getTotalFinishedParticipants();
6350 $results['overview']['tst_eval_total_finished'] = $total_finished;
6351 $results['overview']['tst_eval_total_finished_average_time'] =
6352 $this->secondsToHoursMinutesSecondsString(
6353 $this->evalTotalStartedAverageTime($data->getParticipantIds())
6354 );
6355 $total_passed = 0;
6356 $total_passed_reached = 0;
6357 $total_passed_max = 0;
6358 $total_passed_time = 0;
6359 foreach ($found_participants as $userdata) {
6360 if ($userdata->getMark()?->getPassed()) {
6361 $total_passed++;
6362 $total_passed_reached += $userdata->getReached();
6363 $total_passed_max += $userdata->getMaxpoints();
6364 $total_passed_time += $userdata->getTimeOnTask();
6365 }
6366 }
6367 $average_passed_reached = $total_passed ? $total_passed_reached / $total_passed : 0;
6368 $average_passed_max = $total_passed ? $total_passed_max / $total_passed : 0;
6369 $average_passed_time = $total_passed ? $total_passed_time / $total_passed : 0;
6370 $results['overview']['tst_eval_total_passed'] = $total_passed;
6371 $results['overview']['tst_eval_total_passed_average_points'] = sprintf('%2.2f', $average_passed_reached)
6372 . ' ' . strtolower('of') . ' ' . sprintf('%2.2f', $average_passed_max);
6373 $results['overview']['tst_eval_total_passed_average_time'] =
6374 $this->secondsToHoursMinutesSecondsString($average_passed_time);
6375 }
6376
6377 foreach ($data->getQuestionTitles() as $question_id => $question_title) {
6378 $answered = 0;
6379 $reached = 0;
6380 $max = 0;
6381 foreach ($found_participants as $userdata) {
6382 for ($i = 0; $i <= $userdata->getLastPass(); $i++) {
6383 if (is_object($userdata->getPass($i))) {
6384 $question = $userdata->getPass($i)->getAnsweredQuestionByQuestionId($question_id);
6385 if (is_array($question)) {
6386 $answered++;
6387 $reached += $question['reached'];
6388 $max += $question['points'];
6389 }
6390 }
6391 }
6392 }
6393 $percent = $max ? $reached / $max * 100.0 : 0;
6394 $results['questions'][$question_id] = [
6395 $question_title,
6396 sprintf('%.2f', $answered ? $reached / $answered : 0) . ' ' . strtolower($this->lng->txt('of')) . ' ' . sprintf('%.2f', $answered ? $max / $answered : 0),
6397 sprintf('%.2f', $percent) . '%',
6398 $answered,
6399 sprintf('%.2f', $answered ? $reached / $answered : 0),
6400 sprintf('%.2f', $answered ? $max / $answered : 0),
6401 $percent / 100.0
6402 ];
6403 }
6404 return $results;
6405 }
6406
6407 protected function secondsToHoursMinutesSecondsString(int $seconds): string
6408 {
6409 $diff_hours = floor($seconds / 3600);
6410 $seconds -= $diff_hours * 3600;
6411 $diff_minutes = floor($seconds / 60);
6412 $seconds -= $diff_minutes * 60;
6413 return sprintf('%02d:%02d:%02d', $diff_hours, $diff_minutes, $seconds);
6414 }
6415
6419 public function getXMLZip(): string
6420 {
6421 return $this->export_factory->getExporter($this, 'xml')
6422 ->write();
6423 }
6424
6425 public function getMailNotification(): int
6426 {
6427 return $this->getMainSettings()->getFinishingSettings()->getMailNotificationContentType();
6428 }
6429
6430 public function sendSimpleNotification($active_id)
6431 {
6432 $mail = new ilTestMailNotification();
6433 $owner_id = $this->getOwner();
6434 $usr_data = $this->userLookupFullName(ilObjTest::_getUserIdFromActiveId($active_id));
6435 $mail->sendSimpleNotification($owner_id, $this->getTitle(), $usr_data);
6436 }
6437
6438 public function sendAdvancedNotification(int $active_id): void
6439 {
6440 $mail = new ilTestMailNotification();
6441 $owner_id = $this->getOwner();
6442 $usr_data = $this->userLookupFullName(ilObjTest::_getUserIdFromActiveId($active_id));
6443
6444 $path = $this->export_factory->getExporter(
6445 $this,
6446 ExportImportTypes::SCORED_ATTEMPT
6447 )->withFilterByActiveId($active_id)
6448 ->write();
6449
6450 $delivered_file_name = 'result_' . $active_id . '.xlsx';
6452 $fd->copyAttachmentFile($path, $delivered_file_name);
6453 $file_names[] = $delivered_file_name;
6454
6455 $mail->sendAdvancedNotification($owner_id, $this->getTitle(), $usr_data, $file_names);
6456
6457 if (count($file_names)) {
6458 $fd->unlinkFiles($file_names);
6459 unset($fd);
6460 @unlink($path);
6461 }
6462 }
6463
6464 public function getMailNotificationType(): bool
6465 {
6466 return $this->getMainSettings()->getFinishingSettings()->getAlwaysSendMailNotification();
6467 }
6468
6469 public function getExportSettings(): int
6470 {
6471 return $this->getScoreSettings()->getResultDetailsSettings()->getExportSettings();
6472 }
6473
6474 public function setTemplate(int $template_id)
6475 {
6476 $this->template_id = $template_id;
6477 }
6478
6479 public function getTemplate(): int
6480 {
6481 return $this->template_id;
6482 }
6483
6485 {
6486 $question_set_config = $this->question_set_config_factory->getQuestionSetConfig();
6487 $reindexed_sequence_position_map = $question_set_config->reindexQuestionOrdering();
6488
6489 $this->loadQuestions();
6490
6491 return $reindexed_sequence_position_map;
6492 }
6493
6494 public function setQuestionOrder(array $order)
6495 {
6496 asort($order);
6497
6498 $i = 0;
6499
6500 foreach (array_keys($order) as $id) {
6501 $i++;
6502
6503 $query = "
6504 UPDATE tst_test_question
6505 SET sequence = %s
6506 WHERE question_fi = %s
6507 ";
6508
6509 $this->db->manipulateF(
6510 $query,
6511 ['integer', 'integer'],
6512 [$i, $id]
6513 );
6514 }
6515
6516 if ($this->logger->isLoggingEnabled()) {
6517 $this->logger->logTestAdministrationInteraction(
6518 $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
6519 $this->getRefId(),
6520 $this->user->getId(),
6521 TestAdministrationInteractionTypes::QUESTION_MOVED,
6522 [
6523 AdditionalInformationGenerator::KEY_QUESTION_ORDER => $order
6524 ]
6525 )
6526 );
6527 }
6528
6529 $this->loadQuestions();
6530 }
6531
6532 public function hasQuestionsWithoutQuestionpool(): bool
6533 {
6534 $questions = $this->getQuestionTitlesAndIndexes();
6535
6536 $IN_questions = $this->db->in('q1.question_id', array_keys($questions), false, 'integer');
6537
6538 $query = "
6539 SELECT count(q1.question_id) cnt
6540
6541 FROM qpl_questions q1
6542
6543 INNER JOIN qpl_questions q2
6544 ON q2.question_id = q1.original_id
6545
6546 WHERE $IN_questions
6547 AND q1.obj_fi = q2.obj_fi
6548 ";
6549 $rset = $this->db->query($query);
6550 $row = $this->db->fetchAssoc($rset);
6551
6552 return $row['cnt'] > 0;
6553 }
6554
6561 public static function _lookupFinishedUserTests($a_user_id): array
6562 {
6563 global $DIC;
6564 $ilDB = $DIC['ilDB'];
6565
6566 $result = $ilDB->queryF(
6567 "SELECT test_fi,MAX(pass) AS pass FROM tst_active" .
6568 " JOIN tst_pass_result ON (tst_pass_result.active_fi = tst_active.active_id)" .
6569 " WHERE user_fi=%s" .
6570 " GROUP BY test_fi",
6571 ['integer', 'integer'],
6572 [$a_user_id, 1]
6573 );
6574 $all = [];
6575 while ($row = $ilDB->fetchAssoc($result)) {
6576 $obj_id = self::_getObjectIDFromTestID($row["test_fi"]);
6577 $all[$obj_id] = (bool) $row["pass"];
6578 }
6579 return $all;
6580 }
6581
6582 public function getQuestions(): array
6583 {
6584 return $this->questions;
6585 }
6586
6587 public function isOnline(): bool
6588 {
6589 return $this->online;
6590 }
6591
6592 public function getIntroductionPageId(): int
6593 {
6594 $page_id = $this->getMainSettings()->getIntroductionSettings()->getIntroductionPageId();
6595 if ($page_id !== null) {
6596 return $page_id;
6597 }
6598
6599 $page_object = new ilTestPage();
6600 $page_object->setParentId($this->getId());
6601 $new_page_id = $page_object->createPageWithNextId();
6602 $settings = $this->getMainSettings()->getIntroductionSettings()
6603 ->withIntroductionPageId($new_page_id);
6604 $this->getMainSettingsRepository()->store(
6605 $this->getMainSettings()->withIntroductionSettings($settings)
6606 );
6607 return $new_page_id;
6608 }
6609
6611 {
6612 $page_id = $this->getMainSettings()->getFinishingSettings()->getConcludingRemarksPageId();
6613 if ($page_id !== null) {
6614 return $page_id;
6615 }
6616
6617 $page_object = new ilTestPage();
6618 $page_object->setParentId($this->getId());
6619 $new_page_id = $page_object->createPageWithNextId();
6620 $settings = $this->getMainSettings()->getFinishingSettings()
6621 ->withConcludingRemarksPageId($new_page_id);
6622 $this->getMainSettingsRepository()->store(
6623 $this->getMainSettings()->withFinishingSettings($settings)
6624 );
6625 return $new_page_id;
6626 }
6627
6628 public function getHighscoreEnabled(): bool
6629 {
6630 return $this->getScoreSettings()->getGamificationSettings()->getHighscoreEnabled();
6631 }
6632
6642 public function getHighscoreAnon(): bool
6643 {
6644 return $this->getScoreSettings()->getGamificationSettings()->getHighscoreAnon();
6645 }
6646
6655 public function isHighscoreAnon(): bool
6656 {
6657 return $this->getAnonymity() == 1 || $this->getHighscoreAnon();
6658 }
6659
6663 public function getHighscoreAchievedTS(): bool
6664 {
6665 return $this->getScoreSettings()->getGamificationSettings()->getHighscoreAchievedTS();
6666 }
6667
6671 public function getHighscoreScore(): bool
6672 {
6673 return $this->getScoreSettings()->getGamificationSettings()->getHighscoreScore();
6674 }
6675
6679 public function getHighscorePercentage(): bool
6680 {
6681 return $this->getScoreSettings()->getGamificationSettings()->getHighscorePercentage();
6682 }
6683
6687 public function getHighscoreWTime(): bool
6688 {
6689 return $this->getScoreSettings()->getGamificationSettings()->getHighscoreWTime();
6690 }
6691
6695 public function getHighscoreOwnTable(): bool
6696 {
6697 return $this->getScoreSettings()->getGamificationSettings()->getHighscoreOwnTable();
6698 }
6699
6703 public function getHighscoreTopTable(): bool
6704 {
6705 return $this->getScoreSettings()->getGamificationSettings()->getHighscoreTopTable();
6706 }
6707
6712 public function getHighscoreTopNum(int $a_retval = 10): int
6713 {
6714 return $this->getScoreSettings()->getGamificationSettings()->getHighscoreTopNum();
6715 }
6716
6717 public function getHighscoreMode(): int
6718 {
6719 return $this->getScoreSettings()->getGamificationSettings()->getHighScoreMode();
6720 }
6721
6722 public function getSpecificAnswerFeedback(): bool
6723 {
6724 return $this->getMainSettings()->getQuestionBehaviourSettings()->getInstantFeedbackSpecificEnabled();
6725 }
6726
6727 public function getAutosave(): bool
6728 {
6729 return $this->getMainSettings()->getQuestionBehaviourSettings()->getAutosaveEnabled();
6730 }
6731
6732 public function isPassDeletionAllowed(): bool
6733 {
6734 return $this->getScoreSettings()->getResultSummarySettings()->getPassDeletionAllowed();
6735 }
6736
6737 public function getEnableExamview(): bool
6738 {
6739 return $this->getMainSettings()->getFinishingSettings()->getShowAnswerOverview();
6740 }
6741
6748 public function getStartingTimeOfParticipants(): array
6749 {
6750 $times = [];
6751 $result = $this->db->queryF(
6752 "SELECT tst_times.active_fi, tst_times.started FROM tst_times, tst_active WHERE tst_times.active_fi = tst_active.active_id AND tst_active.test_fi = %s ORDER BY tst_times.tstamp DESC",
6753 ['integer'],
6754 [$this->getTestId()]
6755 );
6756 while ($row = $this->db->fetchAssoc($result)) {
6757 $times[$row['active_fi']] = $row['started'];
6758 }
6759 return $times;
6760 }
6761
6762 public function getTimeExtensionsOfParticipants(): array
6763 {
6764 $times = [];
6765 $result = $this->db->queryF(
6766 "SELECT tst_addtime.active_fi, tst_addtime.additionaltime FROM tst_addtime, tst_active WHERE tst_addtime.active_fi = tst_active.active_id AND tst_active.test_fi = %s",
6767 ['integer'],
6768 [$this->getTestId()]
6769 );
6770 while ($row = $this->db->fetchAssoc($result)) {
6771 $times[$row['active_fi']] = $row['additionaltime'];
6772 }
6773 return $times;
6774 }
6775
6776 private function getExtraTime(int $active_id): int
6777 {
6778 if ($active_id === 0) {
6779 return 0;
6780 }
6781 return $this->participant_repository
6782 ->getParticipantByActiveId($this->getTestId(), $active_id)
6783 ?->getExtraTime() ?? 0;
6784 }
6785
6786 public function getMaxPassOfTest(): int
6787 {
6788 $query = '
6789 SELECT MAX(tst_pass_result.pass) + 1 max_res
6790 FROM tst_pass_result
6791 INNER JOIN tst_active ON tst_active.active_id = tst_pass_result.active_fi
6792 WHERE test_fi = ' . $this->db->quote($this->getTestId(), 'integer') . '
6793 ';
6794 $res = $this->db->query($query);
6795 $data = $this->db->fetchAssoc($res);
6796 return (int) $data['max_res'];
6797 }
6798
6799 public static function lookupExamId($active_id, $pass)
6800 {
6801 global $DIC;
6802 $ilDB = $DIC['ilDB'];
6803
6804 $exam_id_query = 'SELECT exam_id FROM tst_pass_result WHERE active_fi = %s AND pass = %s';
6805 $exam_id_result = $ilDB->queryF($exam_id_query, [ 'integer', 'integer' ], [ $active_id, $pass ]);
6806 if ($ilDB->numRows($exam_id_result) == 1) {
6807 $exam_id_row = $ilDB->fetchAssoc($exam_id_result);
6808
6809 if ($exam_id_row['exam_id'] != null) {
6810 return $exam_id_row['exam_id'];
6811 }
6812 }
6813
6814 return null;
6815 }
6816
6817 public static function buildExamId($active_id, $pass, $test_obj_id = null): string
6818 {
6819 global $DIC;
6820 $ilSetting = $DIC['ilSetting'];
6821
6822 $inst_id = $ilSetting->get('inst_id', null);
6823
6824 if ($test_obj_id === null) {
6825 $obj_id = self::_getObjectIDFromActiveID($active_id);
6826 } else {
6827 $obj_id = $test_obj_id;
6828 }
6829
6830 $examId = 'I' . $inst_id . '_T' . $obj_id . '_A' . $active_id . '_P' . $pass;
6831
6832 return $examId;
6833 }
6834
6835 public function isShowExamIdInTestPassEnabled(): bool
6836 {
6837 return $this->getMainSettings()->getTestBehaviourSettings()->getExamIdInTestAttemptEnabled();
6838 }
6839
6840 public function isShowExamIdInTestResultsEnabled(): bool
6841 {
6842 return $this->getScoreSettings()->getResultDetailsSettings()->getShowExamIdInTestResults();
6843 }
6844
6845
6846 public function setQuestionSetType(string $question_set_type)
6847 {
6848 $this->main_settings = $this->getMainSettings()->withGeneralSettings(
6849 $this->getMainSettings()->getGeneralSettings()
6850 ->withQuestionSetType($question_set_type)
6851 );
6852 }
6853
6854 public function getQuestionSetType(): string
6855 {
6856 return $this->getMainSettings()->getGeneralSettings()->getQuestionSetType();
6857 }
6858
6864 public function isFixedTest(): bool
6865 {
6866 return $this->getQuestionSetType() == self::QUESTION_SET_TYPE_FIXED;
6867 }
6868
6874 public function isRandomTest(): bool
6875 {
6876 return $this->getQuestionSetType() == self::QUESTION_SET_TYPE_RANDOM;
6877 }
6878
6879 public function getQuestionSetTypeTranslation(ilLanguage $lng, $questionSetType): string
6880 {
6881 switch ($questionSetType) {
6883 return $lng->txt('tst_question_set_type_fixed');
6884
6886 return $lng->txt('tst_question_set_type_random');
6887 }
6888
6889 throw new ilTestException('invalid question set type value given: ' . $questionSetType);
6890 }
6891
6892 public function participantDataExist(): bool
6893 {
6894 if ($this->participantDataExist === null) {
6895 $this->participantDataExist = (bool) $this->evalTotalPersons();
6896 }
6897
6898 return $this->participantDataExist;
6899 }
6900
6901 public function recalculateScores($preserve_manscoring = false)
6902 {
6903 $scoring = new TestScoring($this, $this->user, $this->db, $this->lng);
6904 $scoring->setPreserveManualScores($preserve_manscoring);
6905 $scoring->recalculateSolutions();
6906 }
6907
6908 public static function getTestObjIdsWithActiveForUserId($userId): array
6909 {
6910 global $DIC;
6911 $ilDB = $DIC['ilDB'];
6912
6913 $query = "
6914 SELECT obj_fi
6915 FROM tst_active
6916 INNER JOIN tst_tests
6917 ON test_id = test_fi
6918 WHERE user_fi = %s
6919 ";
6920
6921 $res = $ilDB->queryF($query, ['integer'], [$userId]);
6922
6923 $objIds = [];
6924
6925 while ($row = $ilDB->fetchAssoc($res)) {
6926 $objIds[] = (int) $row['obj_fi'];
6927 }
6928
6929 return $objIds;
6930 }
6931
6932 public function isSkillServiceEnabled(): bool
6933 {
6934 return $this->getMainSettings()->getAdditionalSettings()->getSkillsServiceEnabled();
6935 }
6936
6948 public function isSkillServiceToBeConsidered(): bool
6949 {
6950 if (!$this->getMainSettings()->getAdditionalSettings()->getSkillsServiceEnabled()) {
6951 return false;
6952 }
6953
6954 if (!self::isSkillManagementGloballyActivated()) {
6955 return false;
6956 }
6957
6958 return true;
6959 }
6960
6961 private static $isSkillManagementGloballyActivated = null;
6962
6963 public static function isSkillManagementGloballyActivated(): ?bool
6964 {
6965 if (self::$isSkillManagementGloballyActivated === null) {
6966 $skmgSet = new ilSkillManagementSettings();
6967
6968 self::$isSkillManagementGloballyActivated = $skmgSet->isActivated();
6969 }
6970
6971 return self::$isSkillManagementGloballyActivated;
6972 }
6973
6974 public function isShowGradingStatusEnabled(): bool
6975 {
6976 return $this->getScoreSettings()->getResultSummarySettings()->getShowGradingStatusEnabled();
6977 }
6978
6979 public function isShowGradingMarkEnabled(): bool
6980 {
6981 return $this->getScoreSettings()->getResultSummarySettings()->getShowGradingMarkEnabled();
6982 }
6983
6985 {
6986 return $this->getMainSettings()->getQuestionBehaviourSettings()->getLockAnswerOnNextQuestionEnabled();
6987 }
6988
6990 {
6991 return $this->getMainSettings()->getQuestionBehaviourSettings()->getLockAnswerOnInstantFeedbackEnabled();
6992 }
6993
6994 public function isForceInstantFeedbackEnabled(): ?bool
6995 {
6996 return $this->getMainSettings()->getQuestionBehaviourSettings()->getForceInstantFeedbackOnNextQuestion();
6997 }
6998
6999
7000 public static function isParticipantsLastPassActive(int $test_ref_id, int $user_id): bool
7001 {
7002 global $DIC;
7003 $ilDB = $DIC['ilDB'];
7004 $ilUser = $DIC['ilUser'];
7005
7006 $test_obj = ilObjectFactory::getInstanceByRefId($test_ref_id, false);
7007
7008 $active_id = $test_obj->getActiveIdOfUser($user_id);
7009
7010 $test_session_factory = new ilTestSessionFactory($test_obj, $ilDB, $ilUser);
7011
7012 // Added temporarily bugfix smeyer
7013 $test_session_factory->reset();
7014
7015 $test_sequence_factory = new ilTestSequenceFactory($test_obj, $ilDB, TestDIC::dic()['question.general_properties.repository']);
7016
7017 $test_session = $test_session_factory->getSession($active_id);
7018 $test_sequence = $test_sequence_factory->getSequenceByActiveIdAndPass($active_id, $test_session->getPass());
7019 $test_sequence->loadFromDb();
7020
7021 return $test_sequence->hasSequence();
7022 }
7023
7024 public function adjustTestSequence()
7025 {
7026 $query = "
7027 SELECT COUNT(test_question_id) cnt
7028 FROM tst_test_question
7029 WHERE test_fi = %s
7030 ORDER BY sequence
7031 ";
7032
7033 $questRes = $this->db->queryF($query, ['integer'], [$this->getTestId()]);
7034
7035 $row = $this->db->fetchAssoc($questRes);
7036 $questCount = $row['cnt'];
7037
7038 if ($this->getShuffleQuestions()) {
7039 $query = "
7040 SELECT tseq.*
7041 FROM tst_active tac
7042 INNER JOIN tst_sequence tseq
7043 ON tseq.active_fi = tac.active_id
7044 WHERE tac.test_fi = %s
7045 ";
7046
7047 $partRes = $this->db->queryF(
7048 $query,
7049 ['integer'],
7050 [$this->getTestId()]
7051 );
7052
7053 while ($row = $this->db->fetchAssoc($partRes)) {
7054 $sequence = @unserialize($row['sequence']);
7055
7056 if (!$sequence) {
7057 $sequence = [];
7058 }
7059
7060 $sequence = array_filter($sequence, function ($value) use ($questCount) {
7061 return $value <= $questCount;
7062 });
7063
7064 $num_seq = count($sequence);
7065 if ($questCount > $num_seq) {
7066 $diff = $questCount - $num_seq;
7067 for ($i = 1; $i <= $diff; $i++) {
7068 $sequence[$num_seq + $i - 1] = $num_seq + $i;
7069 }
7070 }
7071
7072 $new_sequence = serialize($sequence);
7073
7074 $this->db->update('tst_sequence', [
7075 'sequence' => ['clob', $new_sequence]
7076 ], [
7077 'active_fi' => ['integer', $row['active_fi']],
7078 'pass' => ['integer', $row['pass']]
7079 ]);
7080 }
7081 } else {
7082 $new_sequence = serialize($questCount > 0 ? range(1, $questCount) : []);
7083
7084 $query = "
7085 SELECT tseq.*
7086 FROM tst_active tac
7087 INNER JOIN tst_sequence tseq
7088 ON tseq.active_fi = tac.active_id
7089 WHERE tac.test_fi = %s
7090 ";
7091
7092 $part_rest = $this->db->queryF(
7093 $query,
7094 ['integer'],
7095 [$this->getTestId()]
7096 );
7097
7098 while ($row = $this->db->fetchAssoc($part_rest)) {
7099 $this->db->update('tst_sequence', [
7100 'sequence' => ['clob', $new_sequence]
7101 ], [
7102 'active_fi' => ['integer', $row['active_fi']],
7103 'pass' => ['integer', $row['pass']]
7104 ]);
7105 }
7106 }
7107 }
7108
7113 {
7114 return ilHtmlPurifierFactory::getInstanceByType('qpl_usersolution');
7115 }
7116
7118 {
7119 return $this->questionrepository;
7120 }
7121
7123 {
7124 return $this->global_settings_repo->getGlobalSettings();
7125 }
7126
7128 {
7129 if (!$this->main_settings) {
7130 $this->main_settings = $this->getMainSettingsRepository()
7131 ->getFor($this->getTestId());
7132 }
7133 return $this->main_settings;
7134 }
7135
7137 {
7138 if (!$this->main_settings_repo) {
7139 $this->main_settings_repo = new MainSettingsDatabaseRepository($this->db);
7140 }
7141 return $this->main_settings_repo;
7142 }
7143
7145 {
7146 if (!$this->score_settings) {
7147 $this->score_settings = $this->getScoreSettingsRepository()
7148 ->getFor($this->getTestId());
7149 }
7150 return $this->score_settings;
7151 }
7152
7154 {
7155 if (!$this->score_settings_repo) {
7156 $this->score_settings_repo = new ScoreSettingsDatabaseRepository($this->db);
7157 }
7158 return $this->score_settings_repo;
7159 }
7160
7161 public function addToNewsOnOnline(
7162 bool $old_online_status,
7163 bool $new_online_status
7164 ): void {
7165 if (!$old_online_status && $new_online_status) {
7166 $newsItem = new ilNewsItem();
7167 $newsItem->setContext($this->getId(), 'tst');
7168 $newsItem->setPriority(NEWS_NOTICE);
7169 $newsItem->setTitle('new_test_online');
7170 $newsItem->setContentIsLangVar(true);
7171 $newsItem->setContent('');
7172 $newsItem->setUserId($this->user->getId());
7173 $newsItem->setVisibility(NEWS_USERS);
7174 $newsItem->create();
7175 return;
7176 }
7177
7178 if ($old_online_status && !$new_online_status) {
7179 ilNewsItem::deleteNewsOfContext($this->getId(), 'tst');
7180 return;
7181 }
7182
7183 $newsId = ilNewsItem::getFirstNewsIdForContext($this->getId(), 'tst');
7184 if (!$new_online_status && $newsId > 0) {
7185 $newsItem = new ilNewsItem($newsId);
7186 $newsItem->setTitle('new_test_online');
7187 $newsItem->setContentIsLangVar(true);
7188 $newsItem->setContent('');
7189 $newsItem->update();
7190 }
7191 }
7192
7196 public static function _lookupRandomTest(int $obj_id): bool
7197 {
7198 global $DIC;
7199
7200 $query = 'SELECT question_set_type FROM tst_tests WHERE obj_fi = %s';
7201
7202 $res = $DIC['ilDB']->queryF($query, ['integer'], [$obj_id]);
7203
7204 $question_set_type = null;
7205
7206 while ($row = $DIC['ilDB']->fetchAssoc($res)) {
7207 $question_set_type = $row['question_set_type'];
7208 }
7209
7210 return $question_set_type === self::QUESTION_SET_TYPE_RANDOM;
7211 }
7212
7213 public function getVisitingTimeOfParticipant(int $active_id): array
7214 {
7215 return $this->participant_repository->getFirstAndLastVisitForActiveId($active_id);
7216 }
7217}
$id
plugin.php for ilComponentBuildPluginInfoObjectiveTest::testAddPlugins
Definition: plugin.php:23
$filename
Definition: buildRTE.php:78
Builds data types.
Definition: Factory.php:36
updatePassAndTestResults(array $active_ids)
A class defining mark schemas for assessment test objects.
Definition: MarkSchema.php:36
A class defining marks for assessment test objects.
Definition: Mark.php:36
withGeneralSettings(SettingsGeneral $settings)
withConcludingRemarksPageId(?int $concluding_remarks_page_id)
withGamificationSettings(SettingsGamification $settings)
raiseError(string $a_msg, int $a_err_obj)
wrapper for downward compability
const IL_CAL_UNIX
return true
const NEWS_USERS
const NEWS_NOTICE
static _getSolutionMaxPass(int $question_id, int $active_id)
Returns the maximum pass a users question solution.
static _getSuggestedSolutionOutput(int $question_id)
static instantiateQuestion(int $question_id)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static completeMissingPluginName(array $question_type_data)
Class ilBenchmark.
static formatDate(ilDateTime $date, bool $a_skip_day=false, bool $a_include_wd=false, bool $include_seconds=false, ?ilObjUser $user=null,)
@classDescription Date and time handling
static getASCIIFilename(string $a_filename)
static makeDir(string $a_dir)
creates a new directory and inherits all filesystem permissions of the parent directory You may pass ...
static ilTempnam(?string $a_temp_path=null)
Returns a unique and non existing Path for e temporary file or directory.
static delDir(string $a_dir, bool $a_clean_only=false)
removes a dir and all its content (subdirs and files) recursively
static getDataDir()
get data directory (outside webspace)
static createDirectory(string $a_dir, int $a_mod=0755)
create directory
static removeTrailingPathSeparators(string $path)
static getInstanceByType(string $type)
static _refreshStatus(int $a_obj_id, ?array $a_users=null)
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)
A news item can be created by different sources.
static getFirstNewsIdForContext(int $a_context_obj_id, string $a_context_obj_type, int $a_context_sub_obj_id=0, string $a_context_sub_obj_type="")
Get first new id of news set related to a certain context.
static deleteNewsOfContext(int $a_context_obj_id, string $a_context_obj_type, int $a_context_sub_obj_id=0, string $a_context_sub_obj_type="")
Delete all news of a context.
Class ilObjFile.
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 _saveTempFileAsMediaObject(string $name, string $tmp_name, bool $upload=true)
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 _getAvailableQuestionpools(bool $use_object_id=false, bool $equal_points=false, bool $could_be_offline=false, bool $showPath=false, bool $with_questioncount=false, string $permission='read', int $usr_id=0)
Returns the available question pools for the active user.
isNextPassAllowed(ilTestPassesSelector $testPassesSelector, int &$next_pass_allowed_timestamp)
setQuestionOrder(array $order)
static _getResultPass($active_id)
Retrieves the pass number that should be counted for a given user.
addConcludingRemarksToSettingsFromImport(SettingsFinishing $settings, array $material, string $importdir, array $mappings)
static _getObjectIDFromActiveID($active_id)
Returns the ILIAS test object id for a given active id.
static _instanciateQuestion($question_id)
Creates an instance of a question with a given question id.
getUserData($ids)
Returns a data of all users specified by id list.
buildIso8601PeriodForExportCompatibility(DateTimeImmutable $date_time)
getTestId()
Gets the database id of the additional test data.
endingTimeReached()
Returns true if the ending time of a test is reached An ending time is not available for self assessm...
setTestId($a_id)
Sets the test ID.
getTotalPointsPassedArray()
Returns an array with the total points of all users who passed the test This array could be used for ...
getShowKioskModeParticipant()
getHighscoreWTime()
Gets if the column with the workingtime should be shown.
getHighscoreOwnTable()
Gets if the own rankings table should be shown.
getAnsweredQuestionCount($active_id, $pass=null)
Retrieves the number of answered questions for a given user in a given test.
hasAnyTestResult(ilTestSession $test_session)
getHighscoreTopTable()
Gets, if the top-rankings table should be shown.
isShowExamIdInTestPassEnabled()
RequestDataCollector $testrequest
removeQuestionsWithResults(array $question_ids)
canShowSolutionPrintview($user_id=null)
& getExistingQuestions($pass=null)
Get the id's of the questions which are already part of the test.
saveToDb(bool $properties_only=false)
static _getTestIDFromObjectID($object_id)
Returns the ILIAS test id for a given object id.
pcArrayShuffle($array)
Shuffles the values of a given array.
getInstantFeedbackSolution()
getStartingTimeOfUser($active_id, $pass=null)
Returns the unix timestamp of the time a user started a test.
checkQuestionParent(int $question_id)
& getInvitedUsers(int $user_id=0, $order="login, lastname, firstname")
Returns a list of all invited users in a test.
getDetailedTestResults($participants)
returns all test results for all participants
isNrOfTriesReached($tries)
returns if number of tries are reached
static _lookupRandomTest(int $obj_id)
reindexFixedQuestionOrdering()
static getTestObjIdsWithActiveForUserId($userId)
getAllQuestions($pass=null)
Returns all questions of a test in test order.
getPotentialRandomTestQuestions()
getScoreSettingsRepository()
isPreviousSolutionReuseEnabled($active_id)
inviteUser($user_id, $client_ip="")
Invites a user to a test.
static _getCountSystem($active_id)
removeTestResultsFromSoapLpAdministration(array $user_ids)
replaceFilesInPageImports(string $text, array $mappings)
getQuestiontext($question_id)
Returns the question text for a given question.
loadQuestions(int $active_id=0, ?int $pass=null)
Load the test question id's from the database.
LOMetadata $lo_metadata
const QUESTION_SET_TYPE_RANDOM
getQuestionTitlesAndIndexes()
Returns the titles of the test questions in question sequence.
static _getScoreCutting(int $active_id)
Determines if the score of a question should be cut at 0 points or the score of the whole test.
processPrintoutput2FO($print_output)
Convert a print output to XSL-FO.
sendSimpleNotification($active_id)
getXMLZip()
Get zipped xml file for test.
getShowSolutionListComparison()
static lookupExamId($active_id, $pass)
createExportDirectory()
creates data directory for export files (data_dir/tst_data/tst_<id>/export, depending on data directo...
ExportImportFactory $export_factory
getAnonOnlyParticipantIds()
return int[]
static _getActiveIdOfUser($user_id="", $test_id="")
removeQuestionFromSequences(int $question_id, array $active_ids, ilTestReindexedSequencePositionMap $reindexed_sequence_position_map)
exportXMLPageObjects(&$a_xml_writer, $inst, &$expLog)
export page objects to xml (see ilias_co.dtd)
storeMarkSchema(MarkSchema $mark_schema)
& createTestSequence($active_id, $pass, $shuffle)
createQuestionGUI($question_type, $question_id=-1)
Creates a question GUI instance of a given question type.
sendAdvancedNotification(int $active_id)
questionMoveDown($question_id)
Moves a question down in order.
isMaxProcessingTimeReached(int $starting_time, int $active_id)
Returns whether the maximum processing time for a test is reached or not.
$metadata
A reference to an IMS compatible matadata set.
bool $activation_limited
addToNewsOnOnline(bool $old_online_status, bool $new_online_status)
getExtraTime(int $active_id)
getWorkingTimeOfParticipantForPass(int $active_id, int $pass)
@depracated 11, will be removed in 12, use TestResultManager::fetchWorkingTime instead
ilTestParticipantAccessFilterFactory $participant_access_filter
hasQuestionsWithoutQuestionpool()
exportFileItems($target_dir, &$expLog)
export files of file itmes
getQuestionsOfTest(int $active_id)
getImagePath()
Returns the image path for web accessable images of a test The image path is under the CLIENT_WEB_DIR...
getShowSolutionSignature()
Returns if the signature field should be shown in the test results.
getHighscoreAnon()
Gets if the highscores should be anonymized per setting.
exportXMLMediaObjects(&$a_xml_writer, $a_inst, $a_target_dir, &$expLog)
export media objects to xml (see ilias_co.dtd)
getPassScoring()
Gets the pass scoring type.
ScoreSettingsRepository $score_settings_repo
isShowGradingStatusEnabled()
isHighscoreAnon()
Gets if the highscores should be displayed anonymized.
ilSetting $settings
getShowSolutionAnswersOnly()
Returns if the full solution (including ILIAS content) should be presented to the solution or not.
MarksRepository $marks_repository
startingTimeReached()
Returns true if the starting time of a test is reached A starting time is not available for self asse...
getProcessingTimeInSeconds(int $active_id=0)
getShowPassDetails()
Returns if the pass details should be shown when a test is not finished.
getQuestionSetTypeTranslation(ilLanguage $lng, $questionSetType)
Filesystem $filesystem_web
applyDefaults(array $test_defaults)
getQuestionsOfPass(int $active_id, int $pass)
static _lookupAuthor($obj_id)
Gets the authors name of the ilObjTest object.
const INVITATION_OFF
getShowSolutionFeedback()
Returns if the feedback should be presented to the solution or not.
$test_sequence
contains the test sequence data
getTimeExtensionsOfParticipants()
int $tmpCopyWizardCopyId
const QUESTION_SET_TYPE_FIXED
duplicateQuestionForTest($question_id)
Takes a question and creates a copy of the question for use in the test.
_getLastAccess(int $active_id)
isActiveTestSubmitted($user_id=null)
returns if the active for user_id has been submitted
getTestResult(int $active_id, ?int $attempt=null, bool $ordered_sequence=false, bool $consider_hidden_questions=true, bool $consider_optional_questions=true)
Calculates the results of a test for a given user and returns an array with all test results.
bool $activation_visibility
evalStatistical($active_id)
Returns the statistical evaluation of the test for a specified user.
cloneObject(int $target_id, int $copy_id=0, bool $omit_tree=false)
Clone object.
isExecutable($test_session, $user_id, $allow_pass_increase=false)
Checks if the test is executable by the given user.
addIntroductionToSettingsFromImport(SettingsIntroduction $settings, array $material, string $importdir, array $mappings)
static _getPass($active_id)
Retrieves the actual pass of a given user for a given test.
getAuthor()
Gets the authors name of the ilObjTest object.
getFixedQuestionSetTotalPoints()
isTestQuestion(int $question_id)
getScoreCutting()
Determines if the score of a question should be cut at 0 points or the score of the whole test.
getHighscorePercentage()
Gets if the percentage column should be shown.
getQuestionCountWithoutReloading()
addDefaults($a_name)
Adds the defaults of this test to the test defaults.
prepareTextareaOutput($txt_output, $prepare_for_latex_output=false, $omitNl2BrWhenTextArea=false)
Prepares a string for a text area output in tests.
getTestParticipantsForManualScoring($filter=null)
getHtmlQuestionContentPurifier()
getAvailableQuestions($arr_filter, $completeonly=0)
Calculates the available questions for a test.
static _getObjectIDFromTestID($test_id)
Returns the ILIAS test object id for a given test id.
fromXML(ilQTIAssessment $assessment, array $mappings)
Receives parameters from a QTI parser and creates a valid ILIAS test object.
isSkillServiceToBeConsidered()
Returns whether this test must consider skills, usually by providing appropriate extensions in the us...
modifyExportIdentifier($a_tag, $a_param, $a_value)
Returns the installation id for a given identifier.
lookupQuestionSetTypeByActiveId(int $active_id)
buildDateTimeImmutableFromPeriod(?string $period)
updateWorkingTime($times_id)
Update the working time of a test when a question is answered.
canShowTestResults(ilTestSession $test_session)
removeQuestion(int $question_id)
static isSkillManagementGloballyActivated()
evalTotalStartedAverageTime(?array $active_ids_to_filter=null)
addQTIMaterial(ilXmlWriter &$xml_writer, ?int $page_id, string $material='')
buildStatisticsAccessFilteredParticipantList()
$evaluation_data
Contains the evaluation data settings the tutor defines for the user.
getGenericAnswerFeedback()
static _getUserIdFromActiveId(int $active_id)
doCreateMetaData()
@inheritDoc
MarkSchema $mark_schema
const SCORE_LAST_PASS
static _getMaxPass($active_id)
Retrieves the maximum pass of a given user for a given test in which the user answered at least one q...
isRandomTest()
Returns the fact wether this test is a random questions test or not.
getQuestionType($question_id)
Returns the question type of a question with a given id.
getShowSolutionPrintview()
Returns if the solution printview should be presented to the user or not.
setAccessFilteredParticipantList(ilTestParticipantList $access_filtered_participant_list)
ilCtrlInterface $ctrl
MainSettings $main_settings
removeTestResultsByUserIds(array $user_ids)
getAvailableDefaults()
Returns the available test defaults for the active user.
setQuestionSetSolved($value, $question_id, $user_id)
sets question solved state to value for given user_id
isFixedTest()
Returns the fact wether this test is a fixed question set test or not.
isComplete(ilTestQuestionSetConfig $test_question_set_config)
replaceMobsInPageImports(string $text, array $mappings)
& _getCompleteWorkingTimeOfParticipants($test_id)
Returns the complete working time in seconds for all test participants.
getTestDefaults($test_defaults_id)
deleteDefaults($test_default_id)
convertTimeToDateTimeImmutableIfNecessary(DateTimeImmutable|int|null $date_time)
isTestFinished($active_id)
returns if the active for user_id has been submitted
static _getBestPass($active_id)
Retrieves the best pass of a given user for a given test.
static getSingleManualFeedback(int $active_id, int $question_id, int $pass)
getQuestionDataset($question_id)
Returns the dataset for a given question id.
insertQuestion(int $question_id, bool $link_only=false)
string $author
int $activation_starting_time
getHighscoreAchievedTS()
Returns if date and time of the scores achievement should be displayed.
getHighscoreTopNum(int $a_retval=10)
Gets the number of entries which are to be shown in the top-rankings table.
Repository $test_result_repository
isHTML($a_text)
Checks if a given string contains HTML or not.
& getWorkedQuestions($active_id, $pass=null)
Gets the id's of all questions a user already worked through.
getListOfQuestionsSettings()
Returns the settings for the list of questions options in the test properties This could contain one ...
startWorkingTime($active_id, $pass)
Write the initial entry for the tests working time to the database.
isFollowupQuestionAnswerFixationEnabled()
Refinery $refinery
getCompleteWorkingTime($user_id)
Returns the complete working time in seconds a user worked on the test.
getQuestionTitle($title, $nr=null, $points=null)
Returns the title of a test question and checks if the title output is allowed.
static buildExamId($active_id, $pass, $test_obj_id=null)
const INVITATION_ON
setTemplate(int $template_id)
getAllTestResults($participants)
returns all test results for all participants
GlobalSettingsRepository $global_settings_repo
static _getPassScoring(int $active_id)
Gets the pass scoring type.
deliverPDFfromHTML($content, $title=null)
Delivers a PDF file from XHTML.
const SCORE_BEST_PASS
buildName(?int $user_id, ?string $firstname, ?string $lastname)
Builds a user name for the output depending on test type and existence of the user.
getImagePathWeb()
Returns the web image path for web accessable images of a test The image path is under the web access...
isShowExamIdInTestResultsEnabled()
static _createImportDirectory()
creates data directory for import files (data_dir/tst_data/tst_<id>/import, depending on data directo...
create()
note: title, description and type should be set when this function is called
retrieveMobsFromLegacyImports(string $text, array $mobs, string $importdir)
static _getAvailableTests($use_object_id=false)
Returns the available tests for the active user.
ilBenchmark $bench
ilTestQuestionSetConfigFactory $question_set_config_factory
getCompleteWorkingTimeOfParticipant($active_id)
Returns the complete working time in seconds for a test participant.
getCompleteEvaluationData($filterby='', $filtertext='')
evalTotalPersons()
Returns the number of persons who started the test.
getUnfilteredEvaluationData()
removeTestActives(array $active_ids)
removeTestResults(ilTestParticipantData $participant_data)
isInstantFeedbackAnswerFixationEnabled()
evalTotalParticipantsArray(string $name_sort_order='asc')
saveManualFeedback(int $active_id, int $question_id, int $pass, ?string $feedback, bool $finalized=false)
copyQuestions(array $question_ids)
array $questions
saveCompleteStatus(ilTestQuestionSetConfig $test_question_set_config)
bool $print_best_solution_with_result
saveAuthorToMetadata($author="")
Saves an authors name into the lifecycle metadata if no lifecycle metadata exists This will only be c...
isPluginActive($a_pname)
Checks wheather or not a question plugin with a given name is active.
evalTotalPersonsArray(string $name_sort_order='asc')
deliverPDFfromFO($fo, $title=null)
Delivers a PDF file from a XSL-FO string.
static getManualFeedback(int $active_id, int $question_id, ?int $pass)
Retrieves the feedback comment for a question in a test if it is finalized.
GeneralQuestionPropertiesRepository $questionrepository
toXML()
Returns a QTI xml representation of the test.
static lookupPassResultsUpdateTimestamp($active_id, $pass)
ilComponentFactory $component_factory
setTmpCopyWizardCopyId(int $tmpCopyWizardCopyId)
$participantDataExist
holds the fact wether participant data exists or not DO NOT USE TIS PROPERTY DRIRECTLY ALWAYS USE ilO...
getHighscoreScore()
Gets if the score column should be shown.
static isParticipantsLastPassActive(int $test_ref_id, int $user_id)
isBlockPassesAfterPassedEnabled()
getConcludingRemarksPageId()
ilComponentRepository $component_repository
static _lookupFinishedUserTests($a_user_id)
Gather all finished tests for user.
getAvailableQuestionpools(bool $use_object_id=false, ?bool $equal_points=false, bool $could_be_offline=false, bool $show_path=false, bool $with_questioncount=false, string $permission='read')
Returns the available question pools for the active user.
getImportMapping()
get array of (two) new created questions for import id
insertManualFeedback(int $active_id, int $question_id, int $pass, ?string $feedback, bool $finalized, array $feedback_old)
ScoreSettings $score_settings
TestLogger $logger
secondsToHoursMinutesSecondsString(int $seconds)
getVisitingTimeOfParticipant(int $active_id)
getCompleteManualFeedback(int $question_id)
Retrieves the manual feedback for a question in a test.
getListOfQuestionsDescription()
ilTestParticipantList $access_filtered_participant_list
getActiveIdOfUser($user_id="", $anonymous_id="")
Gets the active id of a given user.
removeTestResultsByActiveIds(array $active_ids)
recalculateScores($preserve_manscoring=false)
TestManScoringDoneHelper $test_man_scoring_done_helper
qtiMaterialToArray($a_material)
Reads an QTI material tag and creates a text string.
moveQuestions(array $move_questions, int $target_index, int $insert_mode)
getGeneralQuestionPropertiesRepository()
static _getSolvedQuestions($active_id, $question_fi=null)
get solved questions
static _lookupAnonymity($a_obj_id)
ParticipantRepository $participant_repository
getJavaScriptOutput()
Returns if Javascript should be chosen for drag & drop actions for the active user.
setQuestionSetType(string $question_set_type)
getTextAnswer($active_id, $question_id, $pass=null)
Returns the text answer of a given user for a given question.
userLookupFullName($user_id, $overwrite_anonymity=false, $sorted_order=false, $suffix="")
Returns the full name of a test user according to the anonymity status.
getParticipants()
Returns all persons who started the test.
getNrOfResultsForPass($active_id, $pass)
Calculates the number of user results for a specific test pass.
int $activation_ending_time
getStartingTimeOfParticipants()
Note, this function should only be used if absolutely necessary, since it perform joins on tables tha...
getShowSolutionListOwnAnswers()
getAccessFilteredParticipantList()
removeQuestions(array $question_ids)
getQuestionCountAndPointsForPassOfParticipant(int $active_id, int $pass)
isTestFinishedToViewResults($active_id, $currentpass)
Returns true if an active user completed a test pass and did not start a new pass.
removeQuestionWithResults(int $question_id, TestScoring $scoring)
hasNrOfTriesRestriction()
returns if the numbers of tries have to be checked
clonePage(int $source_page_id)
TestLogViewer $log_viewer
getParticipantsForTestAndQuestion($test_id, $question_id)
Creates an associated array with all active id's for a given test and original question id.
getTitleFilenameCompliant()
returns the object title prepared to be used as a filename
questionMoveUp($question_id)
Moves a question up in order.
MainSettingsRepository $main_settings_repo
isForceInstantFeedbackEnabled()
& getCompleteWorkingTimeOfParticipants()
Returns the complete working time in seconds for all test participants.
static _lookupTestObjIdForQuestionId(int $q_id)
Get test Object ID for question ID.
exportPagesXML(&$a_xml_writer, $a_inst, $a_target_dir, &$expLog)
export pages of test to xml (see ilias_co.dtd)
static _lookupName(int $a_user_id)
static getInstanceByRefId(int $ref_id, bool $stop_on_error=true)
get an instance of an Ilias object by reference id
static getInstance(int $obj_id)
Class ilObject Basic functions for all objects.
string $title
static _prepareCloneSelection(array $ref_ids, string $new_type, bool $show_path=true)
Prepare copy wizard object selection.
Properties $object_properties
ILIAS $ilias
static _lookupObjId(int $ref_id)
static _lookupTitle(int $obj_id)
static _lookupDescription(int $obj_id)
static collectFileItems(ilPageObject $a_page, DOMDocument $a_domdoc)
Get all file items that are used within the page.
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 factory(string $a_package, int $a_timeout=0)
Creates an ilRpcClient instance to our ilServer.
static get(string $a_var)
static clear(string $a_var)
ILIAS Setting Class.
special template class to simplify handling of ITX/PEAR
Base Exception for all Exceptions relating to Modules/Test.
Class ilTestMailNotification.
@ilCtrl_Calls ilTestPageGUI: ilPageEditorGUI, ilEditClipboardGUI, ilMDEditorGUI @ilCtrl_Calls ilTestP...
@ilCtrl_Calls ilTestEditPageGUI: ilPageEditorGUI, ilEditClipboardGUI, ilMDEditorGUI @ilCtrl_Calls ilT...
Test sequence handler.
static isManScoringDone(int $active_id)
Test session handler.
static getStyleSheetLocation(string $mode="output", string $a_css_name="")
get full style sheet file name (path inclusive) of current user
static insertInstIntoID(string $a_value)
inserts installation id into ILIAS id
static _getObjectsByOperations( $a_obj_type, string $a_operation, int $a_usr_id=0, int $limit=0)
Get all objects of a specific type and check access This function is not recursive,...
static deliverData(string $a_data, string $a_filename, string $mime="application/octet-stream")
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
const ANONYMOUS_USER_ID
Definition: constants.php:27
return['delivery_method'=> 'php',]
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
$info
Definition: entry_point.php:21
$txt
Definition: error.php:31
const ILIAS_VERSION
The filesystem interface provides the public interface for the Filesystem service API consumer.
Definition: Filesystem.php:37
Readable part of repository interface to ilComponentDataDB.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Interface for html sanitizing functionality.
$ref_id
Definition: ltiauth.php:66
if(! $DIC->user() ->getId()||!ilLTIConsumerAccess::hasCustomProviderCreationAccess()) $params
Definition: ltiregstart.php:31
$path
Definition: ltiservices.php:30
$res
Definition: ltiservices.php:69
__construct(Container $dic, ilPlugin $plugin)
@inheritDoc
get(string $class_name)
buildImportDirectoryFromImportFile(string $file_to_import)
Class ilObjForumAdministration.
global $lng
Definition: privfeed.php:31
global $ilSetting
Definition: privfeed.php:31
$results
if(!file_exists('../ilias.ini.php'))
global $DIC
Definition: shib_login.php:26
$counter
$objectives
getGeneralSettings()