ILIAS  trunk Revision v12.0_alpha-1227-g7ff6d300864
class.ilObjTest.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
23use ILIAS\ResourceStorage\Services as ResourceStorage;
31use ILIAS\Test\ExportImport\Factory as ExportImportFactory;
40use ILIAS\Test\Settings\GlobalSettings\Repository as GlobalSettingsRepository;
52use ILIAS\Refinery\Factory as Refinery;
55
66class ilObjTest extends ilObject
67{
69
70 public const QUESTION_SET_TYPE_FIXED = 'FIXED_QUEST_SET';
71 public const QUESTION_SET_TYPE_RANDOM = 'RANDOM_QUEST_SET';
72 public const INVITATION_OFF = 0;
73 public const INVITATION_ON = 1;
74 public const SCORE_LAST_PASS = 0;
75 public const SCORE_BEST_PASS = 1;
76
77 private ?bool $activation_limited = null;
78 private array $mob_ids;
79 private array $file_ids = [];
80 private bool $online;
84 private ?MarkSchema $mark_schema = null;
85 public int $test_id = -1;
87 public string $author;
88
92 public $metadata;
93 public array $questions = [];
94
99
103 public $test_sequence = false;
104
105 private int $template_id = 0;
106
107 protected bool $print_best_solution_with_result = true;
108
109 protected bool $activation_visibility = false;
110 protected ?int $activation_starting_time = null;
111 protected ?int $activation_ending_time = null;
112
118 private $participantDataExist = null;
119
120 private ?int $tmpCopyWizardCopyId = null;
121
124 protected Refinery $refinery;
128
129 protected GlobalSettingsRepository $global_settings_repo;
130 protected ?MainSettings $main_settings = null;
135
139
140 protected ExportImportFactory $export_factory;
141
145
149
150 protected LOMetadata $lo_metadata;
152 protected ResourceStorage $irss;
153
160 public function __construct(int $id = 0, bool $a_call_by_reference = true)
161 {
162 $this->type = "tst";
163
165 global $DIC;
166 $this->ctrl = $DIC['ilCtrl'];
167 $this->refinery = $DIC['refinery'];
168 $this->settings = $DIC['ilSetting'];
169 $this->bench = $DIC['ilBench'];
170 $this->component_repository = $DIC['component.repository'];
171 $this->component_factory = $DIC['component.factory'];
172 $this->filesystem_web = $DIC->filesystem()->web();
173 $this->lo_metadata = $DIC->learningObjectMetadata();
174 $this->media_object_repository = $DIC->mediaObjects()->internal()->repo()->mediaObject();
175 $this->irss = $DIC->resourceStorage();
176
177 $local_dic = $this->getLocalDIC();
178 $this->participant_access_filter = $local_dic['participant.access_filter.factory'];
179 $this->test_man_scoring_done_helper = $local_dic['scoring.manual.done_helper'];
180 $this->logger = $local_dic['logging.logger'];
181 $this->log_viewer = $local_dic['logging.viewer'];
182 $this->global_settings_repo = $local_dic['settings.global.repository'];
183 $this->marks_repository = $local_dic['marks.repository'];
184 $this->settings_factory = $local_dic['settings.factory'];
185 $this->questionrepository = $local_dic['question.general_properties.repository'];
186 $this->testrequest = $local_dic['request_data_collector'];
187 $this->participant_repository = $local_dic['participant.repository'];
188 $this->export_factory = $local_dic['exportimport.factory'];
189 $this->test_result_repository = $local_dic['results.data.repository'];
190 $this->main_settings_repository = $local_dic['settings.main.repository'];
191 $this->score_settings_repository = $local_dic['settings.scoring.repository'];
192
193 parent::__construct($id, $a_call_by_reference);
194
195 $this->lng->loadLanguageModule("assessment");
196 $this->score_settings = null;
197
198 $this->question_set_config_factory = new ilTestQuestionSetConfigFactory(
199 $this->tree,
200 $this->db,
201 $this->lng,
202 $this->logger,
203 $this->component_repository,
204 $this,
205 $this->questionrepository
206 );
207 }
208
209 public function getLocalDIC(): TestDIC
210 {
211 return TestDIC::dic();
212 }
213
214 public function getTestLogger(): TestLogger
215 {
216 return $this->logger;
217 }
218
220 {
221 return $this->log_viewer;
222 }
223
225 {
226 return $this->question_set_config_factory->getQuestionSetConfig();
227 }
228
232 public function getTitleFilenameCompliant(): string
233 {
235 }
236
237 public function getTmpCopyWizardCopyId(): ?int
238 {
240 }
241
242 public function setTmpCopyWizardCopyId(int $tmpCopyWizardCopyId): void
243 {
244 $this->tmpCopyWizardCopyId = $tmpCopyWizardCopyId;
245 }
246
247 public function create(): int
248 {
249 $id = parent::create();
250 $this->createMetaData();
251 return $id;
252 }
253
254 public function update(): bool
255 {
256 if (!parent::update()) {
257 return false;
258 }
259
260 // put here object specific stuff
261 $this->updateMetaData();
262 return true;
263 }
264
265 public function read(): void
266 {
267 parent::read();
268 $this->main_settings = null;
269 $this->score_settings = null;
270 $this->mark_schema = null;
271 $this->loadFromDb();
272 }
273
274 public function delete(): bool
275 {
276 // always call parent delete function first!!
277 if (!parent::delete()) {
278 return false;
279 }
280
281 // delet meta data
282 $this->deleteMetaData();
283
284 //put here your module specific stuff
285 $this->deleteTest();
286
287 $qsaImportFails = new ilAssQuestionSkillAssignmentImportFails($this->getId());
288 $qsaImportFails->deleteRegisteredImportFails();
289 $sltImportFails = new ilTestSkillLevelThresholdImportFails($this->getId());
290 $sltImportFails->deleteRegisteredImportFails();
291
292 if ($this->logger->isLoggingEnabled()) {
293 $this->logger->logTestAdministrationInteraction(
294 $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
295 $this->getRefId(),
296 $this->user->getId(),
298 [
299 AdditionalInformationGenerator::KEY_TEST_TITLE => $test_title = $this->title
300 ]
301 )
302 );
303 }
304
305 return true;
306 }
307
308 public function deleteTest(): void
309 {
310 $participantData = new ilTestParticipantData($this->db, $this->lng);
311 $participantData->load($this->getTestId());
312 $this->removeTestResults($participantData);
313
314 $this->db->manipulateF(
315 "DELETE FROM tst_mark WHERE test_fi = %s",
316 ['integer'],
317 [$this->getTestId()]
318 );
319
320 $this->db->manipulateF(
321 "DELETE FROM tst_tests WHERE test_id = %s",
322 ['integer'],
323 [$this->getTestId()]
324 );
325
326 $this->db->manipulateF(
327 "DELETE FROM tst_test_settings WHERE id = %s",
328 ['integer'],
329 [$this->getMainSettings()->getId()]
330 );
331
332 $tst_data_dir = ilFileUtils::getDataDir() . "/tst_data";
333 $directory = $tst_data_dir . "/tst_" . $this->getId();
334 if (is_dir($directory)) {
335 ilFileUtils::delDir($directory);
336 }
337 $mobs = ilObjMediaObject::_getMobsOfObject("tst:html", $this->getId());
338 // remaining usages are not in text anymore -> delete them
339 // and media objects (note: delete method of ilObjMediaObject
340 // checks whether object is used in another context; if yes,
341 // the object is not deleted!)
342 foreach ($mobs as $mob) {
343 ilObjMediaObject::_removeUsage($mob, "tst:html", $this->getId());
344 if (ilObjMediaObject::_exists($mob)) {
345 $mob_obj = new ilObjMediaObject($mob);
346 $mob_obj->delete();
347 }
348 }
349 }
350
356 public function createExportDirectory(): void
357 {
358 $tst_data_dir = ilFileUtils::getDataDir() . "/tst_data";
359 ilFileUtils::makeDir($tst_data_dir);
360 if (!is_writable($tst_data_dir)) {
361 $this->ilias->raiseError("Test Data Directory (" . $tst_data_dir
362 . ") not writeable.", $this->ilias->error_obj->MESSAGE);
363 }
364
365 // create learning module directory (data_dir/lm_data/lm_<id>)
366 $tst_dir = $tst_data_dir . "/tst_" . $this->getId();
367 ilFileUtils::makeDir($tst_dir);
368 if (!@is_dir($tst_dir)) {
369 $this->ilias->raiseError("Creation of Test Directory failed.", $this->ilias->error_obj->MESSAGE);
370 }
371 // create Export subdirectory (data_dir/lm_data/lm_<id>/Export)
372 $export_dir = $tst_dir . "/export";
373 ilFileUtils::makeDir($export_dir);
374 if (!@is_dir($export_dir)) {
375 $this->ilias->raiseError("Creation of Export Directory failed.", $this->ilias->error_obj->MESSAGE);
376 }
377 }
378
379 public function getExportDirectory(): string
380 {
381 $export_dir = ilFileUtils::getDataDir() . "/tst_data" . "/tst_" . $this->getId() . "/export";
382 return $export_dir;
383 }
384
385 public function getExportFiles(string $dir = ''): array
386 {
387 // quit if import dir not available
388 if (!@is_dir($dir) || !is_writable($dir)) {
389 return [];
390 }
391
392 $files = [];
393 foreach (new DirectoryIterator($dir) as $file) {
397 if ($file->isDir()) {
398 continue;
399 }
400
401 $files[] = $file->getBasename();
402 }
403
404 sort($files);
405
406 return $files;
407 }
408
414 public static function _createImportDirectory(): string
415 {
416 global $DIC;
417 $ilias = $DIC['ilias'];
418 $tst_data_dir = ilFileUtils::getDataDir() . "/tst_data";
419 ilFileUtils::makeDir($tst_data_dir);
420
421 if (!is_writable($tst_data_dir)) {
422 $ilias->raiseError("Test Data Directory (" . $tst_data_dir
423 . ") not writeable.", $ilias->error_obj->FATAL);
424 }
425
426 // create test directory (data_dir/tst_data/tst_import)
427 $tst_dir = $tst_data_dir . "/tst_import";
428 ilFileUtils::makeDir($tst_dir);
429 if (!@is_dir($tst_dir)) {
430 $ilias->raiseError("Creation of test import directory failed.", $ilias->error_obj->FATAL);
431 }
432
433 // assert that this is empty and does not contain old data
434 ilFileUtils::delDir($tst_dir, true);
435
436 return $tst_dir;
437 }
438
439 final public function isComplete(ilTestQuestionSetConfig $test_question_set_config): bool
440 {
441 if ($this->getMarkSchema() === null
442 || $this->getMarkSchema()->getMarkSteps() === []) {
443 return false;
444 }
445
446 if (!$test_question_set_config->isQuestionSetConfigured()) {
447 return false;
448 }
449
450 return true;
451 }
452
453 public function saveCompleteStatus(ilTestQuestionSetConfig $test_question_set_config): void
454 {
455 $complete = 0;
456 if ($this->isComplete($test_question_set_config)) {
457 $complete = 1;
458 }
459 if ($this->getTestId() > 0) {
460 $this->db->manipulateF(
461 'UPDATE tst_tests SET complete = %s WHERE test_id = %s',
462 ['text', 'integer'],
463 [$complete, $this->test_id]
464 );
465 }
466 }
467
468 public function saveToDb(bool $properties_only = false): void
469 {
470 if ($this->test_id === -1) {
471 // Create new dataset
472 $next_id = $this->db->nextId('tst_tests');
473
474 $this->db->insert(
475 'tst_tests',
476 [
477 'test_id' => ['integer', $next_id],
478 'obj_fi' => ['integer', $this->getId()],
479 'created' => ['integer', time()],
480 'tstamp' => ['integer', time()],
481 'template_id' => ['integer', $this->getTemplate()]
482 ]
483 );
484
485 $this->test_id = $next_id;
486
487 $this->getMainSettingsRepository()->store(
488 $this->settings_factory->createDefaultMainSettings(),
489 $this->getTestId()
490 );
491 } else {
492 if ($this->evalTotalPersons() > 0) {
493 // reset the finished status of participants if the nr of test passes did change
494 if ($this->getNrOfTries() > 0) {
495 // set all unfinished tests with nr of passes >= allowed passes finished
496 $aresult = $this->db->queryF(
497 "SELECT active_id FROM tst_active WHERE test_fi = %s AND tries >= %s AND submitted = %s",
498 ['integer', 'integer', 'integer'],
499 [$this->getTestId(), $this->getNrOfTries(), 0]
500 );
501 while ($row = $this->db->fetchAssoc($aresult)) {
502 $this->db->manipulateF(
503 "UPDATE tst_active SET submitted = %s, submittimestamp = %s WHERE active_id = %s",
504 ['integer', 'timestamp', 'integer'],
505 [1, date('Y-m-d H:i:s'), $row["active_id"]]
506 );
507 }
508
509 // set all finished tests with nr of passes < allowed passes not finished
510 $aresult = $this->db->queryF(
511 "SELECT active_id FROM tst_active WHERE test_fi = %s AND tries < %s AND submitted = %s",
512 ['integer', 'integer', 'integer'],
513 [$this->getTestId(), $this->getNrOfTries() - 1, 1]
514 );
515 while ($row = $this->db->fetchAssoc($aresult)) {
516 $this->db->manipulateF(
517 "UPDATE tst_active SET submitted = %s, submittimestamp = %s WHERE active_id = %s",
518 ['integer', 'timestamp', 'integer'],
519 [0, null, $row["active_id"]]
520 );
521 }
522 } else {
523 // set all finished tests with nr of passes >= allowed passes not finished
524 $aresult = $this->db->queryF(
525 "SELECT active_id FROM tst_active WHERE test_fi = %s AND submitted = %s",
526 ['integer', 'integer'],
527 [$this->getTestId(), 1]
528 );
529 while ($row = $this->db->fetchAssoc($aresult)) {
530 $this->db->manipulateF(
531 "UPDATE tst_active SET submitted = %s, submittimestamp = %s WHERE active_id = %s",
532 ['integer', 'timestamp', 'integer'],
533 [0, null, $row["active_id"]]
534 );
535 }
536 }
537 }
538 }
539
540 if ($properties_only) {
541 return;
542 }
543
544 if ($this->getQuestionSetType() == self::QUESTION_SET_TYPE_FIXED) {
545 $this->saveQuestionsToDb();
546 }
547
548 $this->marks_repository->storeMarkSchema($this->getMarkSchema());
549 }
550
551 public function saveQuestionsToDb(): void
552 {
553 $this->db->manipulateF(
554 'DELETE FROM tst_test_question WHERE test_fi = %s',
555 ['integer'],
556 [$this->getTestId()]
557 );
558 foreach ($this->questions as $key => $value) {
559 $next_id = $this->db->nextId('tst_test_question');
560 $this->db->insert('tst_test_question', [
561 'test_question_id' => ['integer', $next_id],
562 'test_fi' => ['integer', $this->getTestId()],
563 'question_fi' => ['integer', $value],
564 'sequence' => ['integer', $key],
565 'tstamp' => ['integer', time()]
566 ]);
567 }
568 }
569
573 public function copyQuestions(array $question_ids): void
574 {
575 $copy_count = 0;
576 $question_titles = $this->getQuestionTitles();
577
578 foreach ($question_ids as $id) {
579 $question = assQuestion::instantiateQuestionGUI($id);
580 if ($question) {
581 $title = $question->getObject()->getTitle();
582 $i = 2;
583 while (in_array($title . ' (' . $i . ')', $question_titles)) {
584 $i++;
585 }
586
587 $title .= ' (' . $i . ')';
588
589 $question_titles[] = $title;
590
591 $new_id = $question->getObject()->duplicate(false, $title);
592
593 $clone = assQuestion::instantiateQuestionGUI($new_id);
594 $question = $clone->getObject();
595 $question->setObjId($this->getId());
596 $clone->setObject($question);
597 $clone->getObject()->saveToDb();
598
599 $this->insertQuestion($new_id, true);
600
601 $copy_count++;
602 }
603 }
604 }
605
609 public function getNrOfResultsForPass($active_id, $pass): int
610 {
611 $result = $this->db->queryF(
612 "SELECT test_result_id FROM tst_test_result WHERE active_fi = %s AND pass = %s",
613 ['integer','integer'],
614 [$active_id, $pass]
615 );
616 return $result->numRows();
617 }
618
619 public function loadFromDb(): void
620 {
621 $result = $this->db->queryF(
622 "SELECT test_id FROM tst_tests WHERE obj_fi = %s",
623 ['integer'],
624 [$this->getId()]
625 );
626 if ($result->numRows() === 1) {
627 $data = $this->db->fetchObject($result);
628 $this->setTestId($data->test_id);
629 $this->loadQuestions();
630 }
631 }
632
637 public function loadQuestions(int $active_id = 0, ?int $pass = null): void
638 {
639 $this->questions = [];
640 if ($this->isRandomTest()) {
641 if ($active_id === 0) {
642 $active_id = $this->getActiveIdOfUser($this->user->getId());
643 }
644 if (is_null($pass)) {
645 $pass = self::_getPass($active_id);
646 }
647 $result = $this->db->queryF(
648 'SELECT tst_test_rnd_qst.* '
649 . 'FROM tst_test_rnd_qst, qpl_questions '
650 . 'WHERE tst_test_rnd_qst.active_fi = %s '
651 . 'AND qpl_questions.question_id = tst_test_rnd_qst.question_fi '
652 . 'AND tst_test_rnd_qst.pass = %s '
653 . 'ORDER BY sequence',
654 ['integer', 'integer'],
655 [$active_id, $pass]
656 );
657 } else {
658 $result = $this->db->queryF(
659 'SELECT tst_test_question.* '
660 . 'FROM tst_test_question, qpl_questions '
661 . 'WHERE tst_test_question.test_fi = %s '
662 . 'AND qpl_questions.question_id = tst_test_question.question_fi '
663 . 'ORDER BY sequence',
664 ['integer'],
665 [$this->test_id]
666 );
667 }
668 $index = 1;
669 if ($this->test_id !== -1) {
670 //Omit loading of questions for non-id'ed test
671 while ($data = $this->db->fetchAssoc($result)) {
672 $this->questions[$index++] = $data["question_fi"];
673 }
674 }
675 }
676
677 public function getIntroduction(): string
678 {
679 $page_id = $this->getMainSettings()->getIntroductionSettings()->getIntroductionPageId();
680 return $page_id !== null
681 ? (new ilTestPageGUI('tst', $page_id))->showPage()
682 : '';
683 }
684
685 private function cloneIntroduction(): ?int
686 {
687 $page_id = $this->getMainSettings()->getIntroductionSettings()->getIntroductionPageId();
688 if ($page_id === null) {
689 return null;
690 }
691 return $this->clonePage($page_id);
692 }
693
694 public function getFinalStatement(): string
695 {
696 $page_id = $this->getMainSettings()->getFinishingSettings()->getConcludingRemarksPageId();
697 return $page_id !== null
698 ? (new ilTestPageGUI('tst', $page_id))->showPage()
699 : '';
700 }
701
702 private function cloneConcludingRemarks(): ?int
703 {
704 $page_id = $this->getMainSettings()->getFinishingSettings()->getConcludingRemarksPageId();
705 if ($page_id === null) {
706 return null;
707 }
708 return $this->clonePage($page_id);
709 }
710
711 private function clonePage(int $source_page_id): int
712 {
713 $page_object = new ilTestPage();
714 $page_object->setParentId($this->getId());
715 $new_page_id = $page_object->createPageWithNextId();
716 (new ilTestPage($source_page_id))->copy($new_page_id);
717 return $new_page_id;
718 }
719
723 public function getTestId(): int
724 {
725 return $this->test_id;
726 }
727
728 public function isPostponingEnabled(): bool
729 {
730 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getPostponedQuestionsMoveToEnd();
731 }
732
733 public function isScoreReportingEnabled(): bool
734 {
735 return $this->getScoreSettings()->getResultSummarySettings()->getScoreReporting()->isReportingEnabled();
736 }
737
738 public function getAnswerFeedbackPoints(): bool
739 {
740 return $this->getMainSettings()->getQuestionBehaviourSettings()->getInstantFeedbackPointsEnabled();
741 }
742
743 public function getGenericAnswerFeedback(): bool
744 {
745 return $this->getMainSettings()->getQuestionBehaviourSettings()->getInstantFeedbackGenericEnabled();
746 }
747
748 public function getInstantFeedbackSolution(): bool
749 {
750 return $this->getMainSettings()->getQuestionBehaviourSettings()->getInstantFeedbackSolutionEnabled();
751 }
752
753 public function getCountSystem(): int
754 {
755 return $this->getScoreSettings()->getScoringSettings()->getCountSystem();
756 }
757
761 private static function _getScoreSettingsByActiveId(int $active_id): ScoreSettings
762 {
763 return TestDIC::dic()['settings.scoring.repository']->getFor(
765 );
766 }
767
771 public static function _getCountSystem(int $active_id): int
772 {
773 return self::_getScoreSettingsByActiveId($active_id)->getScoringSettings()->getCountSystem();
774 }
775
779 public function getScoreCutting(): int
780 {
781 return $this->getScoreSettings()->getScoringSettings()->getScoreCutting();
782 }
783
787 public function getPassScoring(): int
788 {
789 return $this->getScoreSettings()->getScoringSettings()->getPassScoring();
790 }
791
795 public static function _getPassScoring(int $active_id): int
796 {
797 return self::_getScoreSettingsByActiveId($active_id)->getScoringSettings()->getPassScoring();
798 }
799
803 public static function _getScoreCutting(int $active_id): bool
804 {
805 return (bool) self::_getScoreSettingsByActiveId($active_id)->getScoringSettings()->getScoreCutting();
806 }
807
808 public function getMarkSchema(): MarkSchema
809 {
810 if ($this->mark_schema === null) {
811 $this->mark_schema = $this->marks_repository->getMarkSchemaFor($this->getTestId());
812 }
813
814 return $this->mark_schema;
815 }
816
818 {
819 $this->marks_repository->storeMarkSchema($mark_schema);
820 $this->mark_schema = null;
821 }
822
823 public function getNrOfTries(): int
824 {
825 return $this->getMainSettings()->getTestBehaviourSettings()->getNumberOfTries();
826 }
827
828 public function isBlockPassesAfterPassedEnabled(): bool
829 {
830 return $this->getMainSettings()->getTestBehaviourSettings()->getBlockAfterPassedEnabled();
831 }
832
833 public function getKioskMode(): bool
834 {
835 return $this->getMainSettings()->getTestBehaviourSettings()->getKioskModeEnabled();
836 }
837
838 public function getShowKioskModeTitle(): bool
839 {
840 return $this->getMainSettings()->getTestBehaviourSettings()->getShowTitleInKioskMode();
841 }
842 public function getShowKioskModeParticipant(): bool
843 {
844 return $this->getMainSettings()->getTestBehaviourSettings()->getShowParticipantNameInKioskMode();
845 }
846
847 public function getUsePreviousAnswers(): bool
848 {
849 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getUsePreviousAnswerAllowed();
850 }
851
852 public function getTitleOutput(): int
853 {
854 return $this->getMainSettings()->getQuestionBehaviourSettings()->getQuestionTitleOutputMode();
855 }
856
857 public function isPreviousSolutionReuseEnabled(): bool
858 {
859 return $this->getUsePreviousAnswers() && $this->user->getPref('tst_use_previous_answers') === '1';
860 }
861
862 public function getProcessingTime(): ?string
863 {
864 return $this->getMainSettings()->getTestBehaviourSettings()->getProcessingTime();
865 }
866
867 private function getProcessingTimeForXML(): string
868 {
869 $processing_time = $this->getMainSettings()->getTestBehaviourSettings()->getProcessingTime();
870 if ($processing_time === null
871 || $processing_time === ''
872 || !preg_match('/(\d{2}):(\d{2}):(\d{2})/is', $processing_time, $matches)
873 ) {
874 return '';
875 }
876
877 return sprintf(
878 "P0Y0M0DT%dH%dM%dS",
879 $matches[1],
880 $matches[2],
881 $matches[3]
882 );
883 }
884
885 public function getProcessingTimeInSeconds(int $active_id = 0): int
886 {
887 $processing_time = $this->getMainSettings()->getTestBehaviourSettings()->getProcessingTime() ?? '';
888 if (preg_match("/(\d{2}):(\d{2}):(\d{2})/", (string) $processing_time, $matches)) {
889 $extratime = $this->getExtraTime($active_id) * 60;
890 return ($matches[1] * 3600) + ($matches[2] * 60) + $matches[3] + $extratime;
891 } else {
892 return 0;
893 }
894 }
895
896 public function getEnableProcessingTime(): bool
897 {
898 return $this->getMainSettings()->getTestBehaviourSettings()->getProcessingTimeEnabled();
899 }
900
901 public function getResetProcessingTime(): bool
902 {
903 return $this->getMainSettings()->getTestBehaviourSettings()->getResetProcessingTime();
904 }
905
906 public function isStartingTimeEnabled(): bool
907 {
908 return $this->getMainSettings()->getAccessSettings()->getStartTimeEnabled();
909 }
910
911 public function getStartingTime(): int
912 {
913 $start_time = $this->getMainSettings()->getAccessSettings()->getStartTime();
914 return $start_time !== null ? $start_time->getTimestamp() : 0;
915 }
916
917 public function isEndingTimeEnabled(): bool
918 {
919 return $this->getMainSettings()->getAccessSettings()->getEndTimeEnabled();
920 }
921
922 public function getEndingTime(): int
923 {
924 $end_time = $this->getMainSettings()->getAccessSettings()->getEndTime();
925 return $end_time !== null ? $end_time->getTimestamp() : 0;
926 }
927
928 public function isPasswordEnabled(): bool
929 {
930 return $this->getMainSettings()->getAccessSettings()->getPasswordEnabled();
931 }
932
933 public function getPassword(): ?string
934 {
935 return $this->getMainSettings()->getAccessSettings()->getPassword();
936 }
937
938 public function removeQuestionsWithResults(array $question_ids): void
939 {
940 $scoring = new TestScoring(
941 $this,
942 $this->user,
943 $this->db,
944 $this->test_result_repository
945 );
946
947 array_walk(
948 $question_ids,
949 fn(int $v, int $k) => $this->removeQuestionWithResults($v, $scoring)
950 );
951 }
952
953 private function removeQuestionWithResults(int $question_id, TestScoring $scoring): void
954 {
955 $question = \assQuestion::instantiateQuestion($question_id);
956
957 $participant_data = new ilTestParticipantData($this->db, $this->lng);
958 $participant_data->load($this->test_id);
959
960 $question->removeAllExistingSolutions();
961 $scoring->removeAllQuestionResults($question_id);
962
963 $this->removeQuestion($question_id);
964 if (!$this->isRandomTest()) {
966 $question_id,
967 $participant_data->getActiveIds(),
968 $this->reindexFixedQuestionOrdering()
969 );
970 }
971
972 $scoring->updatePassAndTestResults($participant_data->getActiveIds());
973 ilLPStatusWrapper::_refreshStatus($this->getId(), $participant_data->getUserIds());
974 $question->delete($question_id);
975
976 if ($this->getTestQuestions() === []) {
978 $object_properties->storePropertyIsOnline(
979 $object_properties->getPropertyIsOnline()->withOffline()
980 );
981 }
982
983 if ($this->logger->isLoggingEnabled()) {
984 $this->logger->logTestAdministrationInteraction(
985 $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
986 $this->getRefId(),
987 $this->user->getId(),
988 TestAdministrationInteractionTypes::QUESTION_REMOVED_IN_CORRECTIONS,
989 [
990 AdditionalInformationGenerator::KEY_QUESTION_TITLE => $question->getTitleForHTMLOutput(),
991 AdditionalInformationGenerator::KEY_QUESTION_TEXT => $question->getQuestion(),
992 AdditionalInformationGenerator::KEY_QUESTION_ID => $question->getId(),
993 AdditionalInformationGenerator::KEY_QUESTION_TYPE => $question->getQuestionType()
994 ]
995 )
996 );
997 }
998 }
999
1004 int $question_id,
1005 array $active_ids,
1006 ilTestReindexedSequencePositionMap $reindexed_sequence_position_map
1007 ): void {
1008 $test_sequence_factory = new ilTestSequenceFactory(
1009 $this,
1010 $this->db,
1011 $this->questionrepository
1012 );
1013
1014 foreach ($active_ids as $active_id) {
1015 $passSelector = new ilTestPassesSelector($this->db, $this);
1016 $passSelector->setActiveId($active_id);
1017
1018 foreach ($passSelector->getExistingPasses() as $pass) {
1019 $test_sequence = $test_sequence_factory->getSequenceByActiveIdAndPass($active_id, $pass);
1020 $test_sequence->loadFromDb();
1021
1022 $test_sequence->removeQuestion($question_id, $reindexed_sequence_position_map);
1023 $test_sequence->saveToDb();
1024 }
1025 }
1026 }
1027
1031 public function removeQuestions(array $question_ids): void
1032 {
1033 foreach ($question_ids as $question_id) {
1034 $this->removeQuestion((int) $question_id);
1035 }
1036
1037 $this->reindexFixedQuestionOrdering();
1038 }
1039
1040 public function removeQuestion(int $question_id): void
1041 {
1042 try {
1043 $question = self::_instanciateQuestion($question_id);
1044 $question_title = $question->getTitleForHTMLOutput();
1045 $question->delete($question_id);
1046 if ($this->logger->isLoggingEnabled()) {
1047 $this->logger->logTestAdministrationInteraction(
1048 $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
1049 $this->getRefId(),
1050 $this->user->getId(),
1051 TestAdministrationInteractionTypes::QUESTION_REMOVED,
1052 [
1053 AdditionalInformationGenerator::KEY_QUESTION_TITLE => $question_title
1054 ]
1055 )
1056 );
1057 }
1058 } catch (InvalidArgumentException $e) {
1059 $this->logger->error($e->getMessage());
1060 $this->logger->error($e->getTraceAsString());
1061 }
1062 }
1063
1072 public function removeTestResultsFromSoapLpAdministration(array $user_ids)
1073 {
1074 $this->removeTestResultsByUserIds($user_ids);
1075
1076 $participantData = new ilTestParticipantData($this->db, $this->lng);
1077 $participantData->setUserIdsFilter($user_ids);
1078 $participantData->load($this->getTestId());
1079
1080 $this->removeTestActives($participantData->getActiveIds());
1081
1082
1083 if ($this->logger->isLoggingEnabled()) {
1084 $this->logger->logTestAdministrationInteraction(
1085 $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
1086 $this->getRefId(),
1087 $this->user->getId(),
1088 TestAdministrationInteractionTypes::PARTICIPANT_DATA_REMOVED,
1089 [
1090 AdditionalInformationGenerator::KEY_USERS => $participantData->getUserIds()
1091 ]
1092 )
1093 );
1094 }
1095 }
1096
1097 public function removeTestResults(ilTestParticipantData $participant_data): void
1098 {
1099 if ($participant_data->getAnonymousActiveIds() !== []) {
1100 $this->removeTestResultsByActiveIds($participant_data->getAnonymousActiveIds());
1101
1102 $user_ids = array_map(
1103 static fn($active_id) => $participant_data->getUserIdByActiveId($active_id),
1104 $participant_data->getAnonymousActiveIds(),
1105 );
1106 $this->participant_repository->removeExtraTimeByUserId($this->getTestId(), $user_ids);
1107 }
1108
1109 if ($participant_data->getUserIds() !== []) {
1110 /* @var ilTestLP $testLP */
1111 $test_lp = ilObjectLP::getInstance($this->getId());
1112 if ($test_lp instanceof ilTestLP) {
1113 $test_lp->setTestObject($this);
1114 $test_lp->resetLPDataForUserIds($participant_data->getUserIds(), false);
1115 }
1116
1117 $this->participant_repository->removeExtraTimeByUserId($this->getTestId(), $participant_data->getUserIds());
1118 }
1119
1120 if ($participant_data->getActiveIds() !== []) {
1121 $this->removeTestActives($participant_data->getActiveIds());
1122
1123 $user_ids = array_map(
1124 static fn($active_id) => $participant_data->getUserIdByActiveId($active_id),
1125 $participant_data->getActiveIds(),
1126 );
1127 $this->participant_repository->removeExtraTimeByUserId($this->getTestId(), $user_ids);
1128 }
1129
1130 if ($this->logger->isLoggingEnabled()) {
1131 $this->logger->logTestAdministrationInteraction(
1132 $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
1133 $this->getRefId(),
1134 $this->user->getId(),
1135 TestAdministrationInteractionTypes::PARTICIPANT_DATA_REMOVED,
1136 [
1137 AdditionalInformationGenerator::KEY_USERS => $participant_data->getUserIds(),
1138 AdditionalInformationGenerator::KEY_ANON_IDS => $participant_data->getAnonymousActiveIds()
1139 ]
1140 )
1141 );
1142 }
1143 }
1144
1145 public function removeTestResultsByUserIds(array $user_ids): void
1146 {
1147 $participantData = new ilTestParticipantData($this->db, $this->lng);
1148 $participantData->setUserIdsFilter($user_ids);
1149 $participantData->load($this->getTestId());
1150
1151 $in_user_ids = $this->db->in('usr_id', $participantData->getUserIds(), false, 'integer');
1152 $this->db->manipulateF(
1153 "DELETE FROM usr_pref WHERE {$in_user_ids} AND keyword = %s",
1154 ['text'],
1155 ['tst_password_' . $this->getTestId()]
1156 );
1157
1158 if ($participantData->getActiveIds() !== []) {
1159 $this->removeTestResultsByActiveIds($participantData->getActiveIds());
1160 }
1161 }
1162
1163 private function removeTestResultsByActiveIds(array $active_ids): void
1164 {
1165 $in_active_ids = $this->db->in('active_fi', $active_ids, false, 'integer');
1166
1167 $this->db->manipulate("DELETE FROM tst_solutions WHERE {$in_active_ids}");
1168 $this->db->manipulate("DELETE FROM tst_qst_solved WHERE {$in_active_ids}");
1169 $this->db->manipulate("DELETE FROM tst_test_result WHERE {$in_active_ids}");
1170 $this->db->manipulate("DELETE FROM tst_pass_result WHERE {$in_active_ids}");
1171 $this->db->manipulate("DELETE FROM tst_result_cache WHERE {$in_active_ids}");
1172 $this->db->manipulate("DELETE FROM tst_sequence WHERE {$in_active_ids}");
1173 $this->db->manipulate("DELETE FROM tst_times WHERE {$in_active_ids}");
1174 $this->db->manipulate(
1176 . ' WHERE ' . $this->db->in('active_id', $active_ids, false, 'integer')
1177 );
1178
1179 if ($this->isRandomTest()) {
1180 $this->db->manipulate("DELETE FROM tst_test_rnd_qst WHERE {$in_active_ids}");
1181 }
1182
1183 $this->test_result_repository->removeTestResults($active_ids, $this->getId());
1184
1185 foreach ($active_ids as $active_id) {
1186 // remove file uploads
1187 if (is_dir(CLIENT_WEB_DIR . "/assessment/tst_" . $this->getTestId() . "/$active_id")) {
1188 ilFileUtils::delDir(CLIENT_WEB_DIR . "/assessment/tst_" . $this->getTestId() . "/$active_id");
1189 }
1190 }
1191 }
1192
1196 public function removeTestActives(array $active_ids): void
1197 {
1198 $IN_activeIds = $this->db->in('active_id', $active_ids, false, 'integer');
1199 $this->db->manipulate("DELETE FROM tst_active WHERE $IN_activeIds");
1200 }
1201
1209 public function questionMoveUp($question_id)
1210 {
1211 // Move a question up in sequence
1212 $result = $this->db->queryF(
1213 "SELECT * FROM tst_test_question WHERE test_fi=%s AND question_fi=%s",
1214 ['integer', 'integer'],
1215 [$this->getTestId(), $question_id]
1216 );
1217 $data = $this->db->fetchObject($result);
1218 if ($data->sequence > 1) {
1219 // OK, it's not the top question, so move it up
1220 $result = $this->db->queryF(
1221 "SELECT * FROM tst_test_question WHERE test_fi=%s AND sequence=%s",
1222 ['integer','integer'],
1223 [$this->getTestId(), $data->sequence - 1]
1224 );
1225 $data_previous = $this->db->fetchObject($result);
1226 // change previous dataset
1227 $this->db->manipulateF(
1228 "UPDATE tst_test_question SET sequence=%s WHERE test_question_id=%s",
1229 ['integer','integer'],
1230 [$data->sequence, $data_previous->test_question_id]
1231 );
1232 // move actual dataset up
1233 $this->db->manipulateF(
1234 "UPDATE tst_test_question SET sequence=%s WHERE test_question_id=%s",
1235 ['integer','integer'],
1236 [$data->sequence - 1, $data->test_question_id]
1237 );
1238 }
1239 $this->loadQuestions();
1240 }
1241
1249 public function questionMoveDown($question_id)
1250 {
1251 $current_question_result = $this->db->queryF(
1252 "SELECT * FROM tst_test_question WHERE test_fi=%s AND question_fi=%s",
1253 ['integer','integer'],
1254 [$this->getTestId(), $question_id]
1255 );
1256 $current_question_data = $this->db->fetchObject($current_question_result);
1257 $next_question_result = $this->db->queryF(
1258 "SELECT * FROM tst_test_question WHERE test_fi=%s AND sequence=%s",
1259 ['integer','integer'],
1260 [$this->getTestId(), $current_question_data->sequence + 1]
1261 );
1262 if ($this->db->numRows($next_question_result) === 1) {
1263 // OK, it's not the last question, so move it down
1264 $next_question_data = $this->db->fetchObject($next_question_result);
1265 // change next dataset
1266 $this->db->manipulateF(
1267 "UPDATE tst_test_question SET sequence=%s WHERE test_question_id=%s",
1268 ['integer','integer'],
1269 [$current_question_data->sequence, $next_question_data->test_question_id]
1270 );
1271 // move actual dataset down
1272 $this->db->manipulateF(
1273 "UPDATE tst_test_question SET sequence=%s WHERE test_question_id=%s",
1274 ['integer','integer'],
1275 [$current_question_data->sequence + 1, $current_question_data->test_question_id]
1276 );
1277 }
1278 $this->loadQuestions();
1279 }
1280
1287 public function duplicateQuestionForTest($question_id): int
1288 {
1289 $question = ilObjTest::_instanciateQuestion($question_id);
1290 $duplicate_id = $question->duplicate(true, '', '', -1, $this->getId());
1291 return $duplicate_id;
1292 }
1293
1294 public function insertQuestion(int $question_id, bool $link_only = false): int
1295 {
1296 if ($link_only) {
1297 $duplicate_id = $question_id;
1298 } else {
1299 $duplicate_id = $this->duplicateQuestionForTest($question_id);
1300 }
1301
1302 // get maximum sequence index in test
1303 $result = $this->db->queryF(
1304 "SELECT MAX(sequence) seq FROM tst_test_question WHERE test_fi=%s",
1305 ['integer'],
1306 [$this->getTestId()]
1307 );
1308 $sequence = 1;
1309
1310 if ($result->numRows() == 1) {
1311 $data = $this->db->fetchObject($result);
1312 $sequence = $data->seq + 1;
1313 }
1314
1315 $next_id = $this->db->nextId('tst_test_question');
1316 $this->db->manipulateF(
1317 "INSERT INTO tst_test_question (test_question_id, test_fi, question_fi, sequence, tstamp) VALUES (%s, %s, %s, %s, %s)",
1318 ['integer', 'integer','integer','integer','integer'],
1319 [$next_id, $this->getTestId(), $duplicate_id, $sequence, time()]
1320 );
1321 // remove test_active entries, because test has changed
1322 $this->db->manipulateF(
1323 "DELETE FROM tst_active WHERE test_fi = %s",
1324 ['integer'],
1325 [$this->getTestId()]
1326 );
1327 $this->loadQuestions();
1328 $this->saveCompleteStatus($this->question_set_config_factory->getQuestionSetConfig());
1329
1330 if ($this->logger->isLoggingEnabled()) {
1331 $this->logger->logTestAdministrationInteraction(
1332 $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
1333 $this->getRefId(),
1334 $this->user->getId(),
1335 TestAdministrationInteractionTypes::QUESTION_ADDED,
1336 [
1337 AdditionalInformationGenerator::KEY_QUESTION_ID => $question_id
1338 ] + (assQuestion::instantiateQuestion($question_id))
1339 ->toLog($this->logger->getAdditionalInformationGenerator())
1340 )
1341 );
1342 }
1343
1344 return $duplicate_id;
1345 }
1346
1347 private function getQuestionTitles(): array
1348 {
1349 $titles = [];
1350 if ($this->getQuestionSetType() === self::QUESTION_SET_TYPE_FIXED) {
1351 $result = $this->db->queryF(
1352 'SELECT qpl_questions.title FROM tst_test_question, qpl_questions '
1353 . 'WHERE tst_test_question.test_fi = %s AND tst_test_question.question_fi = qpl_questions.question_id '
1354 . 'ORDER BY tst_test_question.sequence',
1355 ['integer'],
1356 [$this->getTestId()]
1357 );
1358 while ($row = $this->db->fetchAssoc($result)) {
1359 array_push($titles, $row['title']);
1360 }
1361 }
1362 return $titles;
1363 }
1364
1372 public function getQuestionTitlesAndIndexes(): array
1373 {
1374 $titles = [];
1375 if ($this->getQuestionSetType() == self::QUESTION_SET_TYPE_FIXED) {
1376 $result = $this->db->queryF(
1377 'SELECT qpl_questions.title, qpl_questions.question_id '
1378 . 'FROM tst_test_question, qpl_questions '
1379 . 'WHERE tst_test_question.test_fi = %s AND tst_test_question.question_fi = qpl_questions.question_id '
1380 . 'ORDER BY tst_test_question.sequence',
1381 ['integer'],
1382 [$this->getTestId()]
1383 );
1384 while ($row = $this->db->fetchAssoc($result)) {
1385 $titles[$row['question_id']] = $row["title"];
1386 }
1387 }
1388 return $titles;
1389 }
1390
1391 // fau: testNav - add number parameter (to show if title should not be shown)
1401 public function getQuestionTitle($title, $nr = null, $points = null): string
1402 {
1403 switch ($this->getTitleOutput()) {
1404 case '0':
1405 case '1':
1406 return $title;
1407 break;
1408 case '2':
1409 if (isset($nr)) {
1410 return $this->lng->txt("ass_question") . ' ' . $nr;
1411 }
1412 return $this->lng->txt("ass_question");
1413 break;
1414 case 3:
1415 if (isset($nr)) {
1416 $txt = $this->lng->txt("ass_question") . ' ' . $nr;
1417 } else {
1418 $txt = $this->lng->txt("ass_question");
1419 }
1420 if ($points != '') {
1421 $lngv = $this->lng->txt('points');
1422 if ($points == 1) {
1423 $lngv = $this->lng->txt('point');
1424 }
1425 $txt .= ' - ' . $points . ' ' . $lngv;
1426 }
1427 return $txt;
1428 break;
1429
1430 }
1431 return $this->lng->txt("ass_question");
1432 }
1433 // fau.
1434
1443 public function getQuestionDataset($question_id): object
1444 {
1445 $result = $this->db->queryF(
1446 "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",
1447 ['integer'],
1448 [$question_id]
1449 );
1450 $row = $this->db->fetchObject($result);
1451 return $row;
1452 }
1453
1460 public function &getExistingQuestions($pass = null): array
1461 {
1462 $existing_questions = [];
1463 $active_id = $this->getActiveIdOfUser($this->user->getId());
1464 if ($this->isRandomTest()) {
1465 if (is_null($pass)) {
1466 $pass = 0;
1467 }
1468 $result = $this->db->queryF(
1469 "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",
1470 ['integer','integer'],
1471 [$active_id, $pass]
1472 );
1473 } else {
1474 $result = $this->db->queryF(
1475 "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",
1476 ['integer'],
1477 [$this->getTestId()]
1478 );
1479 }
1480 while ($data = $this->db->fetchObject($result)) {
1481 if ($data->original_id === null) {
1482 continue;
1483 }
1484
1485 array_push($existing_questions, $data->original_id);
1486 }
1487 return $existing_questions;
1488 }
1489
1497 public function getQuestionType($question_id)
1498 {
1499 if ($question_id < 1) {
1500 return -1;
1501 }
1502 $result = $this->db->queryF(
1503 "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",
1504 ['integer'],
1505 [$question_id]
1506 );
1507 if ($result->numRows() == 1) {
1508 $data = $this->db->fetchObject($result);
1509 return $data->type_tag;
1510 } else {
1511 return "";
1512 }
1513 }
1514
1521 public function startWorkingTime($active_id, $pass)
1522 {
1523 $next_id = $this->db->nextId('tst_times');
1524 $affectedRows = $this->db->manipulateF(
1525 "INSERT INTO tst_times (times_id, active_fi, started, finished, pass, tstamp) VALUES (%s, %s, %s, %s, %s, %s)",
1526 ['integer', 'integer', 'timestamp', 'timestamp', 'integer', 'integer'],
1527 [$next_id, $active_id, date("Y-m-d H:i:s"), date("Y-m-d H:i:s"), $pass, time()]
1528 );
1529 return $next_id;
1530 }
1531
1538 public function updateWorkingTime($times_id)
1539 {
1540 $affectedRows = $this->db->manipulateF(
1541 "UPDATE tst_times SET finished = %s, tstamp = %s WHERE times_id = %s",
1542 ['timestamp', 'integer', 'integer'],
1543 [date('Y-m-d H:i:s'), time(), $times_id]
1544 );
1545 }
1546
1553 public function &getWorkedQuestions($active_id, $pass = null): array
1554 {
1555 if (is_null($pass)) {
1556 $result = $this->db->queryF(
1557 "SELECT question_fi FROM tst_solutions WHERE active_fi = %s AND pass = %s GROUP BY question_fi",
1558 ['integer','integer'],
1559 [$active_id, 0]
1560 );
1561 } else {
1562 $result = $this->db->queryF(
1563 "SELECT question_fi FROM tst_solutions WHERE active_fi = %s AND pass = %s GROUP BY question_fi",
1564 ['integer','integer'],
1565 [$active_id, $pass]
1566 );
1567 }
1568 $result_array = [];
1569 while ($row = $this->db->fetchAssoc($result)) {
1570 array_push($result_array, $row["question_fi"]);
1571 }
1572 return $result_array;
1573 }
1574
1583 public function isTestFinishedToViewResults($active_id, $currentpass): bool
1584 {
1585 $num = ilObjTest::lookupPassResultsUpdateTimestamp($active_id, $currentpass);
1586 return ((($currentpass > 0) && ($num == 0)) || $this->isTestFinished($active_id)) ? true : false;
1587 }
1588
1595 public function getAllQuestions($pass = null): array
1596 {
1597 if ($this->isRandomTest()) {
1598 $active_id = $this->getActiveIdOfUser($this->user->getId());
1599 if ($active_id === null) {
1600 return [];
1601 }
1602 $this->loadQuestions($active_id, $pass);
1603 if (count($this->questions) === 0) {
1604 return [];
1605 }
1606 if (is_null($pass)) {
1607 $pass = self::_getPass($active_id);
1608 }
1609 $result = $this->db->queryF(
1610 "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'),
1611 ['integer','integer'],
1612 [$active_id, $pass]
1613 );
1614 } else {
1615 if (count($this->questions) === 0) {
1616 return [];
1617 }
1618 $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'));
1619 }
1620 $result_array = [];
1621 while ($row = $this->db->fetchAssoc($result)) {
1622 $result_array[$row["question_id"]] = $row;
1623 }
1624 return $result_array;
1625 }
1626
1635 public function getActiveIdOfUser($user_id = "", $anonymous_id = ""): ?int
1636 {
1637 if (!$user_id) {
1638 $user_id = $this->user->getId();
1639 }
1640
1641 $tst_access_code = ilSession::get('tst_access_code');
1642 if (is_array($tst_access_code) &&
1643 $this->user->getId() === ANONYMOUS_USER_ID &&
1644 isset($tst_access_code[$this->getTestId()]) &&
1645 $tst_access_code[$this->getTestId()] !== '') {
1646 $result = $this->db->queryF(
1647 'SELECT active_id FROM tst_active WHERE user_fi = %s AND test_fi = %s AND anonymous_id = %s',
1648 ['integer', 'integer', 'text'],
1649 [$user_id, $this->test_id, $tst_access_code[$this->getTestId()]]
1650 );
1651 } elseif ((string) $anonymous_id !== '') {
1652 $result = $this->db->queryF(
1653 'SELECT active_id FROM tst_active WHERE user_fi = %s AND test_fi = %s AND anonymous_id = %s',
1654 ['integer', 'integer', 'text'],
1655 [$user_id, $this->test_id, $anonymous_id]
1656 );
1657 } else {
1658 if ((int) $user_id === ANONYMOUS_USER_ID) {
1659 return null;
1660 }
1661 $result = $this->db->queryF(
1662 'SELECT active_id FROM tst_active WHERE user_fi = %s AND test_fi = %s',
1663 ['integer', 'integer'],
1664 [$user_id, $this->test_id]
1665 );
1666 }
1667
1668 if ($result->numRows()) {
1669 $row = $this->db->fetchAssoc($result);
1670 return (int) $row['active_id'];
1671 }
1672
1673 return null;
1674 }
1675
1676 public static function _getActiveIdOfUser($user_id = "", $test_id = "")
1677 {
1678 global $DIC;
1679 $ilDB = $DIC['ilDB'];
1680 $ilUser = $DIC['ilUser'];
1681
1682 if (!$user_id) {
1683 $user_id = $ilUser->id;
1684 }
1685 if (!$test_id) {
1686 return "";
1687 }
1688 $result = $ilDB->queryF(
1689 "SELECT tst_active.active_id FROM tst_active WHERE user_fi = %s AND test_fi = %s",
1690 ['integer', 'integer'],
1691 [$user_id, $test_id]
1692 );
1693 if ($result->numRows()) {
1694 $row = $ilDB->fetchAssoc($result);
1695 return $row["active_id"];
1696 } else {
1697 return "";
1698 }
1699 }
1700
1707 public function pcArrayShuffle($array): array
1708 {
1709 $keys = array_keys($array);
1710 shuffle($keys);
1711 $result = [];
1712 foreach ($keys as $key) {
1713 $result[$key] = $array[$key];
1714 }
1715 return $result;
1716 }
1717
1724 public function getTestResult(
1725 int $active_id,
1726 ?int $attempt = null,
1727 bool $ordered_sequence = false,
1728 bool $consider_hidden_questions = true,
1729 bool $consider_optional_questions = true
1730 ): array {
1731 $test_result = $this->test_result_repository->getTestResult($active_id);
1732
1733 if ($test_result === null) {
1734 $test_result = $this->test_result_repository->updateTestResultCache($active_id);
1735 }
1736
1737 if ($attempt === null) {
1738 $attempt = $test_result->getAttempt();
1739 }
1740
1741 $test_sequence_factory = new ilTestSequenceFactory($this, $this->db, $this->questionrepository);
1742 $test_sequence = $test_sequence_factory->getSequenceByActiveIdAndPass($active_id, $attempt);
1743
1744 $test_sequence->setConsiderHiddenQuestionsEnabled($consider_hidden_questions);
1745 $test_sequence->setConsiderOptionalQuestionsEnabled($consider_optional_questions);
1746
1747 $test_sequence->loadFromDb();
1748 $test_sequence->loadQuestions();
1749
1750 if ($ordered_sequence) {
1751 $sequence = $test_sequence->getOrderedSequenceQuestions();
1752 } else {
1753 $sequence = $test_sequence->getUserSequenceQuestions();
1754 }
1755
1756 $arr_results = [];
1757
1758 $query = "
1759 SELECT
1760 tst_test_result.question_fi,
1761 tst_test_result.points reached,
1762 tst_test_result.answered answered,
1763 tst_manual_fb.finalized_evaluation finalized_evaluation
1764
1765 FROM tst_test_result
1766
1767 LEFT JOIN tst_solutions
1768 ON tst_solutions.active_fi = tst_test_result.active_fi
1769 AND tst_solutions.question_fi = tst_test_result.question_fi
1770
1771 LEFT JOIN tst_manual_fb
1772 ON tst_test_result.active_fi = tst_manual_fb.active_fi
1773 AND tst_test_result.question_fi = tst_manual_fb.question_fi
1774
1775 WHERE tst_test_result.active_fi = %s
1776 AND tst_test_result.pass = %s
1777 ";
1778
1779 $solutionresult = $this->db->queryF(
1780 $query,
1781 ['integer', 'integer'],
1782 [$active_id, $attempt]
1783 );
1784
1785 while ($row = $this->db->fetchAssoc($solutionresult)) {
1786 $arr_results[ $row['question_fi'] ] = $row;
1787 }
1788
1789 $result = $this->db->query(
1790 'SELECT qpl_questions.*, qpl_qst_type.type_tag, qpl_sol_sug.question_fi has_sug_sol' . PHP_EOL
1791 . 'FROM qpl_qst_type, qpl_questions' . PHP_EOL
1792 . 'LEFT JOIN qpl_sol_sug' . PHP_EOL
1793 . 'ON qpl_sol_sug.question_fi = qpl_questions.question_id' . PHP_EOL
1794 . 'WHERE qpl_qst_type.question_type_id = qpl_questions.question_type_fi' . PHP_EOL
1795 . 'AND ' . $this->db->in('qpl_questions.question_id', $sequence, false, 'integer')
1796 );
1797
1798 $unordered = [];
1799 $key = 1;
1800 while ($row = $this->db->fetchAssoc($result)) {
1801 if (!isset($arr_results[ $row['question_id'] ])) {
1802 $percentvalue = 0.0;
1803 } else {
1804 $percentvalue = (
1805 $row['points'] ? $arr_results[$row['question_id']]['reached'] / $row['points'] : 0
1806 );
1807 }
1808 if ($percentvalue < 0) {
1809 $percentvalue = 0.0;
1810 }
1811
1812 $data = [
1813 'nr' => $key,
1814 'title' => ilLegacyFormElementsUtil::prepareFormOutput($row['title']),
1815 'max' => round($row['points'], 2),
1816 'reached' => round($arr_results[$row['question_id']]['reached'] ?? 0, 2),
1817 'percent' => sprintf('%2.2f ', ($percentvalue) * 100) . '%',
1818 'solution' => ($row['has_sug_sol']) ? assQuestion::_getSuggestedSolutionOutput($row['question_id']) : '',
1819 'type' => $row['type_tag'],
1820 'qid' => $row['question_id'],
1821 'original_id' => $row['original_id'],
1822 'workedthrough' => isset($arr_results[$row['question_id']]) ? 1 : 0,
1823 'answered' => $arr_results[$row['question_id']]['answered'] ?? 0,
1824 'finalized_evaluation' => $arr_results[$row['question_id']]['finalized_evaluation'] ?? 0,
1825 ];
1826
1827 $unordered[ $row['question_id'] ] = $data;
1828 $key++;
1829 }
1830
1831 $pass_max = 0;
1832 $pass_reached = 0;
1833
1834 $found = [];
1835 foreach ($sequence as $qid) {
1836 // building pass point sums based on prepared data
1837 // for question that exists in users qst sequence
1838 $pass_max += round($unordered[$qid]['max'], 2);
1839 $pass_reached += round($unordered[$qid]['reached'], 2);
1840 $found[] = $unordered[$qid];
1841 }
1842
1843 if ($this->getScoreCutting() == 1) {
1844 if ($pass_reached < 0) {
1845 $pass_reached = 0;
1846 }
1847 }
1848
1849 $found['pass']['total_max_points'] = $pass_max;
1850 $found['pass']['total_reached_points'] = $pass_reached;
1851 $found['pass']['percent'] = ($pass_max > 0) ? $pass_reached / $pass_max : 0;
1852 $found['pass']['num_workedthrough'] = count($arr_results);
1853 $found['pass']['num_questions_total'] = count($unordered);
1854
1855 $found['test']['total_max_points'] = $test_result->getMaxPoints();
1856 $found['test']['total_reached_points'] = $test_result->getReachedPoints();
1857 $found['test']['result_pass'] = $attempt;
1858 $found['test']['result_tstamp'] = $test_result->getTimestamp();
1859 $found['test']['passed'] = $test_result->isPassed();
1860
1861 return $found;
1862 }
1863
1870 public function evalTotalPersons(): int
1871 {
1872 $result = $this->db->queryF(
1873 'SELECT COUNT(active_id) total FROM tst_active WHERE test_fi = %s',
1874 ['integer'],
1875 [$this->getTestId()]
1876 );
1877 $row = $this->db->fetchAssoc($result);
1878 return $row['total'];
1879 }
1880
1888 {
1889 $result = $this->db->queryF(
1890 "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",
1891 ['integer','integer'],
1892 [$this->getTestId(), $user_id]
1893 );
1894 $time = 0;
1895 while ($row = $this->db->fetchAssoc($result)) {
1896 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["started"], $matches);
1897 $epoch_1 = mktime(
1898 (int) $matches[4],
1899 (int) $matches[5],
1900 (int) $matches[6],
1901 (int) $matches[2],
1902 (int) $matches[3],
1903 (int) $matches[1]
1904 );
1905 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["finished"], $matches);
1906 $epoch_2 = mktime(
1907 (int) $matches[4],
1908 (int) $matches[5],
1909 (int) $matches[6],
1910 (int) $matches[2],
1911 (int) $matches[3],
1912 (int) $matches[1]
1913 );
1914 $time += ($epoch_2 - $epoch_1);
1915 }
1916 return $time;
1917 }
1918
1926 {
1927 return $this->_getCompleteWorkingTimeOfParticipants($this->getTestId());
1928 }
1929
1937 public function &_getCompleteWorkingTimeOfParticipants($test_id): array
1938 {
1939 $result = $this->db->queryF(
1940 "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",
1941 ['integer'],
1942 [$test_id]
1943 );
1944 $time = 0;
1945 $times = [];
1946 while ($row = $this->db->fetchAssoc($result)) {
1947 if (!array_key_exists($row["active_fi"], $times)) {
1948 $times[$row["active_fi"]] = 0;
1949 }
1950 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["started"], $matches);
1951 $epoch_1 = mktime(
1952 (int) $matches[4],
1953 (int) $matches[5],
1954 (int) $matches[6],
1955 (int) $matches[2],
1956 (int) $matches[3],
1957 (int) $matches[1]
1958 );
1959 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["finished"], $matches);
1960 $epoch_2 = mktime(
1961 (int) $matches[4],
1962 (int) $matches[5],
1963 (int) $matches[6],
1964 (int) $matches[2],
1965 (int) $matches[3],
1966 (int) $matches[1]
1967 );
1968 $times[$row["active_fi"]] += ($epoch_2 - $epoch_1);
1969 }
1970 return $times;
1971 }
1972
1979 public function getCompleteWorkingTimeOfParticipant($active_id): int
1980 {
1981 $result = $this->db->queryF(
1982 "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",
1983 ['integer','integer'],
1984 [$this->getTestId(), $active_id]
1985 );
1986 $time = 0;
1987 while ($row = $this->db->fetchAssoc($result)) {
1988 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["started"], $matches);
1989 $epoch_1 = mktime(
1990 (int) $matches[4],
1991 (int) $matches[5],
1992 (int) $matches[6],
1993 (int) $matches[2],
1994 (int) $matches[3],
1995 (int) $matches[1]
1996 );
1997 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["finished"], $matches);
1998 $epoch_2 = mktime(
1999 (int) $matches[4],
2000 (int) $matches[5],
2001 (int) $matches[6],
2002 (int) $matches[2],
2003 (int) $matches[3],
2004 (int) $matches[1]
2005 );
2006 $time += ($epoch_2 - $epoch_1);
2007 }
2008 return $time;
2009 }
2010
2014 public function getWorkingTimeOfParticipantForPass(int $active_id, int $pass): int
2015 {
2016 return $this->test_result_repository->fetchWorkingTime($active_id, $pass);
2017 }
2018
2022 public function evalStatistical($active_id): array
2023 {
2024 $pass = ilObjTest::_getResultPass($active_id);
2025 $test_result = &$this->getTestResult($active_id, $pass);
2026 $result = $this->db->queryF(
2027 "SELECT tst_times.* FROM tst_active, tst_times WHERE tst_active.active_id = %s AND tst_active.active_id = tst_times.active_fi",
2028 ['integer'],
2029 [$active_id]
2030 );
2031 $times = [];
2032 $first_visit = 0;
2033 $last_visit = 0;
2034 while ($row = $this->db->fetchObject($result)) {
2035 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row->started, $matches);
2036 $epoch_1 = mktime(
2037 (int) $matches[4],
2038 (int) $matches[5],
2039 (int) $matches[6],
2040 (int) $matches[2],
2041 (int) $matches[3],
2042 (int) $matches[1]
2043 );
2044 if (!$first_visit) {
2045 $first_visit = $epoch_1;
2046 }
2047 if ($epoch_1 < $first_visit) {
2048 $first_visit = $epoch_1;
2049 }
2050 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row->finished, $matches);
2051 $epoch_2 = mktime(
2052 (int) $matches[4],
2053 (int) $matches[5],
2054 (int) $matches[6],
2055 (int) $matches[2],
2056 (int) $matches[3],
2057 (int) $matches[1]
2058 );
2059 if (!$last_visit) {
2060 $last_visit = $epoch_2;
2061 }
2062 if ($epoch_2 > $last_visit) {
2063 $last_visit = $epoch_2;
2064 }
2065 $times[$row->active_fi] += ($epoch_2 - $epoch_1);
2066 }
2067 $max_time = 0;
2068 foreach ($times as $key => $value) {
2069 $max_time += $value;
2070 }
2071 if ((!$test_result["test"]["total_reached_points"]) or (!$test_result["test"]["total_max_points"])) {
2072 $percentage = 0.0;
2073 } else {
2074 $percentage = ($test_result["test"]["total_reached_points"] / $test_result["test"]["total_max_points"]) * 100.0;
2075 if ($percentage < 0) {
2076 $percentage = 0.0;
2077 }
2078 }
2079 $mark_obj = $this->getMarkSchema()->getMatchingMark($percentage);
2080 $first_date = getdate($first_visit);
2081 $last_date = getdate($last_visit);
2082 $qworkedthrough = 0;
2083 foreach ($test_result as $key => $value) {
2084 if (preg_match("/\d+/", $key)) {
2085 $qworkedthrough += $value["workedthrough"];
2086 }
2087 }
2088 if (!$qworkedthrough) {
2089 $atimeofwork = 0;
2090 } else {
2091 $atimeofwork = $max_time / $qworkedthrough;
2092 }
2093
2094 $result_mark = "";
2095 $passed = "";
2096
2097 if ($mark_obj !== null) {
2098 $result_mark = $mark_obj->getShortName();
2099
2100 if ($mark_obj->getPassed()) {
2101 $passed = 1;
2102 } else {
2103 $passed = 0;
2104 }
2105 }
2106 $percent_worked_through = 0;
2107 if (count($this->questions)) {
2108 $percent_worked_through = $qworkedthrough / count($this->questions);
2109 }
2110 $result_array = [
2111 "qworkedthrough" => $qworkedthrough,
2112 "qmax" => count($this->questions),
2113 "pworkedthrough" => $percent_worked_through,
2114 "timeofwork" => $max_time,
2115 "atimeofwork" => $atimeofwork,
2116 "firstvisit" => $first_date,
2117 "lastvisit" => $last_date,
2118 "resultspoints" => $test_result["test"]["total_reached_points"],
2119 "maxpoints" => $test_result["test"]["total_max_points"],
2120 "resultsmarks" => $result_mark,
2121 "passed" => $passed,
2122 "distancemedian" => "0"
2123 ];
2124 foreach ($test_result as $key => $value) {
2125 if (preg_match("/\d+/", $key)) {
2126 $result_array[$key] = $value;
2127 }
2128 }
2129 return $result_array;
2130 }
2131
2139 public function getTotalPointsPassedArray(): array
2140 {
2141 $totalpoints_array = [];
2142 $all_users = $this->evalTotalParticipantsArray();
2143 foreach ($all_users as $active_id => $user_name) {
2144 $test_result = &$this->getTestResult($active_id);
2145 $reached = $test_result["test"]["total_reached_points"];
2146 $total = $test_result["test"]["total_max_points"];
2147 $percentage = $total != 0 ? $reached / $total : 0;
2148 $mark = $this->getMarkSchema()->getMatchingMark($percentage * 100.0);
2149
2150 if ($mark !== null && $mark->getPassed()) {
2151 array_push($totalpoints_array, $test_result["test"]["total_reached_points"]);
2152 }
2153 }
2154 return $totalpoints_array;
2155 }
2156
2162 public function getParticipants(): array
2163 {
2164 $result = $this->db->queryF(
2165 "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",
2166 ['integer'],
2167 [$this->getTestId()]
2168 );
2169 $persons_array = [];
2170 while ($row = $this->db->fetchAssoc($result)) {
2171 $name = $this->lng->txt("anonymous");
2172 $fullname = $this->lng->txt("anonymous");
2173 $login = "";
2174 if (!$this->getAnonymity()) {
2175 if (strlen($row["firstname"] . $row["lastname"] . $row["title"]) == 0) {
2176 $name = $this->lng->txt("deleted_user");
2177 $fullname = $this->lng->txt("deleted_user");
2178 $login = $this->lng->txt("unknown");
2179 } else {
2180 $login = $row["login"];
2181 if ($row["usr_id"] == ANONYMOUS_USER_ID) {
2182 $name = $this->lng->txt("anonymous");
2183 $fullname = $this->lng->txt("anonymous");
2184 } else {
2185 $name = trim($row["lastname"] . ", " . $row["firstname"] . " " . $row["title"]);
2186 $fullname = trim($row["title"] . " " . $row["firstname"] . " " . $row["lastname"]);
2187 }
2188 }
2189 }
2190 $persons_array[$row["active_id"]] = [
2191 "name" => $name,
2192 "fullname" => $fullname,
2193 "login" => $login
2194 ];
2195 }
2196 return $persons_array;
2197 }
2198
2199 public function evalTotalPersonsArray(string $name_sort_order = 'asc'): array
2200 {
2201 $result = $this->db->queryF(
2202 "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),
2203 ['integer'],
2204 [$this->getTestId()]
2205 );
2206 $persons_array = [];
2207 while ($row = $this->db->fetchAssoc($result)) {
2208 if ($this->getAccessFilteredParticipantList() && !$this->getAccessFilteredParticipantList()->isActiveIdInList($row["active_id"])) {
2209 continue;
2210 }
2211
2212 if ($this->getAnonymity()) {
2213 $persons_array[$row["active_id"]] = $this->lng->txt("anonymous");
2214 } else {
2215 if (strlen($row["firstname"] . $row["lastname"] . $row["title"]) == 0) {
2216 $persons_array[$row["active_id"]] = $this->lng->txt("deleted_user");
2217 } else {
2218 if ($row["user_fi"] == ANONYMOUS_USER_ID) {
2219 $persons_array[$row["active_id"]] = $row["lastname"];
2220 } else {
2221 $persons_array[$row["active_id"]] = trim($row["lastname"] . ", " . $row["firstname"] . " " . $row["title"]);
2222 }
2223 }
2224 }
2225 }
2226 return $persons_array;
2227 }
2228
2229 public function evalTotalParticipantsArray(string $name_sort_order = 'asc'): array
2230 {
2231 $result = $this->db->queryF(
2232 'SELECT tst_active.user_fi, tst_active.active_id, usr_data.login, '
2233 . 'usr_data.firstname, usr_data.lastname, usr_data.title FROM tst_active '
2234 . 'LEFT JOIN usr_data ON tst_active.user_fi = usr_data.usr_id '
2235 . 'WHERE tst_active.test_fi = %s '
2236 . 'ORDER BY usr_data.lastname ' . strtoupper($name_sort_order),
2237 ['integer'],
2238 [$this->getTestId()]
2239 );
2240 $persons_array = [];
2241 while ($row = $this->db->fetchAssoc($result)) {
2242 if ($this->getAnonymity()) {
2243 $persons_array[$row['active_id']] = ['name' => $this->lng->txt("anonymous")];
2244 } else {
2245 if (strlen($row['firstname'] . $row['lastname'] . $row["title"]) == 0) {
2246 $persons_array[$row['active_id']] = ['name' => $this->lng->txt('deleted_user')];
2247 } else {
2248 if ($row['user_fi'] == ANONYMOUS_USER_ID) {
2249 $persons_array[$row['active_id']] = ['name' => $row['lastname']];
2250 } else {
2251 $persons_array[$row['active_id']] = [
2252 'name' => trim($row['lastname'] . ', ' . $row['firstname']
2253 . ' ' . $row['title']),
2254 'login' => $row['login']
2255 ];
2256 }
2257 }
2258 }
2259 }
2260 return $persons_array;
2261 }
2262
2263 public function getQuestionsOfTest(int $active_id): array
2264 {
2265 if ($this->isRandomTest()) {
2266 $this->db->setLimit($this->getQuestionCount(), 0);
2267 $result = $this->db->queryF(
2268 'SELECT tst_test_rnd_qst.sequence, tst_test_rnd_qst.question_fi, '
2269 . 'tst_test_rnd_qst.pass, qpl_questions.points '
2270 . 'FROM tst_test_rnd_qst, qpl_questions '
2271 . 'WHERE tst_test_rnd_qst.question_fi = qpl_questions.question_id '
2272 . 'AND tst_test_rnd_qst.active_fi = %s ORDER BY tst_test_rnd_qst.sequence',
2273 ['integer'],
2274 [$active_id]
2275 );
2276 } else {
2277 $result = $this->db->queryF(
2278 'SELECT tst_test_question.sequence, tst_test_question.question_fi, '
2279 . 'qpl_questions.points '
2280 . 'FROM tst_test_question, tst_active, qpl_questions '
2281 . 'WHERE tst_test_question.question_fi = qpl_questions.question_id '
2282 . 'AND tst_active.active_id = %s AND tst_active.test_fi = tst_test_question.test_fi',
2283 ['integer'],
2284 [$active_id]
2285 );
2286 }
2287 $qtest = [];
2288 if ($result->numRows()) {
2289 while ($row = $this->db->fetchAssoc($result)) {
2290 array_push($qtest, $row);
2291 }
2292 }
2293 return $qtest;
2294 }
2295
2296 public function getQuestionsOfPass(int $active_id, int $pass): array
2297 {
2298 if ($this->isRandomTest()) {
2299 $this->db->setLimit($this->getQuestionCount(), 0);
2300 $result = $this->db->queryF(
2301 'SELECT tst_test_rnd_qst.sequence, tst_test_rnd_qst.question_fi, '
2302 . 'qpl_questions.points '
2303 . 'FROM tst_test_rnd_qst, qpl_questions '
2304 . 'WHERE tst_test_rnd_qst.question_fi = qpl_questions.question_id '
2305 . 'AND tst_test_rnd_qst.active_fi = %s AND tst_test_rnd_qst.pass = %s '
2306 . 'ORDER BY tst_test_rnd_qst.sequence',
2307 ['integer', 'integer'],
2308 [$active_id, $pass]
2309 );
2310 } else {
2311 $result = $this->db->queryF(
2312 'SELECT tst_test_question.sequence, tst_test_question.question_fi, '
2313 . 'qpl_questions.points '
2314 . 'FROM tst_test_question, tst_active, qpl_questions '
2315 . 'WHERE tst_test_question.question_fi = qpl_questions.question_id '
2316 . 'AND tst_active.active_id = %s AND tst_active.test_fi = tst_test_question.test_fi',
2317 ['integer'],
2318 [$active_id]
2319 );
2320 }
2321 $qpass = [];
2322 if ($result->numRows()) {
2323 while ($row = $this->db->fetchAssoc($result)) {
2324 array_push($qpass, $row);
2325 }
2326 }
2327 return $qpass;
2328 }
2329
2331 {
2332 return $this->access_filtered_participant_list;
2333 }
2334
2335 public function setAccessFilteredParticipantList(ilTestParticipantList $access_filtered_participant_list): void
2336 {
2337 $this->access_filtered_participant_list = $access_filtered_participant_list;
2338 }
2339
2341 {
2342 $list = new ilTestParticipantList($this, $this->user, $this->lng, $this->db);
2343 $list->initializeFromDbRows($this->getTestParticipants());
2344
2345 return $list->getAccessFilteredList(
2346 $this->participant_access_filter->getAccessStatisticsUserFilter($this->getRefId())
2347 );
2348 }
2349
2353 public function getAnonOnlyParticipantIds(): array
2354 {
2355 $list = new ilTestParticipantList($this, $this->user, $this->lng, $this->db);
2356 $list->initializeFromDbRows($this->getTestParticipants());
2357 if ($this->getAnonymity()) {
2358 return $list->getAllUserIds();
2359 }
2360 return $list->getAccessFilteredList(
2361 $this->participant_access_filter->getAnonOnlyParticipantsUserFilter($this->getRefId())
2362 )->getAllUserIds();
2363 }
2364
2366 {
2367 return (new ilTestEvaluationFactory($this->db, $this))
2368 ->getEvaluationData();
2369 }
2370
2371 public function getQuestionCountAndPointsForPassOfParticipant(int $active_id, int $pass): array
2372 {
2373 $question_set_type = $this->lookupQuestionSetTypeByActiveId($active_id);
2374
2375 switch ($question_set_type) {
2377 $res = $this->db->queryF(
2378 "
2379 SELECT tst_test_rnd_qst.pass,
2380 COUNT(tst_test_rnd_qst.question_fi) qcount,
2381 SUM(qpl_questions.points) qsum
2382
2383 FROM tst_test_rnd_qst,
2384 qpl_questions
2385
2386 WHERE tst_test_rnd_qst.question_fi = qpl_questions.question_id
2387 AND tst_test_rnd_qst.active_fi = %s
2388 AND pass = %s
2389
2390 GROUP BY tst_test_rnd_qst.active_fi,
2391 tst_test_rnd_qst.pass
2392 ",
2393 ['integer', 'integer'],
2394 [$active_id, $pass]
2395 );
2396 break;
2397
2399 $res = $this->db->queryF(
2400 "
2401 SELECT COUNT(tst_test_question.question_fi) qcount,
2402 SUM(qpl_questions.points) qsum
2403
2404 FROM tst_test_question,
2405 qpl_questions,
2406 tst_active
2407
2408 WHERE tst_test_question.question_fi = qpl_questions.question_id
2409 AND tst_test_question.test_fi = tst_active.test_fi
2410 AND tst_active.active_id = %s
2411
2412 GROUP BY tst_test_question.test_fi
2413 ",
2414 ['integer'],
2415 [$active_id]
2416 );
2417 break;
2418
2419 default:
2420 throw new ilTestException("not supported question set type: $question_set_type");
2421 }
2422
2423 $row = $this->db->fetchAssoc($res);
2424
2425 if (is_array($row)) {
2426 return ["count" => $row["qcount"], "points" => $row["qsum"]];
2427 }
2428
2429 return ["count" => 0, "points" => 0];
2430 }
2431
2432 public function getCompleteEvaluationData($filterby = '', $filtertext = ''): ilTestEvaluationData
2433 {
2434 $data = $this->getUnfilteredEvaluationData();
2435 $data->setFilter($filterby, $filtertext);
2436 return $data;
2437 }
2438
2450 public function buildName(
2451 ?int $user_id,
2452 ?string $firstname,
2453 ?string $lastname
2454 ): string {
2455 if ($user_id === null
2456 || $firstname . $lastname === '') {
2457 return $this->lng->txt('deleted_user');
2458 }
2459
2460 if ($this->getAnonymity()) {
2461 return $this->lng->txt('anonymous');
2462 }
2463
2464 if ($user_id == ANONYMOUS_USER_ID) {
2465 return $lastname;
2466 }
2467
2468 return trim($lastname . ', ' . $firstname);
2469 }
2470
2471 public function evalTotalStartedAverageTime(?array $active_ids_to_filter = null): int
2472 {
2473 $query = "SELECT tst_times.* FROM tst_active, tst_times WHERE tst_active.test_fi = %s AND tst_active.active_id = tst_times.active_fi";
2474
2475 if ($active_ids_to_filter !== null && $active_ids_to_filter !== []) {
2476 $query .= " AND " . $this->db->in('active_id', $active_ids_to_filter, false, 'integer');
2477 }
2478
2479 $result = $this->db->queryF($query, ['integer'], [$this->getTestId()]);
2480 $times = [];
2481 while ($row = $this->db->fetchObject($result)) {
2482 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row->started, $matches);
2483 $epoch_1 = mktime(
2484 (int) $matches[4],
2485 (int) $matches[5],
2486 (int) $matches[6],
2487 (int) $matches[2],
2488 (int) $matches[3],
2489 (int) $matches[1]
2490 );
2491 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row->finished, $matches);
2492 $epoch_2 = mktime(
2493 (int) $matches[4],
2494 (int) $matches[5],
2495 (int) $matches[6],
2496 (int) $matches[2],
2497 (int) $matches[3],
2498 (int) $matches[1]
2499 );
2500 if (isset($times[$row->active_fi])) {
2501 $times[$row->active_fi] += ($epoch_2 - $epoch_1);
2502 } else {
2503 $times[$row->active_fi] = ($epoch_2 - $epoch_1);
2504 }
2505 }
2506 $max_time = 0;
2507 $counter = 0;
2508 foreach ($times as $value) {
2509 $max_time += $value;
2510 $counter++;
2511 }
2512 if ($counter === 0) {
2513 return 0;
2514 }
2515 return (int) round($max_time / $counter);
2516 }
2517
2525 bool $use_object_id = false,
2526 ?bool $equal_points = false,
2527 bool $could_be_offline = false,
2528 bool $show_path = false,
2529 bool $with_questioncount = false,
2530 string $permission = 'read'
2531 ): array {
2532 return ilObjQuestionPool::_getAvailableQuestionpools(
2533 $use_object_id,
2534 $equal_points ?? false,
2535 $could_be_offline,
2536 $show_path,
2537 $with_questioncount,
2538 $permission
2539 );
2540 }
2541
2548 public function getImagePath(): string
2549 {
2550 return CLIENT_WEB_DIR . "/assessment/" . $this->getId() . "/images/";
2551 }
2552
2559 public function getImagePathWeb()
2560 {
2561 $webdir = ilFileUtils::removeTrailingPathSeparators(CLIENT_WEB_DIR) . "/assessment/" . $this->getId() . "/images/";
2562 return str_replace(
2563 ilFileUtils::removeTrailingPathSeparators(ILIAS_ABSOLUTE_PATH),
2565 $webdir
2566 );
2567 }
2568
2577 public function createQuestionGUI($question_type, $question_id = -1): ?assQuestionGUI
2578 {
2579 if ((!$question_type) and ($question_id > 0)) {
2580 $question_type = $this->getQuestionType($question_id);
2581 }
2582
2583 if (!strlen($question_type)) {
2584 return null;
2585 }
2586
2587 if ($question_id > 0) {
2588 $question_gui = assQuestion::instantiateQuestionGUI($question_id);
2589 } else {
2590 $question_type_gui = $question_type . 'GUI';
2591 $question_gui = new $question_type_gui();
2592 }
2593
2594 return $question_gui;
2595 }
2596
2603 public static function _instanciateQuestion($question_id): ?assQuestion
2604 {
2605 if (strcmp((string) $question_id, "") !== 0) {
2606 return assQuestion::instantiateQuestion((int) $question_id);
2607 }
2608
2609 return null;
2610 }
2611
2616 public function moveQuestions(array $move_questions, int $target_index, int $insert_mode): void
2617 {
2618 $this->questions = array_values($this->questions);
2619 $array_pos = array_search($target_index, $this->questions);
2620 if ($insert_mode == 0) {
2621 $part1 = array_slice($this->questions, 0, $array_pos);
2622 $part2 = array_slice($this->questions, $array_pos);
2623 } elseif ($insert_mode == 1) {
2624 $part1 = array_slice($this->questions, 0, $array_pos + 1);
2625 $part2 = array_slice($this->questions, $array_pos + 1);
2626 }
2627 foreach ($move_questions as $question_id) {
2628 if (!(array_search($question_id, $part1) === false)) {
2629 unset($part1[array_search($question_id, $part1)]);
2630 }
2631 if (!(array_search($question_id, $part2) === false)) {
2632 unset($part2[array_search($question_id, $part2)]);
2633 }
2634 }
2635 $part1 = array_values($part1);
2636 $part2 = array_values($part2);
2637 $new_array = array_values(array_merge($part1, $move_questions, $part2));
2638 $this->questions = [];
2639 $counter = 1;
2640 foreach ($new_array as $question_id) {
2641 $this->questions[$counter] = $question_id;
2642 $counter++;
2643 }
2644 $this->saveQuestionsToDb();
2645
2646 if ($this->logger->isLoggingEnabled()) {
2647 $this->logger->logTestAdministrationInteraction(
2648 $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
2649 $this->getRefId(),
2650 $this->user->getId(),
2651 TestAdministrationInteractionTypes::QUESTION_MOVED,
2652 [
2653 AdditionalInformationGenerator::KEY_QUESTION_ORDER => $this->questions
2654 ]
2655 )
2656 );
2657 }
2658 }
2659
2660
2668 public function startingTimeReached(): bool
2669 {
2670 if ($this->isStartingTimeEnabled() && $this->getStartingTime() != 0) {
2671 $now = time();
2672 if ($now < $this->getStartingTime()) {
2673 return false;
2674 }
2675 }
2676 return true;
2677 }
2678
2686 public function endingTimeReached(): bool
2687 {
2688 if ($this->isEndingTimeEnabled() && $this->getEndingTime() != 0) {
2689 $now = time();
2690 if ($now > $this->getEndingTime()) {
2691 return true;
2692 }
2693 }
2694 return false;
2695 }
2696
2702 public function getAvailableQuestions($arr_filter, $completeonly = 0): array
2703 {
2704 $available_pools = array_keys(ilObjQuestionPool::_getAvailableQuestionpools(true, false, false, false, false));
2705 $available = "";
2706 if (count($available_pools)) {
2707 $available = " AND " . $this->db->in('qpl_questions.obj_fi', $available_pools, false, 'integer');
2708 } else {
2709 return [];
2710 }
2711 if ($completeonly) {
2712 $available .= " AND qpl_questions.complete = " . $this->db->quote("1", 'text');
2713 }
2714
2715 $where = "";
2716 if (is_array($arr_filter)) {
2717 if (array_key_exists('title', $arr_filter) && strlen($arr_filter['title'])) {
2718 $where .= " AND " . $this->db->like('qpl_questions.title', 'text', "%%" . $arr_filter['title'] . "%%");
2719 }
2720 if (array_key_exists('description', $arr_filter) && strlen($arr_filter['description'])) {
2721 $where .= " AND " . $this->db->like('qpl_questions.description', 'text', "%%" . $arr_filter['description'] . "%%");
2722 }
2723 if (array_key_exists('author', $arr_filter) && strlen($arr_filter['author'])) {
2724 $where .= " AND " . $this->db->like('qpl_questions.author', 'text', "%%" . $arr_filter['author'] . "%%");
2725 }
2726 if (array_key_exists('type', $arr_filter) && strlen($arr_filter['type'])) {
2727 $where .= " AND qpl_qst_type.type_tag = " . $this->db->quote($arr_filter['type'], 'text');
2728 }
2729 if (array_key_exists('qpl', $arr_filter) && strlen($arr_filter['qpl'])) {
2730 $where .= " AND " . $this->db->like('object_data.title', 'text', "%%" . $arr_filter['qpl'] . "%%");
2731 }
2732 }
2733
2734 $original_ids = &$this->getExistingQuestions();
2735 $original_clause = " qpl_questions.original_id IS NULL";
2736 if (count($original_ids)) {
2737 $original_clause = " qpl_questions.original_id IS NULL AND " . $this->db->in('qpl_questions.question_id', $original_ids, true, 'integer');
2738 }
2739
2740 $query_result = $this->db->query("
2741 SELECT qpl_questions.*, qpl_questions.tstamp,
2742 qpl_qst_type.type_tag, qpl_qst_type.plugin, qpl_qst_type.plugin_name,
2743 object_data.title parent_title
2744 FROM qpl_questions, qpl_qst_type, object_data
2745 WHERE $original_clause $available
2746 AND object_data.obj_id = qpl_questions.obj_fi
2747 AND qpl_questions.tstamp > 0
2748 AND qpl_questions.question_type_fi = qpl_qst_type.question_type_id
2749 $where
2750 ");
2751 $rows = [];
2752
2753 if ($query_result->numRows()) {
2754 while ($row = $this->db->fetchAssoc($query_result)) {
2756
2757 if (!$row['plugin']) {
2758 $row[ 'ttype' ] = $this->lng->txt($row[ "type_tag" ]);
2759
2760 $rows[] = $row;
2761 continue;
2762 }
2763
2764 $plugin = $this->component_repository->getPluginByName($row['plugin_name']);
2765 if (!$plugin->isActive()) {
2766 continue;
2767 }
2768
2769 $pl = $this->component_factory->getPlugin($plugin->getId());
2770 $row[ 'ttype' ] = $pl->getQuestionTypeTranslation();
2771
2772 $rows[] = $row;
2773 }
2774 }
2775 return $rows;
2776 }
2777
2782 public function fromXML(ilQTIAssessment $assessment, array $mappings): void
2783 {
2784 $this->saveToDb(true);
2785
2786 $main_settings = $this->getMainSettings();
2787 $general_settings = $main_settings->getGeneralSettings();
2788 $introduction_settings = $main_settings->getIntroductionSettings();
2789 $access_settings = $main_settings->getAccessSettings();
2790 $test_behaviour_settings = $main_settings->getTestBehaviourSettings();
2791 $question_behaviour_settings = $main_settings->getQuestionBehaviourSettings();
2792 $participant_functionality_settings = $main_settings->getParticipantFunctionalitySettings();
2793 $finishing_settings = $main_settings->getFinishingSettings();
2794 $additional_settings = $main_settings->getAdditionalSettings();
2795
2796 $introduction_settings = $introduction_settings->withIntroductionEnabled(false);
2797 foreach ($assessment->objectives as $objectives) {
2798 foreach ($objectives->materials as $material) {
2799 $introduction_settings = $this->addIntroductionToSettingsFromImport(
2800 $introduction_settings,
2801 $this->qtiMaterialToArray($material),
2802 $mappings
2803 );
2804 }
2805 }
2806
2807 if ($assessment->getPresentationMaterial()
2808 && $assessment->getPresentationMaterial()->getFlowMat(0)
2809 && $assessment->getPresentationMaterial()->getFlowMat(0)->getMaterial(0)) {
2810 $finishing_settings = $this->addConcludingRemarksToSettingsFromImport(
2811 $finishing_settings,
2812 $this->qtiMaterialToArray(
2813 $assessment->getPresentationMaterial()->getFlowMat(0)->getMaterial(0)
2814 ),
2815 $mappings
2816 );
2817 }
2818
2819 $score_settings = $this->getScoreSettings();
2820 $scoring_settings = $score_settings->getScoringSettings();
2821 $gamification_settings = $score_settings->getGamificationSettings();
2822 $result_summary_settings = $score_settings->getResultSummarySettings();
2823 $result_details_settings = $score_settings->getResultDetailsSettings();
2824
2825 $mark_steps = [];
2826 foreach ($assessment->qtimetadata as $metadata) {
2827 switch ($metadata["label"]) {
2828 case "solution_details":
2829 $result_details_settings = $result_details_settings->withShowPassDetails((bool) $metadata["entry"]);
2830 break;
2831 case "show_solution_list_comparison":
2832 $result_details_settings = $result_details_settings->withShowSolutionListComparison((bool) $metadata["entry"]);
2833 break;
2834 case "print_bs_with_res":
2835 $result_details_settings = $result_details_settings->withShowSolutionListComparison((bool) $metadata["entry"]);
2836 break;
2837 case "author":
2838 $this->saveAuthorToMetadata($metadata["entry"]);
2839 break;
2840 case "nr_of_tries":
2841 $test_behaviour_settings = $test_behaviour_settings->withNumberOfTries((int) $metadata["entry"]);
2842 break;
2843 case 'block_after_passed':
2844 $test_behaviour_settings = $test_behaviour_settings->withBlockAfterPassedEnabled((bool) $metadata['entry']);
2845 break;
2846 case "pass_waiting":
2847 $test_behaviour_settings = $test_behaviour_settings->withPassWaiting($metadata["entry"]);
2848 break;
2849 case "kiosk":
2850 $test_behaviour_settings = $test_behaviour_settings->withKioskMode((int) $metadata["entry"]);
2851 break;
2852 case 'show_introduction':
2853 $introduction_settings = $introduction_settings->withIntroductionEnabled((bool) $metadata['entry']);
2854 break;
2855 case "showfinalstatement":
2856 case 'show_concluding_remarks':
2857 $finishing_settings = $finishing_settings->withConcludingRemarksEnabled((bool) $metadata["entry"]);
2858 break;
2859 case 'exam_conditions':
2860 $introduction_settings = $introduction_settings->withExamConditionsCheckboxEnabled($metadata['entry'] === '1');
2861 break;
2862 case "highscore_enabled":
2863 $gamification_settings = $gamification_settings->withHighscoreEnabled((bool) $metadata["entry"]);
2864 break;
2865 case "highscore_anon":
2866 $gamification_settings = $gamification_settings->withHighscoreAnon((bool) $metadata["entry"]);
2867 break;
2868 case "highscore_achieved_ts":
2869 $gamification_settings = $gamification_settings->withHighscoreAchievedTS((bool) $metadata["entry"]);
2870 break;
2871 case "highscore_score":
2872 $gamification_settings = $gamification_settings->withHighscoreScore((bool) $metadata["entry"]);
2873 break;
2874 case "highscore_percentage":
2875 $gamification_settings = $gamification_settings->withHighscorePercentage((bool) $metadata["entry"]);
2876 break;
2877 case "highscore_wtime":
2878 $gamification_settings = $gamification_settings->withHighscoreWTime((bool) $metadata["entry"]);
2879 break;
2880 case "highscore_own_table":
2881 $gamification_settings = $gamification_settings->withHighscoreOwnTable((bool) $metadata["entry"]);
2882 break;
2883 case "highscore_top_table":
2884 $gamification_settings = $gamification_settings->withHighscoreTopTable((bool) $metadata["entry"]);
2885 break;
2886 case "highscore_top_num":
2887 $gamification_settings = $gamification_settings->withHighscoreTopNum((int) $metadata["entry"]);
2888 break;
2889 case "use_previous_answers":
2890 $participant_functionality_settings = $participant_functionality_settings->withUsePreviousAnswerAllowed((bool) $metadata["entry"]);
2891 break;
2892 case 'question_list_enabled':
2893 $participant_functionality_settings = $participant_functionality_settings->withQuestionListEnabled((bool) $metadata['entry']);
2894 // no break
2895 case "title_output":
2896 $question_behaviour_settings = $question_behaviour_settings->withQuestionTitleOutputMode((int) $metadata["entry"]);
2897 break;
2898 case "question_set_type":
2899 if ($metadata['entry'] === self::QUESTION_SET_TYPE_RANDOM) {
2900 $this->questions = [];
2901 }
2902 $general_settings = $general_settings->withQuestionSetType($metadata["entry"]);
2903 break;
2904 case "anonymity":
2905 $general_settings = $general_settings->withAnonymity((bool) $metadata["entry"]);
2906 break;
2907 case "results_presentation":
2908 $result_details_settings = $result_details_settings->withResultsPresentation((int) $metadata["entry"]);
2909 break;
2910 case "reset_processing_time":
2911 $test_behaviour_settings = $test_behaviour_settings->withResetProcessingTime($metadata["entry"] === '1');
2912 break;
2913 case "answer_feedback_points":
2914 $question_behaviour_settings = $question_behaviour_settings->withInstantFeedbackPointsEnabled((bool) $metadata["entry"]);
2915 break;
2916 case "answer_feedback":
2917 $question_behaviour_settings = $question_behaviour_settings->withInstantFeedbackGenericEnabled((bool) $metadata["entry"]);
2918 break;
2919 case 'instant_feedback_specific':
2920 $question_behaviour_settings = $question_behaviour_settings->withInstantFeedbackSpecificEnabled((bool) $metadata['entry']);
2921 break;
2922 case "instant_verification":
2923 $question_behaviour_settings = $question_behaviour_settings->withInstantFeedbackSolutionEnabled((bool) $metadata["entry"]);
2924 break;
2925 case "force_instant_feedback":
2926 $question_behaviour_settings = $question_behaviour_settings->withForceInstantFeedbackOnNextQuestion((bool) $metadata["entry"]);
2927 break;
2928 case "follow_qst_answer_fixation":
2929 $question_behaviour_settings = $question_behaviour_settings->withLockAnswerOnNextQuestionEnabled((bool) $metadata["entry"]);
2930 break;
2931 case "instant_feedback_answer_fixation":
2932 $question_behaviour_settings = $question_behaviour_settings->withLockAnswerOnInstantFeedbackEnabled((bool) $metadata["entry"]);
2933 break;
2934 case "show_cancel":
2935 case "suspend_test_allowed":
2936 $participant_functionality_settings = $participant_functionality_settings->withSuspendTestAllowed((bool) $metadata["entry"]);
2937 break;
2938 case "sequence_settings":
2939 $participant_functionality_settings = $participant_functionality_settings->withPostponedQuestionsMoveToEnd((bool) $metadata["entry"]);
2940 break;
2941 case "show_marker":
2942 $participant_functionality_settings = $participant_functionality_settings->withQuestionMarkingEnabled((bool) $metadata["entry"]);
2943 break;
2944 case "fixed_participants":
2945 $access_settings = $access_settings->withFixedParticipants((bool) $metadata["entry"]);
2946 break;
2947 case "score_reporting":
2948 if ($metadata['entry'] !== null) {
2949 $result_summary_settings = $result_summary_settings->withScoreReporting(
2950 ScoreReportingTypes::tryFrom((int) $metadata['entry']) ?? ScoreReportingTypes::SCORE_REPORTING_DISABLED
2951 );
2952 }
2953 break;
2954 case "shuffle_questions":
2955 $question_behaviour_settings = $question_behaviour_settings->withShuffleQuestions((bool) $metadata["entry"]);
2956 break;
2957 case "count_system":
2958 $scoring_settings = $scoring_settings->withCountSystem((int) $metadata["entry"]);
2959 break;
2960 case "exportsettings":
2961 $result_details_settings = $result_details_settings->withExportSettings((int) $metadata["entry"]);
2962 break;
2963 case "score_cutting":
2964 $scoring_settings = $scoring_settings->withScoreCutting((int) $metadata["entry"]);
2965 break;
2966 case "password":
2967 $access_settings = $access_settings->withPasswordEnabled(
2968 $metadata["entry"] !== null && $metadata["entry"] !== ''
2969 )->withPassword($metadata["entry"]);
2970 break;
2971 case 'ip_range_from':
2972 if ($metadata['entry'] !== '') {
2973 $access_settings = $access_settings->withIpRangeFrom($metadata['entry']);
2974 }
2975 break;
2976 case 'ip_range_to':
2977 if ($metadata['entry'] !== '') {
2978 $access_settings = $access_settings->withIpRangeTo($metadata['entry']);
2979 }
2980 break;
2981 case "pass_scoring":
2982 $scoring_settings = $scoring_settings->withPassScoring((int) $metadata["entry"]);
2983 break;
2984 case 'pass_deletion_allowed':
2985 $result_summary_settings = $result_summary_settings->withPassDeletionAllowed((bool) $metadata["entry"]);
2986 break;
2987 case "usr_pass_overview_mode":
2988 $participant_functionality_settings = $participant_functionality_settings->withUsrPassOverviewMode((int) $metadata["entry"]);
2989 break;
2990 case "question_list":
2991 $participant_functionality_settings = $participant_functionality_settings->withQuestionListEnabled((bool) $metadata["entry"]);
2992 break;
2993
2994 case "reporting_date":
2995 $reporting_date = $this->buildDateTimeImmutableFromPeriod($metadata['entry']);
2996 if ($reporting_date !== null) {
2997 $result_summary_settings = $result_summary_settings->withReportingDate($reporting_date);
2998 }
2999 break;
3000 case 'enable_processing_time':
3001 $test_behaviour_settings = $test_behaviour_settings->withProcessingTimeEnabled((bool) $metadata['entry']);
3002 break;
3003 case "processing_time":
3004 $test_behaviour_settings = $test_behaviour_settings->withProcessingTime($metadata['entry']);
3005 break;
3006 case "starting_time":
3007 $starting_time = $this->buildDateTimeImmutableFromPeriod($metadata['entry']);
3008 if ($starting_time !== null) {
3009 $access_settings = $access_settings->withStartTime($starting_time)
3010 ->withStartTimeEnabled(true);
3011 }
3012 break;
3013 case "ending_time":
3014 $ending_time = $this->buildDateTimeImmutableFromPeriod($metadata['entry']);
3015 if ($ending_time !== null) {
3016 $access_settings = $access_settings->withEndTime($ending_time)
3017 ->withStartTimeEnabled(true);
3018 }
3019 break;
3020 case "enable_examview":
3021 $finishing_settings = $finishing_settings->withShowAnswerOverview((bool) $metadata["entry"]);
3022 break;
3023 case 'redirection_mode':
3024 $finishing_settings = $finishing_settings->withRedirectionMode(
3025 RedirectionModes::tryFrom((int) ($metadata['entry'] ?? 0)) ?? RedirectionModes::NONE
3026 );
3027 break;
3028 case 'redirection_url':
3029 $finishing_settings = $finishing_settings->withRedirectionUrl($metadata['entry']);
3030 break;
3031 case 'examid_in_test_pass':
3032 $test_behaviour_settings = $test_behaviour_settings->withExamIdInTestAttemptEnabled((bool) $metadata['entry']);
3033 break;
3034 case 'examid_in_test_res':
3035 $result_details_settings = $result_details_settings->withShowExamIdInTestResults((bool) $metadata["entry"]);
3036 break;
3037 case 'skill_service':
3038 $additional_settings = $additional_settings->withSkillsServiceEnabled((bool) $metadata['entry']);
3039 break;
3040 case 'show_grading_status':
3041 $result_summary_settings = $result_summary_settings->withShowGradingStatusEnabled((bool) $metadata["entry"]);
3042 break;
3043 case 'show_grading_mark':
3044 $result_summary_settings = $result_summary_settings->withShowGradingMarkEnabled((bool) $metadata["entry"]);
3045 break;
3046 case 'autosave':
3047 $question_behaviour_settings = $question_behaviour_settings->withAutosaveEnabled((bool) $metadata['entry']);
3048 break;
3049 case 'autosave_ival':
3050 $question_behaviour_settings = $question_behaviour_settings->withAutosaveInterval((int) $metadata['entry']);
3051 break;
3052 case 'show_summary':
3053 $participant_functionality_settings = $participant_functionality_settings->withQuestionListEnabled(($metadata['entry'] & 1) > 0)
3054 ->withUsrPassOverviewMode((int) $metadata['entry']);
3055
3056 // no break
3057 case 'hide_info_tab':
3058 $additional_settings = $additional_settings->withHideInfoTab($metadata['entry'] === '1');
3059 }
3060 if (preg_match("/mark_step_\d+/", $metadata["label"])) {
3061 $xmlmark = $metadata["entry"];
3062 preg_match("/<short>(.*?)<\/short>/", $xmlmark, $matches);
3063 $mark_short = $matches[1];
3064 preg_match("/<official>(.*?)<\/official>/", $xmlmark, $matches);
3065 $mark_official = $matches[1];
3066 preg_match("/<percentage>(.*?)<\/percentage>/", $xmlmark, $matches);
3067 $mark_percentage = (float) $matches[1];
3068 preg_match("/<passed>(.*?)<\/passed>/", $xmlmark, $matches);
3069 $mark_passed = (bool) $matches[1];
3070 $mark_steps[] = new Mark($mark_short, $mark_official, $mark_percentage, $mark_passed);
3071 }
3072 }
3073 $this->mark_schema = $this->getMarkSchema()->withMarkSteps($mark_steps);
3074 $this->saveToDb();
3075 $this->getObjectProperties()->storePropertyTitleAndDescription(
3076 $this->getObjectProperties()->getPropertyTitleAndDescription()
3077 ->withTitle($assessment->getTitle())
3078 ->withDescription($assessment->getComment())
3079 );
3080 $this->addToNewsOnOnline(false, $this->getObjectProperties()->getPropertyIsOnline()->getIsOnline());
3081 $main_settings = $main_settings
3082 ->withGeneralSettings($general_settings)
3083 ->withIntroductionSettings($introduction_settings)
3084 ->withAccessSettings($access_settings)
3085 ->withParticipantFunctionalitySettings($participant_functionality_settings)
3086 ->withTestBehaviourSettings($test_behaviour_settings)
3087 ->withQuestionBehaviourSettings($question_behaviour_settings)
3088 ->withFinishingSettings($finishing_settings)
3089 ->withAdditionalSettings($additional_settings);
3090 $this->getMainSettingsRepository()->store($main_settings);
3091 $this->main_settings = $main_settings;
3092
3093 $score_settings = $score_settings
3094 ->withGamificationSettings($gamification_settings)
3095 ->withScoringSettings($scoring_settings)
3096 ->withResultDetailsSettings($result_details_settings)
3097 ->withResultSummarySettings($result_summary_settings);
3098 $this->getScoreSettingsRepository()->store($score_settings);
3099 $this->score_settings = $score_settings;
3100 $this->loadFromDb();
3101 }
3102
3104 SettingsIntroduction $settings,
3105 array $material,
3106 array $mappings
3108 if (!str_starts_with($material['text'], '<PageObject>')) {
3109 return $settings;
3110 }
3111
3112 $text = $this->replaceFilesInPageImports(
3113 $this->replaceMobsInPageImports(
3114 $material['text'],
3115 $mappings['components/ILIAS/MediaObjects']['mob'] ?? []
3116 ),
3117 $mappings['components/ILIAS/File']['file'] ?? []
3118 );
3119
3120 $page_object = new ilTestPage();
3121 $page_object->setParentId($this->getId());
3122 $page_object->setXMLContent($text);
3123 $new_page_id = $page_object->createPageWithNextId();
3124 return $settings->withIntroductionPageId($new_page_id);
3125 }
3126
3128 SettingsFinishing $settings,
3129 array $material,
3130 array $mappings
3132 if (!str_starts_with($material['text'], '<PageObject>')) {
3133 return $settings;
3134 }
3135
3136 $text = $this->replaceFilesInPageImports(
3137 $this->replaceMobsInPageImports(
3138 $material['text'],
3139 $mappings['components/ILIAS/MediaObjects']['mob'] ?? []
3140 ),
3141 $mappings['components/ILIAS/File']['file'] ?? []
3142 );
3143
3144 $page_object = new ilTestPage();
3145 $page_object->setParentId($this->getId());
3146 $page_object->setXMLContent($text);
3147 $new_page_id = $page_object->createPageWithNextId();
3148 return $settings->withConcludingRemarksPageId($new_page_id);
3149 }
3150
3151 private function replaceMobsInPageImports(string $text, array $mappings): string
3152 {
3153 preg_match_all('/il_(\d+)_mob_(\d+)/', $text, $matches);
3154 foreach ($matches[0] as $index => $match) {
3155 if (empty($mappings[$matches[2][$index]])) {
3156 continue;
3157 }
3158 $text = str_replace($match, "il__mob_{$mappings[$matches[2][$index]]}", $text);
3159 ilObjMediaObject::_saveUsage((int) $mappings[$matches[2][$index]], 'tst', $this->getId());
3160 }
3161 return $text;
3162 }
3163
3164 private function replaceFilesInPageImports(string $text, array $mappings): string
3165 {
3166 preg_match_all('/il_(\d+)_file_(\d+)/', $text, $matches);
3167 foreach ($matches[0] as $index => $match) {
3168 if (empty($mappings[$matches[2][$index]])) {
3169 continue;
3170 }
3171 $text = str_replace($match, "il__file_{$mappings[$matches[2][$index]]}", $text);
3172 }
3173 return $text;
3174 }
3175
3181 public function toXML(): string
3182 {
3183 $main_settings = $this->getMainSettings();
3184 $a_xml_writer = new ilXmlWriter();
3185 // set xml header
3186 $a_xml_writer->xmlHeader();
3187 $a_xml_writer->xmlSetDtdDef("<!DOCTYPE questestinterop SYSTEM \"ims_qtiasiv1p2p1.dtd\">");
3188 $a_xml_writer->xmlStartTag("questestinterop");
3189
3190 $attrs = [
3191 "ident" => "il_" . IL_INST_ID . "_tst_" . $this->getTestId(),
3192 "title" => $this->getTitle()
3193 ];
3194 $a_xml_writer->xmlStartTag("assessment", $attrs);
3195 $a_xml_writer->xmlElement("qticomment", null, $this->getDescription());
3196
3197 if ($main_settings->getTestBehaviourSettings()->getProcessingTimeEnabled()) {
3198 $a_xml_writer->xmlElement(
3199 "duration",
3200 null,
3201 $this->getProcessingTimeForXML()
3202 );
3203 }
3204
3205 $a_xml_writer->xmlStartTag("qtimetadata");
3206 $a_xml_writer->xmlStartTag("qtimetadatafield");
3207 $a_xml_writer->xmlElement("fieldlabel", null, "ILIAS_VERSION");
3208 $a_xml_writer->xmlElement("fieldentry", null, ILIAS_VERSION);
3209 $a_xml_writer->xmlEndTag("qtimetadatafield");
3210
3211 $a_xml_writer->xmlStartTag("qtimetadatafield");
3212 $a_xml_writer->xmlElement("fieldlabel", null, "anonymity");
3213 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getGeneralSettings()->getAnonymity() ? 1 : 0);
3214 $a_xml_writer->xmlEndTag("qtimetadatafield");
3215
3216 $a_xml_writer->xmlStartTag("qtimetadatafield");
3217 $a_xml_writer->xmlElement("fieldlabel", null, "question_set_type");
3218 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getGeneralSettings()->getQuestionSetType());
3219 $a_xml_writer->xmlEndTag("qtimetadatafield");
3220
3221 $a_xml_writer->xmlStartTag("qtimetadatafield");
3222 $a_xml_writer->xmlElement("fieldlabel", null, "sequence_settings");
3223 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getParticipantFunctionalitySettings()->getPostponedQuestionsMoveToEnd() ? 1 : 0);
3224 $a_xml_writer->xmlEndTag("qtimetadatafield");
3225
3226 $a_xml_writer->xmlStartTag("qtimetadatafield");
3227 $a_xml_writer->xmlElement("fieldlabel", null, "author");
3228 $a_xml_writer->xmlElement("fieldentry", null, $this->getAuthor());
3229 $a_xml_writer->xmlEndTag("qtimetadatafield");
3230
3231 $a_xml_writer->xmlStartTag("qtimetadatafield");
3232 $a_xml_writer->xmlElement("fieldlabel", null, "reset_processing_time");
3233 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getTestBehaviourSettings()->getResetProcessingTime() ? 1 : 0);
3234 $a_xml_writer->xmlEndTag("qtimetadatafield");
3235
3236 $a_xml_writer->xmlStartTag("qtimetadatafield");
3237 $a_xml_writer->xmlElement("fieldlabel", null, "count_system");
3238 $a_xml_writer->xmlElement("fieldentry", null, $this->getCountSystem());
3239 $a_xml_writer->xmlEndTag("qtimetadatafield");
3240
3241 $a_xml_writer->xmlStartTag("qtimetadatafield");
3242 $a_xml_writer->xmlElement("fieldlabel", null, "score_cutting");
3243 $a_xml_writer->xmlElement("fieldentry", null, $this->getScoreCutting());
3244 $a_xml_writer->xmlEndTag("qtimetadatafield");
3245
3246 $a_xml_writer->xmlStartTag("qtimetadatafield");
3247 $a_xml_writer->xmlElement("fieldlabel", null, "password");
3248 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getAccessSettings()->getPassword() ?? '');
3249 $a_xml_writer->xmlEndTag("qtimetadatafield");
3250
3251 $a_xml_writer->xmlStartTag("qtimetadatafield");
3252 $a_xml_writer->xmlElement("fieldlabel", null, "ip_range_from");
3253 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getAccessSettings()->getIpRangeFrom());
3254 $a_xml_writer->xmlEndTag("qtimetadatafield");
3255
3256 $a_xml_writer->xmlStartTag("qtimetadatafield");
3257 $a_xml_writer->xmlElement("fieldlabel", null, "ip_range_to");
3258 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getAccessSettings()->getIpRangeTo());
3259 $a_xml_writer->xmlEndTag("qtimetadatafield");
3260
3261 $a_xml_writer->xmlStartTag("qtimetadatafield");
3262 $a_xml_writer->xmlElement("fieldlabel", null, "pass_scoring");
3263 $a_xml_writer->xmlElement("fieldentry", null, $this->getPassScoring());
3264 $a_xml_writer->xmlEndTag("qtimetadatafield");
3265
3266 $a_xml_writer->xmlStartTag('qtimetadatafield');
3267 $a_xml_writer->xmlElement('fieldlabel', null, 'pass_deletion_allowed');
3268 $a_xml_writer->xmlElement('fieldentry', null, $this->isPassDeletionAllowed() ? 1 : 0);
3269 $a_xml_writer->xmlEndTag('qtimetadatafield');
3270
3271 if ($this->getScoreSettings()->getResultSummarySettings()->getReportingDate() !== null) {
3272 $a_xml_writer->xmlStartTag("qtimetadatafield");
3273 $a_xml_writer->xmlElement("fieldlabel", null, "reporting_date");
3274 $a_xml_writer->xmlElement(
3275 "fieldentry",
3276 null,
3277 $this->buildIso8601PeriodForExportCompatibility(
3278 $this->getScoreSettings()->getResultSummarySettings()->getReportingDate(),
3279 ),
3280 );
3281 $a_xml_writer->xmlEndTag("qtimetadatafield");
3282 }
3283
3284 $a_xml_writer->xmlStartTag("qtimetadatafield");
3285 $a_xml_writer->xmlElement("fieldlabel", null, "nr_of_tries");
3286 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getTestBehaviourSettings()->getNumberOfTries());
3287 $a_xml_writer->xmlEndTag("qtimetadatafield");
3288
3289 $a_xml_writer->xmlStartTag('qtimetadatafield');
3290 $a_xml_writer->xmlElement('fieldlabel', null, 'block_after_passed');
3291 $a_xml_writer->xmlElement('fieldentry', null, $main_settings->getTestBehaviourSettings()->getBlockAfterPassedEnabled() ? 1 : 0);
3292 $a_xml_writer->xmlEndTag('qtimetadatafield');
3293
3294 $a_xml_writer->xmlStartTag("qtimetadatafield");
3295 $a_xml_writer->xmlElement("fieldlabel", null, "pass_waiting");
3296 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getTestBehaviourSettings()->getPassWaiting());
3297 $a_xml_writer->xmlEndTag("qtimetadatafield");
3298
3299 $a_xml_writer->xmlStartTag("qtimetadatafield");
3300 $a_xml_writer->xmlElement("fieldlabel", null, "kiosk");
3301 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getTestBehaviourSettings()->getKioskMode());
3302 $a_xml_writer->xmlEndTag("qtimetadatafield");
3303
3304 $a_xml_writer->xmlStartTag('qtimetadatafield');
3305 $a_xml_writer->xmlElement("fieldlabel", null, "redirection_mode");
3306 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getFinishingSettings()->getRedirectionMode()->value);
3307 $a_xml_writer->xmlEndTag("qtimetadatafield");
3308
3309 $a_xml_writer->xmlStartTag('qtimetadatafield');
3310 $a_xml_writer->xmlElement("fieldlabel", null, "redirection_url");
3311 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getFinishingSettings()->getRedirectionUrl());
3312 $a_xml_writer->xmlEndTag("qtimetadatafield");
3313
3314 $a_xml_writer->xmlStartTag("qtimetadatafield");
3315 $a_xml_writer->xmlElement("fieldlabel", null, "use_previous_answers");
3316 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getParticipantFunctionalitySettings()->getUsePreviousAnswerAllowed() ? 1 : 0);
3317 $a_xml_writer->xmlEndTag("qtimetadatafield");
3318
3319 $a_xml_writer->xmlStartTag('qtimetadatafield');
3320 $a_xml_writer->xmlElement('fieldlabel', null, 'question_list_enabled');
3321 $a_xml_writer->xmlElement('fieldentry', null, $main_settings->getParticipantFunctionalitySettings()->getQuestionListEnabled() ? 1 : 0);
3322 $a_xml_writer->xmlEndTag('qtimetadatafield');
3323
3324 $a_xml_writer->xmlStartTag("qtimetadatafield");
3325 $a_xml_writer->xmlElement("fieldlabel", null, "title_output");
3326 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getQuestionTitleOutputMode());
3327 $a_xml_writer->xmlEndTag("qtimetadatafield");
3328
3329 $a_xml_writer->xmlStartTag("qtimetadatafield");
3330 $a_xml_writer->xmlElement("fieldlabel", null, "results_presentation");
3331 $a_xml_writer->xmlElement("fieldentry", null, $this->getScoreSettings()->getResultDetailsSettings()->getResultsPresentation());
3332 $a_xml_writer->xmlEndTag("qtimetadatafield");
3333
3334 $a_xml_writer->xmlStartTag("qtimetadatafield");
3335 $a_xml_writer->xmlElement("fieldlabel", null, "examid_in_test_pass");
3336 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getTestBehaviourSettings()->getExamIdInTestAttemptEnabled() ? 1 : 0);
3337 $a_xml_writer->xmlEndTag("qtimetadatafield");
3338
3339 $a_xml_writer->xmlStartTag("qtimetadatafield");
3340 $a_xml_writer->xmlElement("fieldlabel", null, "examid_in_test_res");
3341 $a_xml_writer->xmlElement("fieldentry", null, $this->getScoreSettings()->getResultDetailsSettings()->getShowExamIdInTestResults() ? 1 : 0);
3342 $a_xml_writer->xmlEndTag("qtimetadatafield");
3343
3344 $a_xml_writer->xmlStartTag("qtimetadatafield");
3345 $a_xml_writer->xmlElement("fieldlabel", null, "usr_pass_overview_mode");
3346 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getParticipantFunctionalitySettings()->getUsrPassOverviewMode());
3347 $a_xml_writer->xmlEndTag("qtimetadatafield");
3348
3349 $a_xml_writer->xmlStartTag("qtimetadatafield");
3350 $a_xml_writer->xmlElement("fieldlabel", null, "score_reporting");
3351 $a_xml_writer->xmlElement("fieldentry", null, $this->getScoreSettings()->getResultSummarySettings()->getScoreReporting()->value);
3352 $a_xml_writer->xmlEndTag("qtimetadatafield");
3353
3354 $a_xml_writer->xmlStartTag("qtimetadatafield");
3355 $a_xml_writer->xmlElement("fieldlabel", null, "show_solution_list_comparison");
3356 $a_xml_writer->xmlElement("fieldentry", null, $this->score_settings->getResultDetailsSettings()->getShowSolutionListComparison() ? 1 : 0);
3357 $a_xml_writer->xmlEndTag("qtimetadatafield");
3358
3359 $a_xml_writer->xmlStartTag("qtimetadatafield");
3360 $a_xml_writer->xmlElement("fieldlabel", null, "instant_verification");
3361 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackSolutionEnabled() ? 1 : 0);
3362 $a_xml_writer->xmlEndTag("qtimetadatafield");
3363
3364 $a_xml_writer->xmlStartTag("qtimetadatafield");
3365 $a_xml_writer->xmlElement("fieldlabel", null, "answer_feedback");
3366 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackGenericEnabled() ? 1 : 0);
3367 $a_xml_writer->xmlEndTag("qtimetadatafield");
3368
3369 $a_xml_writer->xmlStartTag("qtimetadatafield");
3370 $a_xml_writer->xmlElement("fieldlabel", null, "instant_feedback_specific");
3371 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackSpecificEnabled() ? 1 : 0);
3372 $a_xml_writer->xmlEndTag("qtimetadatafield");
3373
3374 $a_xml_writer->xmlStartTag("qtimetadatafield");
3375 $a_xml_writer->xmlElement("fieldlabel", null, "answer_feedback_points");
3376 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackPointsEnabled() ? 1 : 0);
3377 $a_xml_writer->xmlEndTag("qtimetadatafield");
3378
3379 $a_xml_writer->xmlStartTag("qtimetadatafield");
3380 $a_xml_writer->xmlElement("fieldlabel", null, "follow_qst_answer_fixation");
3381 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getLockAnswerOnNextQuestionEnabled() ? 1 : 0);
3382 $a_xml_writer->xmlEndTag("qtimetadatafield");
3383
3384 $a_xml_writer->xmlStartTag("qtimetadatafield");
3385 $a_xml_writer->xmlElement("fieldlabel", null, "instant_feedback_answer_fixation");
3386 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getLockAnswerOnInstantFeedbackEnabled() ? 1 : 0);
3387 $a_xml_writer->xmlEndTag("qtimetadatafield");
3388
3389 $a_xml_writer->xmlStartTag("qtimetadatafield");
3390 $a_xml_writer->xmlElement("fieldlabel", null, "force_instant_feedback");
3391 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getForceInstantFeedbackOnNextQuestion() ? 1 : 0);
3392 $a_xml_writer->xmlEndTag("qtimetadatafield");
3393
3394 $highscore_metadata = [
3395 'highscore_enabled' => $this->getHighscoreEnabled(),
3396 'highscore_anon' => $this->getHighscoreAnon(),
3397 'highscore_achieved_ts' => $this->getHighscoreAchievedTS(),
3398 'highscore_score' => $this->getHighscoreScore(),
3399 'highscore_percentage' => $this->getHighscorePercentage(),
3400 'highscore_wtime' => $this->getHighscoreWTime(),
3401 'highscore_own_table' => $this->getHighscoreOwnTable(),
3402 'highscore_top_table' => $this->getHighscoreTopTable(),
3403 ];
3404 foreach ($highscore_metadata as $label => $value) {
3405 $a_xml_writer->xmlStartTag("qtimetadatafield");
3406 $a_xml_writer->xmlElement("fieldlabel", null, $label);
3407 $a_xml_writer->xmlElement("fieldentry", null, $value ? 1 : 0);
3408 $a_xml_writer->xmlEndTag("qtimetadatafield");
3409 }
3410 $a_xml_writer->xmlStartTag("qtimetadatafield");
3411 $a_xml_writer->xmlElement("fieldlabel", null, "highscore_top_num");
3412 $a_xml_writer->xmlElement("fieldentry", null, $this->getHighscoreTopNum());
3413 $a_xml_writer->xmlEndTag("qtimetadatafield");
3414
3415 $a_xml_writer->xmlStartTag("qtimetadatafield");
3416 $a_xml_writer->xmlElement("fieldlabel", null, "suspend_test_allowed");
3417 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getParticipantFunctionalitySettings()->getSuspendTestAllowed() ? 1 : 0);
3418 $a_xml_writer->xmlEndTag("qtimetadatafield");
3419
3420 $a_xml_writer->xmlStartTag("qtimetadatafield");
3421 $a_xml_writer->xmlElement("fieldlabel", null, "show_marker");
3422 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getParticipantFunctionalitySettings()->getQuestionMarkingEnabled() ? 1 : 0);
3423 $a_xml_writer->xmlEndTag("qtimetadatafield");
3424
3425 $a_xml_writer->xmlStartTag("qtimetadatafield");
3426 $a_xml_writer->xmlElement("fieldlabel", null, "fixed_participants");
3427 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getAccessSettings()->getFixedParticipants() ? 1 : 0);
3428 $a_xml_writer->xmlEndTag("qtimetadatafield");
3429
3430 $a_xml_writer->xmlStartTag("qtimetadatafield");
3431 $a_xml_writer->xmlElement("fieldlabel", null, "show_introduction");
3432 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getIntroductionSettings()->getIntroductionEnabled() ? 1 : 0);
3433 $a_xml_writer->xmlEndTag("qtimetadatafield");
3434
3435 $a_xml_writer->xmlStartTag("qtimetadatafield");
3436 $a_xml_writer->xmlElement("fieldlabel", null, 'exam_conditions');
3437 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getIntroductionSettings()->getExamConditionsCheckboxEnabled() ? 1 : 0);
3438 $a_xml_writer->xmlEndTag("qtimetadatafield");
3439
3440 $a_xml_writer->xmlStartTag("qtimetadatafield");
3441 $a_xml_writer->xmlElement("fieldlabel", null, "show_concluding_remarks");
3442 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getFinishingSettings()->getConcludingRemarksEnabled() ? 1 : 0);
3443 $a_xml_writer->xmlEndTag("qtimetadatafield");
3444
3445 $a_xml_writer->xmlStartTag("qtimetadatafield");
3446 $a_xml_writer->xmlElement("fieldlabel", null, "exportsettings");
3447 $a_xml_writer->xmlElement("fieldentry", null, $this->getExportSettings());
3448 $a_xml_writer->xmlEndTag("qtimetadatafield");
3449
3450 $a_xml_writer->xmlStartTag("qtimetadatafield");
3451 $a_xml_writer->xmlElement("fieldlabel", null, "shuffle_questions");
3452 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getShuffleQuestions() ? 1 : 0);
3453 $a_xml_writer->xmlEndTag("qtimetadatafield");
3454
3455 $a_xml_writer->xmlStartTag("qtimetadatafield");
3456 $a_xml_writer->xmlElement("fieldlabel", null, "processing_time");
3457 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getTestBehaviourSettings()->getProcessingTime());
3458 $a_xml_writer->xmlEndTag("qtimetadatafield");
3459
3460 $a_xml_writer->xmlStartTag("qtimetadatafield");
3461 $a_xml_writer->xmlElement("fieldlabel", null, "enable_examview");
3462 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getFinishingSettings()->getShowAnswerOverview() ? 1 : 0);
3463 $a_xml_writer->xmlEndTag("qtimetadatafield");
3464
3465 $a_xml_writer->xmlStartTag("qtimetadatafield");
3466 $a_xml_writer->xmlElement("fieldlabel", null, "skill_service");
3467 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getAdditionalSettings()->getSkillsServiceEnabled() ? 1 : 0);
3468 $a_xml_writer->xmlEndTag("qtimetadatafield");
3469
3470 if ($this->getInstantFeedbackSolution() == 1) {
3471 $attrs = [
3472 "solutionswitch" => "Yes"
3473 ];
3474 } else {
3475 $attrs = null;
3476 }
3477 $a_xml_writer->xmlElement("assessmentcontrol", $attrs, null);
3478
3479 $a_xml_writer->xmlStartTag("qtimetadatafield");
3480 $a_xml_writer->xmlElement("fieldlabel", null, "show_grading_status");
3481 $a_xml_writer->xmlElement("fieldentry", null, $this->isShowGradingStatusEnabled() ? 1 : 0);
3482 $a_xml_writer->xmlEndTag("qtimetadatafield");
3483
3484 $a_xml_writer->xmlStartTag("qtimetadatafield");
3485 $a_xml_writer->xmlElement("fieldlabel", null, "show_grading_mark");
3486 $a_xml_writer->xmlElement("fieldentry", null, $this->isShowGradingMarkEnabled() ? 1 : 0);
3487 $a_xml_writer->xmlEndTag("qtimetadatafield");
3488
3489 $a_xml_writer->xmlStartTag('qtimetadatafield');
3490 $a_xml_writer->xmlElement('fieldlabel', null, 'hide_info_tab');
3491 $a_xml_writer->xmlElement('fieldentry', null, $this->getMainSettings()->getAdditionalSettings()->getHideInfoTab() ? 1 : 0);
3492 $a_xml_writer->xmlEndTag("qtimetadatafield");
3493
3494 if ($this->getStartingTime() > 0) {
3495 $a_xml_writer->xmlStartTag("qtimetadatafield");
3496 $a_xml_writer->xmlElement("fieldlabel", null, "starting_time");
3497 $a_xml_writer->xmlElement(
3498 "fieldentry",
3499 null,
3500 $this->buildIso8601PeriodForExportCompatibility(
3501 (new DateTimeImmutable())->setTimestamp($this->getStartingTime()),
3502 ),
3503 );
3504 $a_xml_writer->xmlEndTag("qtimetadatafield");
3505 }
3506
3507 if ($this->getEndingTime() > 0) {
3508 $a_xml_writer->xmlStartTag("qtimetadatafield");
3509 $a_xml_writer->xmlElement("fieldlabel", null, "ending_time");
3510 $a_xml_writer->xmlElement(
3511 "fieldentry",
3512 null,
3513 $this->buildIso8601PeriodForExportCompatibility(
3514 (new DateTimeImmutable())->setTimestamp($this->getEndingTime()),
3515 ),
3516 );
3517 $a_xml_writer->xmlEndTag("qtimetadatafield");
3518 }
3519
3520 $a_xml_writer->xmlStartTag("qtimetadatafield");
3521 $a_xml_writer->xmlElement("fieldlabel", null, "autosave");
3522 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getAutosaveEnabled() ? 1 : 0);
3523 $a_xml_writer->xmlEndTag("qtimetadatafield");
3524
3525 $a_xml_writer->xmlStartTag("qtimetadatafield");
3526 $a_xml_writer->xmlElement("fieldlabel", null, "autosave_ival");
3527 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getAutosaveInterval());
3528 $a_xml_writer->xmlEndTag("qtimetadatafield");
3529
3530 $a_xml_writer->xmlStartTag("qtimetadatafield");
3531 $a_xml_writer->xmlElement("fieldlabel", null, "instant_feedback_specific");
3532 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackSpecificEnabled() ? 1 : 0);
3533 $a_xml_writer->xmlEndTag("qtimetadatafield");
3534
3535 $a_xml_writer->xmlStartTag("qtimetadatafield");
3536 $a_xml_writer->xmlElement("fieldlabel", null, "instant_feedback_answer_fixation");
3537 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getLockAnswerOnInstantFeedbackEnabled() ? 1 : 0);
3538 $a_xml_writer->xmlEndTag("qtimetadatafield");
3539
3540 $a_xml_writer->xmlStartTag("qtimetadatafield");
3541 $a_xml_writer->xmlElement("fieldlabel", null, "enable_processing_time");
3542 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getTestBehaviourSettings()->getProcessingTimeEnabled() ? 1 : 0);
3543 $a_xml_writer->xmlEndTag("qtimetadatafield");
3544
3545 foreach ($this->getMarkSchema()->getMarkSteps() as $index => $mark) {
3546 $a_xml_writer->xmlStartTag("qtimetadatafield");
3547 $a_xml_writer->xmlElement("fieldlabel", null, "mark_step_$index");
3548 $a_xml_writer->xmlElement("fieldentry", null, sprintf(
3549 "<short>%s</short><official>%s</official><percentage>%.2f</percentage><passed>%d</passed>",
3550 $mark->getShortName(),
3551 $mark->getOfficialName(),
3552 $mark->getMinimumLevel(),
3553 $mark->getPassed()
3554 ));
3555 $a_xml_writer->xmlEndTag("qtimetadatafield");
3556 }
3557 $a_xml_writer->xmlEndTag("qtimetadata");
3558
3559 $page_id = $main_settings->getIntroductionSettings()->getIntroductionPageId();
3560 $introduction = $page_id !== null
3561 ? (new ilTestPage($page_id))->getXMLContent()
3562 : ilRTE::_replaceMediaObjectImageSrc($this->getIntroduction(), 0);
3563
3564 $a_xml_writer->xmlStartTag("objectives");
3565 $this->addQTIMaterial($a_xml_writer, $page_id, $introduction);
3566 $a_xml_writer->xmlEndTag("objectives");
3567
3568 if ($this->getInstantFeedbackSolution() == 1) {
3569 $attrs = [
3570 "solutionswitch" => "Yes"
3571 ];
3572 } else {
3573 $attrs = null;
3574 }
3575 $a_xml_writer->xmlElement("assessmentcontrol", $attrs, null);
3576
3577 if (strlen($this->getFinalStatement())) {
3578 $page_id = $main_settings->getFinishingSettings()->getConcludingRemarksPageId();
3579 $concluding_remarks = $page_id !== null
3580 ? (new ilTestPage($page_id))->getXMLContent()
3581 : ilRTE::_replaceMediaObjectImageSrc($this->getFinalStatement());
3582 // add qti presentation_material
3583 $a_xml_writer->xmlStartTag("presentation_material");
3584 $a_xml_writer->xmlStartTag("flow_mat");
3585 $this->addQTIMaterial($a_xml_writer, $page_id, $concluding_remarks);
3586 $a_xml_writer->xmlEndTag("flow_mat");
3587 $a_xml_writer->xmlEndTag("presentation_material");
3588 }
3589
3590 $attrs = [
3591 "ident" => "1"
3592 ];
3593 $a_xml_writer->xmlElement("section", $attrs, null);
3594 $a_xml_writer->xmlEndTag("assessment");
3595 $a_xml_writer->xmlEndTag("questestinterop");
3596
3597 $xml = $a_xml_writer->xmlDumpMem(false);
3598 return $xml;
3599 }
3600
3601 protected function buildIso8601PeriodForExportCompatibility(DateTimeImmutable $date_time): string
3602 {
3603 return $date_time->setTimezone(new DateTimeZone('UTC'))->format('\PY\Yn\Mj\D\TG\Hi\Ms\S');
3604 }
3605
3606 protected function buildDateTimeImmutableFromPeriod(?string $period): ?DateTimeImmutable
3607 {
3608 if ($period === null) {
3609 return null;
3610 }
3611 if (preg_match("/P(\d+)Y(\d+)M(\d+)DT(\d+)H(\d+)M(\d+)S/", $period, $matches)) {
3612 return new DateTimeImmutable(
3613 sprintf(
3614 "%02d-%02d-%02d %02d:%02d:%02d",
3615 $matches[1],
3616 $matches[2],
3617 $matches[3],
3618 $matches[4],
3619 $matches[5],
3620 $matches[6]
3621 ),
3622 new \DateTimeZone('UTC')
3623 );
3624 }
3625 return null;
3626 }
3627
3634 public function exportPagesXML(&$a_xml_writer, $a_inst, $a_target_dir, &$expLog): void
3635 {
3636 $this->mob_ids = [];
3637
3638 // PageObjects
3639 $expLog->write(date("[y-m-d H:i:s] ") . "Start Export Page Objects");
3640 $this->bench->start("ContentObjectExport", "exportPageObjects");
3641 $this->exportXMLPageObjects($a_xml_writer, $a_inst, $expLog);
3642 $this->bench->stop("ContentObjectExport", "exportPageObjects");
3643 $expLog->write(date("[y-m-d H:i:s] ") . "Finished Export Page Objects");
3644
3645 // MediaObjects
3646 $expLog->write(date("[y-m-d H:i:s] ") . "Start Export Media Objects");
3647 $this->bench->start("ContentObjectExport", "exportMediaObjects");
3648 $this->exportXMLMediaObjects($a_xml_writer, $a_inst, $a_target_dir, $expLog);
3649 $this->bench->stop("ContentObjectExport", "exportMediaObjects");
3650 $expLog->write(date("[y-m-d H:i:s] ") . "Finished Export Media Objects");
3651
3652 // FileItems
3653 $expLog->write(date("[y-m-d H:i:s] ") . "Start Export File Items");
3654 $this->bench->start("ContentObjectExport", "exportFileItems");
3655 $this->exportFileItems($a_target_dir, $expLog);
3656 $this->bench->stop("ContentObjectExport", "exportFileItems");
3657 $expLog->write(date("[y-m-d H:i:s] ") . "Finished Export File Items");
3658 }
3659
3665 public function modifyExportIdentifier($a_tag, $a_param, $a_value)
3666 {
3667 if ($a_tag == "Identifier" && $a_param == "Entry") {
3668 $a_value = ilUtil::insertInstIntoID($a_value);
3669 }
3670
3671 return $a_value;
3672 }
3673
3674
3681 public function exportXMLPageObjects(&$a_xml_writer, $inst, &$expLog)
3682 {
3683 foreach ($this->questions as $question_id) {
3684 $this->bench->start("ContentObjectExport", "exportPageObject");
3685 $expLog->write(date("[y-m-d H:i:s] ") . "Page Object " . $question_id);
3686
3687 $attrs = [];
3688 $a_xml_writer->xmlStartTag("PageObject", $attrs);
3689
3690
3691 // export xml to writer object
3692 $this->bench->start("ContentObjectExport", "exportPageObject_XML");
3693 $page_object = new ilAssQuestionPage($question_id);
3694 $page_object->buildDom();
3695 $page_object->insertInstIntoIDs((string) $inst);
3696 $mob_ids = $page_object->collectMediaObjects(false);
3697 $file_ids = ilPCFileList::collectFileItems($page_object, $page_object->getDomDoc());
3698 $xml = $page_object->getXMLFromDom(false, false, false, "", true);
3699 $xml = str_replace("&", "&amp;", $xml);
3700 $a_xml_writer->appendXML($xml);
3701 $page_object->freeDom();
3702 unset($page_object);
3703
3704 $this->bench->stop("ContentObjectExport", "exportPageObject_XML");
3705
3706 // collect media objects
3707 $this->bench->start("ContentObjectExport", "exportPageObject_CollectMedia");
3708 //$mob_ids = $page_obj->getMediaObjectIDs();
3709 foreach ($mob_ids as $mob_id) {
3710 $this->mob_ids[$mob_id] = $mob_id;
3711 }
3712 $this->bench->stop("ContentObjectExport", "exportPageObject_CollectMedia");
3713
3714 // collect all file items
3715 $this->bench->start("ContentObjectExport", "exportPageObject_CollectFileItems");
3716 //$file_ids = $page_obj->getFileItemIds();
3717 foreach ($file_ids as $file_id) {
3718 $this->file_ids[$file_id] = $file_id;
3719 }
3720 $this->bench->stop("ContentObjectExport", "exportPageObject_CollectFileItems");
3721
3722 $a_xml_writer->xmlEndTag("PageObject");
3723 //unset($page_obj);
3724
3725 $this->bench->stop("ContentObjectExport", "exportPageObject");
3726 }
3727 }
3728
3732 public function exportXMLMediaObjects(&$a_xml_writer, $a_inst, $a_target_dir, &$expLog)
3733 {
3734 foreach ($this->mob_ids as $mob_id) {
3735 $expLog->write(date("[y-m-d H:i:s] ") . "Media Object " . $mob_id);
3736 if (ilObjMediaObject::_exists((int) $mob_id)) {
3737 $target_dir = $a_target_dir . DIRECTORY_SEPARATOR . 'objects'
3738 . DIRECTORY_SEPARATOR . 'il_' . IL_INST_ID . '_mob_' . $mob_id;
3739 ilFileUtils::createDirectory($target_dir);
3740 $media_obj = new ilObjMediaObject((int) $mob_id);
3741 $media_obj->exportXML($a_xml_writer, (int) $a_inst);
3743 foreach ($media_obj->getMediaItems() as $item) {
3744 $rid = $this->media_object_repository->getById($item->getMobId())['rid'] ?? null;
3745 if (!is_string($rid) || !$this->irss->manage()->find($rid) instanceof ResourceIdentification) {
3746 $expLog->write(date('[y-m-d H:i:s] ') . "The resource for Media Object {$item->getMobId()} does not exist (skipping)");
3747 continue;
3748 }
3749 $stream = $item->getLocationStream();
3750 file_put_contents($target_dir . DIRECTORY_SEPARATOR . $item->getLocation(), $stream);
3751 $stream->close();
3752 }
3753 unset($media_obj);
3754 }
3755 }
3756 }
3757
3762 public function exportFileItems($target_dir, &$expLog)
3763 {
3764 foreach ($this->file_ids as $file_id) {
3765 $expLog->write(date("[y-m-d H:i:s] ") . "File Item " . $file_id);
3766 $file_dir = $target_dir . '/objects/il_' . IL_INST_ID . '_file_' . $file_id;
3767 ilFileUtils::makeDir($file_dir);
3768 $file_obj = new ilObjFile((int) $file_id, false);
3769 $source_file = $file_obj->getFile($file_obj->getVersion());
3770 if (!is_file($source_file)) {
3771 $source_file = $file_obj->getFile();
3772 }
3773 if (is_file($source_file)) {
3774 copy($source_file, $file_dir . '/' . $file_obj->getFileName());
3775 }
3776 unset($file_obj);
3777 }
3778 }
3779
3784 public function getImportMapping(): array
3785 {
3786 return [];
3787 }
3788
3791 public function onMarkSchemaSaved(): void
3792 {
3793 $this->saveCompleteStatus($this->question_set_config_factory->getQuestionSetConfig());
3794
3795 if ($this->participantDataExist()) {
3796 $this->recalculateScores(true);
3797 }
3798 }
3799
3803 public function marksEditable(): bool
3804 {
3805 $total = $this->evalTotalPersons();
3806 $results_summary_settings = $this->getScoreSettings()->getResultSummarySettings();
3807 if ($total === 0
3808 || $results_summary_settings->getScoreReporting()->isReportingEnabled() === false) {
3809 return true;
3810 }
3811
3812 if ($results_summary_settings->getScoreReporting() === ScoreReportingTypes::SCORE_REPORTING_DATE) {
3813 return $results_summary_settings->getReportingDate()
3814 >= new DateTimeImmutable('now', new DateTimeZone('UTC'));
3815 }
3816
3817 return false;
3818 }
3819
3829 public function saveAuthorToMetadata($author = "")
3830 {
3831 $path_to_lifecycle = $this->lo_metadata->paths()->custom()->withNextStep('lifeCycle')->get();
3832 $path_to_authors = $this->lo_metadata->paths()->authors();
3833
3834 $reader = $this->lo_metadata->read($this->getId(), 0, $this->getType(), $path_to_lifecycle);
3835 if (!is_null($reader->allData($path_to_lifecycle)->current())) {
3836 return;
3837 }
3838
3839 if ($author === '') {
3840 $author = $this->user->getFullname();
3841 }
3842 $this->lo_metadata->manipulate($this->getId(), 0, $this->getType())
3843 ->prepareCreateOrUpdate($path_to_authors, $author)
3844 ->execute();
3845 }
3846
3850 protected function doCreateMetaData(): void
3851 {
3852 $this->saveAuthorToMetadata();
3853 }
3854
3862 public function getAuthor(): string
3863 {
3864 $path_to_authors = $this->lo_metadata->paths()->authors();
3865 $author_data = $this->lo_metadata->read($this->getId(), 0, $this->getType(), $path_to_authors)
3866 ->allData($path_to_authors);
3867
3868 return $this->lo_metadata->dataHelper()->makePresentableAsList(', ', ...$author_data);
3869 }
3870
3878 public static function _lookupAuthor($obj_id): string
3879 {
3880 global $DIC;
3881
3882 $lo_metadata = $DIC->learningObjectMetadata();
3883
3884 $path_to_authors = $lo_metadata->paths()->authors();
3885 $author_data = $lo_metadata->read($obj_id, 0, "tst", $path_to_authors)
3886 ->allData($path_to_authors);
3887
3888 return $lo_metadata->dataHelper()->makePresentableAsList(',', ...$author_data);
3889 }
3890
3897 public static function _getAvailableTests($use_object_id = false): array
3898 {
3899 global $DIC;
3900 $ilUser = $DIC['ilUser'];
3901
3902 $result_array = [];
3903 $tests = array_slice(
3904 array_reverse(
3905 ilUtil::_getObjectsByOperations("tst", "write", $ilUser->getId(), PHP_INT_MAX)
3906 ),
3907 0,
3908 10000
3909 );
3910
3911 if (count($tests)) {
3912 $titles = ilObject::_prepareCloneSelection($tests, "tst");
3913 foreach ($tests as $ref_id) {
3914 if ($use_object_id) {
3916 $result_array[$obj_id] = $titles[$ref_id];
3917 } else {
3918 $result_array[$ref_id] = $titles[$ref_id];
3919 }
3920 }
3921 }
3922 return $result_array;
3923 }
3924
3933 public function cloneObject(int $target_id, int $copy_id = 0, bool $omit_tree = false): ?ilObject
3934 {
3935 $this->loadFromDb();
3936
3937 $new_obj = parent::cloneObject($target_id, $copy_id, $omit_tree);
3938 $new_obj->setTmpCopyWizardCopyId($copy_id);
3939 $this->cloneMetaData($new_obj);
3940
3941 $new_obj->saveToDb();
3942 $new_obj->addToNewsOnOnline(false, $new_obj->getObjectProperties()->getPropertyIsOnline()->getIsOnline());
3943
3944 $new_main_settings = $this->getMainSettings()
3945 ->withIntroductionSettings(
3946 $this->getMainSettings()->getIntroductionSettings()->withIntroductionPageId(
3947 $this->cloneIntroduction()
3948 )
3949 )->withFinishingSettings(
3950 $this->getMainSettings()->getFinishingSettings()->withConcludingRemarksPageId(
3951 $this->cloneConcludingRemarks()
3952 )
3953 )->withId(0);
3954
3955 $new_main_settings = $this->getMainSettingsRepository()->store($new_main_settings, $new_obj->getTestId());
3956 $this->getScoreSettingsRepository()->store(
3957 $this->getScoreSettings()->withId($new_main_settings->getId())
3958 );
3959 $this->marks_repository->storeMarkSchema(
3960 $this->getMarkSchema()->withTestId($new_obj->getTestId())
3961 );
3962
3963 $new_obj->setTemplate($this->getTemplate());
3964
3965 // clone certificate
3966 $pathFactory = new ilCertificatePathFactory();
3967 $templateRepository = new ilCertificateTemplateDatabaseRepository($this->db);
3968
3969 $cloneAction = new ilCertificateCloneAction(
3970 $this->db,
3971 $pathFactory,
3972 $templateRepository,
3974 );
3975
3976 $cloneAction->cloneCertificate($this, $new_obj);
3977
3978 $this->question_set_config_factory->getQuestionSetConfig()->cloneQuestionSetRelatedData($new_obj);
3979 $new_obj->saveQuestionsToDb();
3980
3981 $skillLevelThresholdList = new ilTestSkillLevelThresholdList($this->db);
3982 $skillLevelThresholdList->setTestId($this->getTestId());
3983 $skillLevelThresholdList->loadFromDb();
3984 $skillLevelThresholdList->cloneListForTest($new_obj->getTestId());
3985
3986 $obj_settings = new ilLPObjSettings($this->getId());
3987 $obj_settings->cloneSettings($new_obj->getId());
3988
3989 if ($new_obj->getTestLogger()->isLoggingEnabled()) {
3990 $new_obj->getTestLogger()->logTestAdministrationInteraction(
3991 $new_obj->getTestLogger()->getInteractionFactory()->buildTestAdministrationInteraction(
3992 $new_obj->getRefId(),
3993 $this->user->getId(),
3994 TestAdministrationInteractionTypes::NEW_TEST_CREATED,
3995 []
3996 )
3997 );
3998 }
3999
4000 return $new_obj;
4001 }
4002
4003 public function getQuestionCount(): int
4004 {
4005 $num = 0;
4006
4007 if ($this->isRandomTest()) {
4008 $questionSetConfig = new ilTestRandomQuestionSetConfig(
4009 $this->tree,
4010 $this->db,
4011 $this->lng,
4012 $this->logger,
4013 $this->component_repository,
4014 $this,
4015 $this->questionrepository
4016 );
4017
4018 $questionSetConfig->loadFromDb();
4019
4020 if ($questionSetConfig->isQuestionAmountConfigurationModePerPool()) {
4021 $sourcePoolDefinitionList = new ilTestRandomQuestionSetSourcePoolDefinitionList(
4022 $this->db,
4023 $this,
4025 );
4026
4027 $sourcePoolDefinitionList->loadDefinitions();
4028
4029 if (is_int($sourcePoolDefinitionList->getQuestionAmount())) {
4030 $num = $sourcePoolDefinitionList->getQuestionAmount();
4031 }
4032 } elseif (is_int($questionSetConfig->getQuestionAmountPerTest())) {
4033 $num = $questionSetConfig->getQuestionAmountPerTest();
4034 }
4035 } else {
4036 $this->loadQuestions();
4037 $num = count($this->questions);
4038 }
4039
4040 return $num;
4041 }
4042
4044 {
4045 if ($this->isRandomTest()) {
4046 return $this->getQuestionCount();
4047 }
4048 return count($this->questions);
4049 }
4050
4058 public static function _getObjectIDFromTestID($test_id)
4059 {
4060 global $DIC;
4061 $ilDB = $DIC['ilDB'];
4062 $object_id = false;
4063 $result = $ilDB->queryF(
4064 "SELECT obj_fi FROM tst_tests WHERE test_id = %s",
4065 ['integer'],
4066 [$test_id]
4067 );
4068 if ($result->numRows()) {
4069 $row = $ilDB->fetchAssoc($result);
4070 $object_id = $row["obj_fi"];
4071 }
4072 return $object_id;
4073 }
4074
4082 public static function _getObjectIDFromActiveID($active_id)
4083 {
4084 global $DIC;
4085 $ilDB = $DIC['ilDB'];
4086 $object_id = false;
4087 $result = $ilDB->queryF(
4088 "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",
4089 ['integer'],
4090 [$active_id]
4091 );
4092 if ($result->numRows()) {
4093 $row = $ilDB->fetchAssoc($result);
4094 $object_id = $row["obj_fi"];
4095 }
4096 return $object_id;
4097 }
4098
4106 public static function _getTestIDFromObjectID($object_id)
4107 {
4108 global $DIC;
4109 $ilDB = $DIC['ilDB'];
4110 $test_id = false;
4111 $result = $ilDB->queryF(
4112 "SELECT test_id FROM tst_tests WHERE obj_fi = %s",
4113 ['integer'],
4114 [$object_id]
4115 );
4116 if ($result->numRows()) {
4117 $row = $ilDB->fetchAssoc($result);
4118 $test_id = $row["test_id"];
4119 }
4120 return $test_id;
4121 }
4122
4131 public function getTextAnswer($active_id, $question_id, $pass = null): string
4132 {
4133 if (($active_id) && ($question_id)) {
4134 if ($pass === null) {
4135 $pass = assQuestion::_getSolutionMaxPass($question_id, $active_id);
4136 }
4137 if ($pass === null) {
4138 return '';
4139 }
4140 $query = $this->db->queryF(
4141 "SELECT value1 FROM tst_solutions WHERE active_fi = %s AND question_fi = %s AND pass = %s",
4142 ['integer', 'integer', 'integer'],
4143 [$active_id, $question_id, $pass]
4144 );
4145 $result = $this->db->fetchAll($query);
4146 if (count($result) == 1) {
4147 return $result[0]["value1"];
4148 }
4149 }
4150 return '';
4151 }
4152
4160 public function getQuestiontext($question_id): string
4161 {
4162 $res = "";
4163 if ($question_id) {
4164 $result = $this->db->queryF(
4165 "SELECT question_text FROM qpl_questions WHERE question_id = %s",
4166 ['integer'],
4167 [$question_id]
4168 );
4169 if ($result->numRows() == 1) {
4170 $row = $this->db->fetchAssoc($result);
4171 $res = $row["question_text"];
4172 }
4173 }
4174 return $res;
4175 }
4176
4178 {
4179 $participant_list = new ilTestParticipantList($this, $this->user, $this->lng, $this->db);
4180 $participant_list->initializeFromDbRows($this->getTestParticipants());
4181
4182 return $participant_list;
4183 }
4184
4191 public function &getInvitedUsers(int $user_id = 0, $order = "login, lastname, firstname"): array
4192 {
4193 $result_array = [];
4194
4195 if ($this->getAnonymity()) {
4196 if ($user_id !== 0) {
4197 $result = $this->db->queryF(
4198 "SELECT tst_active.active_id, tst_active.tries, usr_id, %s login, %s lastname, %s firstname, " .
4199 "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 " .
4200 "LEFT JOIN tst_active ON tst_active.user_fi = tst_invited_user.user_fi AND tst_active.test_fi = tst_invited_user.test_fi " .
4201 "WHERE tst_invited_user.test_fi = %s and tst_invited_user.user_fi=usr_data.usr_id AND usr_data.usr_id=%s " .
4202 "ORDER BY $order",
4203 ['text', 'text', 'text', 'integer', 'integer'],
4204 ['', $this->lng->txt('anonymous'), '', $this->getTestId(), $user_id]
4205 );
4206 } else {
4207 $result = $this->db->queryF(
4208 "SELECT tst_active.active_id, tst_active.tries, usr_id, %s login, %s lastname, %s firstname, " .
4209 "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 " .
4210 "LEFT JOIN tst_active ON tst_active.user_fi = tst_invited_user.user_fi AND tst_active.test_fi = tst_invited_user.test_fi " .
4211 "WHERE tst_invited_user.test_fi = %s and tst_invited_user.user_fi=usr_data.usr_id " .
4212 "ORDER BY $order",
4213 ['text', 'text', 'text', 'integer'],
4214 ['', $this->lng->txt('anonymous'), '', $this->getTestId()]
4215 );
4216 }
4217 } else {
4218 if ($user_id !== 0) {
4219 $result = $this->db->queryF(
4220 "SELECT tst_active.active_id, tst_active.tries, usr_id, login, lastname, firstname, " .
4221 "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 " .
4222 "LEFT JOIN tst_active ON tst_active.user_fi = tst_invited_user.user_fi AND tst_active.test_fi = tst_invited_user.test_fi " .
4223 "WHERE tst_invited_user.test_fi = %s and tst_invited_user.user_fi=usr_data.usr_id AND usr_data.usr_id=%s " .
4224 "ORDER BY $order",
4225 ['integer', 'integer'],
4226 [$this->getTestId(), $user_id]
4227 );
4228 } else {
4229 $result = $this->db->queryF(
4230 "SELECT tst_active.active_id, tst_active.tries, usr_id, login, lastname, firstname, " .
4231 "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 " .
4232 "LEFT JOIN tst_active ON tst_active.user_fi = tst_invited_user.user_fi AND tst_active.test_fi = tst_invited_user.test_fi " .
4233 "WHERE tst_invited_user.test_fi = %s and tst_invited_user.user_fi=usr_data.usr_id " .
4234 "ORDER BY $order",
4235 ['integer'],
4236 [$this->getTestId()]
4237 );
4238 }
4239 }
4240 $result_array = [];
4241 while ($row = $this->db->fetchAssoc($result)) {
4242 $result_array[$row['usr_id']] = $row;
4243 }
4244 return $result_array;
4245 }
4246
4247 public function getTestParticipants(): array
4248 {
4249 if ($this->getMainSettings()->getGeneralSettings()->getAnonymity()) {
4250 $query = "
4251 SELECT tst_active.active_id,
4252 tst_active.tries,
4253 tst_active.user_fi usr_id,
4254 %s login,
4255 %s lastname,
4256 %s firstname,
4257 tst_active.submitted test_finished,
4258 usr_data.matriculation,
4259 usr_data.active,
4260 tst_active.lastindex,
4261 COALESCE(tst_active.last_finished_pass, -1) <> tst_active.last_started_pass unfinished_passes
4262 FROM tst_active
4263 LEFT JOIN usr_data
4264 ON tst_active.user_fi = usr_data.usr_id
4265 WHERE tst_active.test_fi = %s
4266 ORDER BY usr_data.lastname
4267 ";
4268 $result = $this->db->queryF(
4269 $query,
4270 ['text', 'text', 'text', 'integer'],
4271 ['', $this->lng->txt("anonymous"), "", $this->getTestId()]
4272 );
4273 } else {
4274 $query = "
4275 SELECT tst_active.active_id,
4276 tst_active.tries,
4277 tst_active.user_fi usr_id,
4278 usr_data.login,
4279 usr_data.lastname,
4280 usr_data.firstname,
4281 tst_active.submitted test_finished,
4282 usr_data.matriculation,
4283 usr_data.active,
4284 tst_active.lastindex,
4285 COALESCE(tst_active.last_finished_pass, -1) <> tst_active.last_started_pass unfinished_passes
4286 FROM tst_active
4287 LEFT JOIN usr_data
4288 ON tst_active.user_fi = usr_data.usr_id
4289 WHERE tst_active.test_fi = %s
4290 ORDER BY usr_data.lastname
4291 ";
4292 $result = $this->db->queryF(
4293 $query,
4294 ['integer'],
4295 [$this->getTestId()]
4296 );
4297 }
4298 $data = [];
4299 while ($row = $this->db->fetchAssoc($result)) {
4300 $data[$row['active_id']] = $row;
4301 }
4302 foreach ($data as $index => $participant) {
4303 if (strlen(trim($participant["firstname"] . $participant["lastname"])) == 0) {
4304 $data[$index]["lastname"] = $this->lng->txt("deleted_user");
4305 }
4306 }
4307 return $data;
4308 }
4309
4310 public function getTestParticipantsForManualScoring($filter = null): array
4311 {
4312 if (!$this->getGlobalSettings()->isManualScoringEnabled()) {
4313 return [];
4314 }
4315
4316 $filtered_participants = [];
4317 foreach ($this->getTestParticipants() as $active_id => $participant) {
4318 if ($participant['tries'] > 0) {
4319 switch ($filter) {
4320 case 4:
4321 if ($this->test_man_scoring_done_helper->isDone((int) $active_id)) {
4322 $filtered_participants[$active_id] = $participant;
4323 }
4324 break;
4325 case 5:
4326 if (!$this->test_man_scoring_done_helper->isDone((int) $active_id)) {
4327 $filtered_participants[$active_id] = $participant;
4328 }
4329 break;
4330 default:
4331 $filtered_participants[$active_id] = $participant;
4332 }
4333 }
4334 }
4335 return $filtered_participants;
4336 }
4337
4344 public function getUserData($ids): array
4345 {
4346 if (!is_array($ids) || count($ids) == 0) {
4347 return [];
4348 }
4349
4350 if ($this->getAnonymity()) {
4351 $result = $this->db->queryF(
4352 "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",
4353 ['text', 'text', 'text'],
4354 ["", $this->lng->txt("anonymous"), ""]
4355 );
4356 } else {
4357 $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");
4358 }
4359
4360 $result_array = [];
4361 while ($row = $this->db->fetchAssoc($result)) {
4362 $result_array[$row["usr_id"]] = $row;
4363 }
4364 return $result_array;
4365 }
4366
4367 public function getGroupData($ids): array
4368 {
4369 if (!is_array($ids) || count($ids) == 0) {
4370 return [];
4371 }
4372 $result = [];
4373 foreach ($ids as $ref_id) {
4375 $result[$ref_id] = ["ref_id" => $ref_id, "title" => ilObject::_lookupTitle($obj_id), "description" => ilObject::_lookupDescription($obj_id)];
4376 }
4377 return $result;
4378 }
4379
4380 public function getRoleData($ids): array
4381 {
4382 if (!is_array($ids) || count($ids) == 0) {
4383 return [];
4384 }
4385 $result = [];
4386 foreach ($ids as $obj_id) {
4387 $result[$obj_id] = ["obj_id" => $obj_id, "title" => ilObject::_lookupTitle($obj_id), "description" => ilObject::_lookupDescription($obj_id)];
4388 }
4389 return $result;
4390 }
4391
4398 public function inviteUser($user_id, $client_ip = "")
4399 {
4400 $this->db->manipulateF(
4401 "DELETE FROM tst_invited_user WHERE test_fi = %s AND user_fi = %s",
4402 ['integer', 'integer'],
4403 [$this->getTestId(), $user_id]
4404 );
4405 $this->db->manipulateF(
4406 "INSERT INTO tst_invited_user (test_fi, user_fi, ip_range_from, ip_range_to, tstamp) VALUES (%s, %s, %s, %s, %s)",
4407 ['integer', 'integer', 'text', 'text', 'integer'],
4408 [$this->getTestId(), $user_id, (strlen($client_ip)) ? $client_ip : null, (strlen($client_ip)) ? $client_ip : null,time()]
4409 );
4410 }
4411
4417 public static function _getSolvedQuestions($active_id, $question_fi = null): array
4418 {
4419 global $DIC;
4420 $ilDB = $DIC['ilDB'];
4421 if (is_numeric($question_fi)) {
4422 $result = $ilDB->queryF(
4423 "SELECT question_fi, solved FROM tst_qst_solved WHERE active_fi = %s AND question_fi=%s",
4424 ['integer', 'integer'],
4425 [$active_id, $question_fi]
4426 );
4427 } else {
4428 $result = $ilDB->queryF(
4429 "SELECT question_fi, solved FROM tst_qst_solved WHERE active_fi = %s",
4430 ['integer'],
4431 [$active_id]
4432 );
4433 }
4434 $result_array = [];
4435 while ($row = $ilDB->fetchAssoc($result)) {
4436 $result_array[$row["question_fi"]] = $row;
4437 }
4438 return $result_array;
4439 }
4440
4441
4445 public function setQuestionSetSolved($value, $question_id, $user_id)
4446 {
4447 $active_id = $this->getActiveIdOfUser($user_id);
4448 $this->db->manipulateF(
4449 "DELETE FROM tst_qst_solved WHERE active_fi = %s AND question_fi = %s",
4450 ['integer', 'integer'],
4451 [$active_id, $question_id]
4452 );
4453 $this->db->manipulateF(
4454 "INSERT INTO tst_qst_solved (solved, question_fi, active_fi) VALUES (%s, %s, %s)",
4455 ['integer', 'integer', 'integer'],
4456 [$value, $question_id, $active_id]
4457 );
4458 }
4459
4463 public function isTestFinished($active_id): bool
4464 {
4465 $result = $this->db->queryF(
4466 "SELECT submitted FROM tst_active WHERE active_id=%s AND submitted=%s",
4467 ['integer', 'integer'],
4468 [$active_id, 1]
4469 );
4470 return $result->numRows() == 1;
4471 }
4472
4476 public function isActiveTestSubmitted($user_id = null): bool
4477 {
4478 if (!is_numeric($user_id)) {
4479 $user_id = $this->user->getId();
4480 }
4481
4482 $result = $this->db->queryF(
4483 "SELECT submitted FROM tst_active WHERE test_fi=%s AND user_fi=%s AND submitted=%s",
4484 ['integer', 'integer', 'integer'],
4485 [$this->getTestId(), $user_id, 1]
4486 );
4487 return $result->numRows() == 1;
4488 }
4489
4493 public function hasNrOfTriesRestriction(): bool
4494 {
4495 return $this->getNrOfTries() != 0;
4496 }
4497
4498
4504 public function isNrOfTriesReached($tries): bool
4505 {
4506 return $tries >= $this->getNrOfTries();
4507 }
4508
4509
4517 public function getAllTestResults($participants): array
4518 {
4519 $results = [];
4520 $row = [
4521 "user_id" => $this->lng->txt("user_id"),
4522 "matriculation" => $this->lng->txt("matriculation"),
4523 "lastname" => $this->lng->txt("lastname"),
4524 "firstname" => $this->lng->txt("firstname"),
4525 "login" => $this->lng->txt("login"),
4526 "reached_points" => $this->lng->txt("tst_reached_points"),
4527 "max_points" => $this->lng->txt("tst_maximum_points"),
4528 "percent_value" => $this->lng->txt("tst_percent_solved"),
4529 "mark" => $this->lng->txt("tst_mark"),
4530 "passed" => $this->lng->txt("tst_mark_passed"),
4531 ];
4532 $results[] = $row;
4533 if (count($participants)) {
4534 foreach ($participants as $active_id => $user_rec) {
4535 $mark = '';
4536 $row = [];
4537 $reached_points = 0;
4538 $max_points = 0;
4539 $pass = ilObjTest::_getResultPass($active_id);
4540 // abort if no valid pass can be found
4541 if (!is_int($pass)) {
4542 continue;
4543 }
4544 foreach ($this->questions as $value) {
4545 $question = ilObjTest::_instanciateQuestion($value);
4546 if (is_object($question)) {
4547 $max_points += $question->getMaximumPoints();
4548 $reached_points += $question->getReachedPoints($active_id, $pass);
4549 }
4550 }
4551 if ($max_points > 0) {
4552 $percentvalue = $reached_points / $max_points;
4553 if ($percentvalue < 0) {
4554 $percentvalue = 0.0;
4555 }
4556 } else {
4557 $percentvalue = 0;
4558 }
4559 $mark_obj = $this->getMarkSchema()->getMatchingMark($percentvalue * 100);
4560 $passed = "";
4561 if ($mark_obj !== null) {
4562 $mark = $mark_obj->getOfficialName();
4563 }
4564 if ($this->getAnonymity()) {
4565 $user_rec['firstname'] = "";
4566 $user_rec['lastname'] = $this->lng->txt("anonymous");
4567 }
4568 $results[] = [
4569 "user_id" => $user_rec['usr_id'],
4570 "matriculation" => $user_rec['matriculation'],
4571 "lastname" => $user_rec['lastname'],
4572 "firstname" => $user_rec['firstname'],
4573 "login" => $user_rec['login'],
4574 "reached_points" => $reached_points,
4575 "max_points" => $max_points,
4576 "percent_value" => $percentvalue,
4577 "mark" => $mark,
4578 "passed" => $user_rec['passed'] ? '1' : '0',
4579 ];
4580 }
4581 }
4582 return $results;
4583 }
4584
4593 public static function _getPass($active_id): int
4594 {
4595 global $DIC;
4596 $ilDB = $DIC['ilDB'];
4597 $result = $ilDB->queryF(
4598 "SELECT tries FROM tst_active WHERE active_id = %s",
4599 ['integer'],
4600 [$active_id]
4601 );
4602 if ($result->numRows()) {
4603 $row = $ilDB->fetchAssoc($result);
4604 return $row["tries"];
4605 } else {
4606 return 0;
4607 }
4608 }
4609
4619 public static function _getMaxPass($active_id): ?int
4620 {
4621 global $DIC;
4622 $ilDB = $DIC['ilDB'];
4623 $result = $ilDB->queryF(
4624 "SELECT MAX(pass) maxpass FROM tst_pass_result WHERE active_fi = %s",
4625 ['integer'],
4626 [$active_id]
4627 );
4628
4629 if ($result->numRows()) {
4630 $row = $ilDB->fetchAssoc($result);
4631 return $row["maxpass"];
4632 }
4633
4634 return null;
4635 }
4636
4642 public static function _getBestPass($active_id): ?int
4643 {
4644 global $DIC;
4645 $ilDB = $DIC['ilDB'];
4646
4647 $result = $ilDB->queryF(
4648 "SELECT * FROM tst_pass_result WHERE active_fi = %s",
4649 ['integer'],
4650 [$active_id]
4651 );
4652
4653 if (!$result->numRows()) {
4654 return null;
4655 }
4656
4657 $bestrow = null;
4658 $bestfactor = 0.0;
4659 while ($row = $ilDB->fetchAssoc($result)) {
4660 if ($row["maxpoints"] > 0.0) {
4661 $factor = (float) ($row["points"] / $row["maxpoints"]);
4662 } else {
4663 $factor = 0.0;
4664 }
4665 if ($factor === 0.0 && $bestfactor === 0.0
4666 || $factor > $bestfactor) {
4667 $bestrow = $row;
4668 $bestfactor = $factor;
4669 }
4670 }
4671
4672 if (is_array($bestrow)) {
4673 return $bestrow["pass"];
4674 }
4675
4676 return null;
4677 }
4678
4687 public static function _getResultPass($active_id): ?int
4688 {
4689 $counted_pass = null;
4690 if (ilObjTest::_getPassScoring($active_id) == self::SCORE_BEST_PASS) {
4691 $counted_pass = ilObjTest::_getBestPass($active_id);
4692 } else {
4693 $counted_pass = ilObjTest::_getMaxPass($active_id);
4694 }
4695 return $counted_pass;
4696 }
4697
4707 public function getAnsweredQuestionCount($active_id, $pass = null): int
4708 {
4709 if ($this->isRandomTest()) {
4710 $this->loadQuestions($active_id, $pass);
4711 }
4712 $workedthrough = 0;
4713 foreach ($this->questions as $value) {
4714 if ($this->questionrepository->lookupResultRecordExist($active_id, $value, $pass)) {
4715 $workedthrough += 1;
4716 }
4717 }
4718 return $workedthrough;
4719 }
4720
4727 public static function lookupPassResultsUpdateTimestamp($active_id, $pass): int
4728 {
4729 global $DIC;
4730 $ilDB = $DIC['ilDB'];
4731
4732 if (is_null($pass)) {
4733 $pass = 0;
4734 }
4735
4736 $query = "
4737 SELECT tst_pass_result.tstamp pass_res_tstamp,
4738 tst_test_result.tstamp quest_res_tstamp
4739
4740 FROM tst_pass_result
4741
4742 LEFT JOIN tst_test_result
4743 ON tst_test_result.active_fi = tst_pass_result.active_fi
4744 AND tst_test_result.pass = tst_pass_result.pass
4745
4746 WHERE tst_pass_result.active_fi = %s
4747 AND tst_pass_result.pass = %s
4748
4749 ORDER BY tst_test_result.tstamp DESC
4750 ";
4751
4752 $result = $ilDB->queryF(
4753 $query,
4754 ['integer', 'integer'],
4755 [$active_id, $pass]
4756 );
4757
4758 while ($row = $ilDB->fetchAssoc($result)) {
4759 if ($row['quest_res_tstamp']) {
4760 return $row['quest_res_tstamp'];
4761 }
4762
4763 return $row['pass_res_tstamp'];
4764 }
4765
4766 return 0;
4767 }
4768
4777 public function isExecutable($test_session, $user_id, $allow_pass_increase = false): array
4778 {
4779 $result = [
4780 "executable" => true,
4781 "errormessage" => ""
4782 ];
4783
4784 if (!$this->getObjectProperties()->getPropertyIsOnline()->getIsOnline()) {
4785 $result["executable"] = false;
4786 $result["errormessage"] = $this->lng->txt('autosave_failed') . ': ' . $this->lng->txt('offline');
4787 return $result;
4788 }
4789
4790 if (!$this->startingTimeReached()) {
4791 $result["executable"] = false;
4792 $result["errormessage"] = sprintf($this->lng->txt("detail_starting_time_not_reached"), ilDatePresentation::formatDate(new ilDateTime($this->getStartingTime(), IL_CAL_UNIX)));
4793 return $result;
4794 }
4795 if ($this->endingTimeReached()) {
4796 $result["executable"] = false;
4797 $result["errormessage"] = sprintf($this->lng->txt("detail_ending_time_reached"), ilDatePresentation::formatDate(new ilDateTime($this->getEndingTime(), IL_CAL_UNIX)));
4798 return $result;
4799 }
4800
4801 $active_id = $this->getActiveIdOfUser($user_id);
4802
4803 if ($this->getEnableProcessingTime()
4804 && $active_id > 0
4805 && ($starting_time = $this->getStartingTimeOfUser($active_id)) !== false
4806 && $this->isMaxProcessingTimeReached($starting_time, $active_id)) {
4807 $result["executable"] = false;
4808 $result["errormessage"] = $this->lng->txt("detail_max_processing_time_reached");
4809 return $result;
4810 }
4811
4812 $testPassesSelector = new ilTestPassesSelector($this->db, $this);
4813 $testPassesSelector->setActiveId($active_id);
4814 $testPassesSelector->setLastFinishedPass($test_session->getLastFinishedPass());
4815
4816 if ($this->hasNrOfTriesRestriction() && ($active_id > 0)) {
4817 $closedPasses = $testPassesSelector->getClosedPasses();
4818
4819 if (count($closedPasses) >= $this->getNrOfTries()) {
4820 $result["executable"] = false;
4821 $result["errormessage"] = $this->lng->txt("maximum_nr_of_tries_reached");
4822 return $result;
4823 }
4824
4825 if ($this->isBlockPassesAfterPassedEnabled() && !$testPassesSelector->openPassExists()) {
4826 if ($this->test_result_repository->isPassed($user_id, $this->getId())) {
4827 $result['executable'] = false;
4828 $result['errormessage'] = $this->lng->txt("tst_addit_passes_blocked_after_passed_msg");
4829 return $result;
4830 }
4831 }
4832 }
4833
4834 $next_pass_allowed_timestamp = 0;
4835 if (!$this->isNextPassAllowed($testPassesSelector, $next_pass_allowed_timestamp)) {
4836 $date = ilDatePresentation::formatDate(new ilDateTime($next_pass_allowed_timestamp, IL_CAL_UNIX));
4837
4838 $result['executable'] = false;
4839 $result['errormessage'] = sprintf($this->lng->txt('wait_for_next_pass_hint_msg'), $date);
4840 return $result;
4841 }
4842 return $result;
4843 }
4844
4845 public function isNextPassAllowed(ilTestPassesSelector $testPassesSelector, int &$next_pass_allowed_timestamp): bool
4846 {
4847 $waiting_between_passes = $this->getMainSettings()->getTestBehaviourSettings()->getPassWaiting();
4848 $last_finished_pass_timestamp = $testPassesSelector->getLastFinishedPassTimestamp();
4849
4850 if (
4851 $this->getMainSettings()->getTestBehaviourSettings()->getPassWaitingEnabled()
4852 && ($waiting_between_passes !== '')
4853 && ($testPassesSelector->getLastFinishedPass() !== null)
4854 && ($last_finished_pass_timestamp !== null)
4855 ) {
4856 $time_values = explode(':', $waiting_between_passes);
4857 $next_pass_allowed_timestamp = strtotime('+ ' . $time_values[0] . ' Days + ' . $time_values[1] . ' Hours' . $time_values[2] . ' Minutes', $last_finished_pass_timestamp);
4858 return (time() > $next_pass_allowed_timestamp);
4859 }
4860
4861 return true;
4862 }
4863
4864 public function canShowTestResults(ilTestSession $test_session): bool
4865 {
4866 $pass_selector = new ilTestPassesSelector($this->db, $this);
4867
4868 $pass_selector->setActiveId($test_session->getActiveId());
4869 $pass_selector->setLastFinishedPass($test_session->getLastFinishedPass());
4870
4871 return $pass_selector->hasReportablePasses();
4872 }
4873
4874 public function hasAnyTestResult(ilTestSession $test_session): bool
4875 {
4876 $pass_selector = new ilTestPassesSelector($this->db, $this);
4877
4878 $pass_selector->setActiveId($test_session->getActiveId());
4879 $pass_selector->setLastFinishedPass($test_session->getLastFinishedPass());
4880
4881 return $pass_selector->hasExistingPasses();
4882 }
4883
4891 public function getStartingTimeOfUser($active_id, $pass = null)
4892 {
4893 if ($active_id < 1) {
4894 return false;
4895 }
4896 if ($pass === null) {
4897 $pass = ($this->getResetProcessingTime()) ? self::_getPass($active_id) : 0;
4898 }
4899 $result = $this->db->queryF(
4900 "SELECT tst_times.started FROM tst_times WHERE tst_times.active_fi = %s AND tst_times.pass = %s ORDER BY tst_times.started",
4901 ['integer', 'integer'],
4902 [$active_id, $pass]
4903 );
4904 if ($result->numRows()) {
4905 $row = $this->db->fetchAssoc($result);
4906 if (preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["started"], $matches)) {
4907 return mktime(
4908 (int) $matches[4],
4909 (int) $matches[5],
4910 (int) $matches[6],
4911 (int) $matches[2],
4912 (int) $matches[3],
4913 (int) $matches[1]
4914 );
4915 } else {
4916 return time();
4917 }
4918 } else {
4919 return time();
4920 }
4921 }
4922
4929 public function isMaxProcessingTimeReached(int $starting_time, int $active_id): bool
4930 {
4931 if (!$this->getEnableProcessingTime()) {
4932 return false;
4933 }
4934
4935 $processing_time = $this->getProcessingTimeInSeconds($active_id);
4936 $now = time();
4937 if ($now > ($starting_time + $processing_time)) {
4938 return true;
4939 }
4940
4941 return false;
4942 }
4943
4944 public function getTestQuestions(): array
4945 {
4946 $tags_trafo = $this->refinery->string()->stripTags();
4947
4948 $query = "
4949 SELECT questions.*,
4950 questtypes.type_tag,
4951 tstquest.sequence,
4952 origquest.obj_fi orig_obj_fi
4953
4954 FROM qpl_questions questions
4955
4956 INNER JOIN qpl_qst_type questtypes
4957 ON questtypes.question_type_id = questions.question_type_fi
4958
4959 INNER JOIN tst_test_question tstquest
4960 ON tstquest.question_fi = questions.question_id
4961
4962 LEFT JOIN qpl_questions origquest
4963 ON origquest.question_id = questions.original_id
4964
4965 WHERE tstquest.test_fi = %s
4966
4967 ORDER BY tstquest.sequence
4968 ";
4969
4970 $query_result = $this->db->queryF(
4971 $query,
4972 ['integer'],
4973 [$this->getTestId()]
4974 );
4975
4976 $questions = [];
4977
4978 while ($row = $this->db->fetchAssoc($query_result)) {
4979 $row['title'] = $tags_trafo->transform($row['title']);
4980 $row['description'] = $tags_trafo->transform($row['description'] !== '' && $row['description'] !== null ? $row['description'] : '&nbsp;');
4981 $row['author'] = $tags_trafo->transform($row['author']);
4982
4983 $questions[] = $row;
4984 }
4985
4986 return $questions;
4987 }
4988
4989 public function isTestQuestion(int $question_id): bool
4990 {
4991 foreach ($this->getTestQuestions() as $questionData) {
4992 if ($questionData['question_id'] != $question_id) {
4993 continue;
4994 }
4995
4996 return true;
4997 }
4998
4999 return false;
5000 }
5001
5002 public function checkQuestionParent(int $question_id): bool
5003 {
5004 $row = $this->db->fetchAssoc($this->db->queryF(
5005 "SELECT COUNT(question_id) cnt FROM qpl_questions WHERE question_id = %s AND obj_fi = %s",
5006 ['integer', 'integer'],
5007 [$question_id, $this->getId()]
5008 ));
5009
5010 return (bool) $row['cnt'];
5011 }
5012
5013 public function getFixedQuestionSetTotalPoints(): float
5014 {
5015 $points = 0;
5016
5017 foreach ($this->getTestQuestions() as $question_data) {
5018 $points += $question_data['points'];
5019 }
5020
5021 return $points;
5022 }
5023
5027 public function getPotentialRandomTestQuestions(): array
5028 {
5029 $query = "
5030 SELECT questions.*,
5031 questtypes.type_tag,
5032 origquest.obj_fi orig_obj_fi
5033
5034 FROM qpl_questions questions
5035
5036 INNER JOIN qpl_qst_type questtypes
5037 ON questtypes.question_type_id = questions.question_type_fi
5038
5039 INNER JOIN tst_rnd_cpy tstquest
5040 ON tstquest.qst_fi = questions.question_id
5041
5042 LEFT JOIN qpl_questions origquest
5043 ON origquest.question_id = questions.original_id
5044
5045 WHERE tstquest.tst_fi = %s
5046 ";
5047
5048 $query_result = $this->db->queryF(
5049 $query,
5050 ['integer'],
5051 [$this->getTestId()]
5052 );
5053
5054 return $this->db->fetchAll($query_result);
5055 }
5056
5057 public function getShuffleQuestions(): bool
5058 {
5059 return $this->getMainSettings()->getQuestionBehaviourSettings()->getShuffleQuestions();
5060 }
5061
5074 {
5075 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getUsrPassOverviewMode();
5076 }
5077
5078 public function getListOfQuestions(): bool
5079 {
5080 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getQuestionListEnabled();
5081 }
5082
5083 public function getUsrPassOverviewEnabled(): bool
5084 {
5085 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getUsrPassOverviewEnabled();
5086 }
5087
5088 public function getListOfQuestionsStart(): bool
5089 {
5090 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getShownQuestionListAtBeginning();
5091 }
5092
5093 public function getListOfQuestionsEnd(): bool
5094 {
5095 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getShownQuestionListAtEnd();
5096 }
5097
5098 public function getListOfQuestionsDescription(): bool
5099 {
5100 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getShowDescriptionInQuestionList();
5101 }
5102
5106 public function getShowPassDetails(): bool
5107 {
5108 return $this->getScoreSettings()->getResultDetailsSettings()->getShowPassDetails();
5109 }
5110
5114 public function getShowSolutionPrintview(): bool
5115 {
5116 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionPrintview();
5117 }
5121 public function canShowSolutionPrintview($user_id = null): bool
5122 {
5123 return $this->getShowSolutionPrintview();
5124 }
5125
5129 public function getShowSolutionFeedback(): bool
5130 {
5131 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionFeedback();
5132 }
5133
5137 public function getShowSolutionAnswersOnly(): bool
5138 {
5139 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionAnswersOnly();
5140 }
5141
5145 public function getShowSolutionSignature(): bool
5146 {
5147 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionSignature();
5148 }
5149
5153 public function getShowSolutionSuggested(): bool
5154 {
5155 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionSuggested();
5156 }
5157
5162 public function getShowSolutionListComparison(): bool
5163 {
5164 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionListComparison();
5165 }
5166
5167 public function getShowSolutionListOwnAnswers(): bool
5168 {
5169 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionListOwnAnswers();
5170 }
5171
5175 public static function _getUserIdFromActiveId(int $active_id): int
5176 {
5177 global $DIC;
5178 $ilDB = $DIC['ilDB'];
5179 $result = $ilDB->queryF(
5180 "SELECT user_fi FROM tst_active WHERE active_id = %s",
5181 ['integer'],
5182 [$active_id]
5183 );
5184 if ($result->numRows()) {
5185 $row = $ilDB->fetchAssoc($result);
5186 return $row["user_fi"];
5187 } else {
5188 return -1;
5189 }
5190 }
5191
5192 public function _getLastAccess(int $active_id): string
5193 {
5194 $result = $this->db->queryF(
5195 "SELECT finished FROM tst_times WHERE active_fi = %s ORDER BY finished DESC",
5196 ['integer'],
5197 [$active_id]
5198 );
5199 if ($result->numRows()) {
5200 $row = $this->db->fetchAssoc($result);
5201 return $row["finished"];
5202 }
5203 return "";
5204 }
5205
5206 public static function lookupLastTestPassAccess(int $active_id, int $pass_index): ?int
5207 {
5209 global $DIC;
5210 $ilDB = $DIC['ilDB'];
5211
5212 $query = "
5213 SELECT MAX(tst_times.tstamp) as last_pass_access
5214 FROM tst_times
5215 WHERE active_fi = %s
5216 AND pass = %s
5217 ";
5218
5219 $res = $ilDB->queryF(
5220 $query,
5221 ['integer', 'integer'],
5222 [$active_id, $pass_index]
5223 );
5224
5225 while ($row = $ilDB->fetchAssoc($res)) {
5226 return $row['last_pass_access'];
5227 }
5228
5229 return null;
5230 }
5231
5239 public function isHTML($a_text): bool
5240 {
5241 if (preg_match("/<[^>]*?>/", $a_text)) {
5242 return true;
5243 } else {
5244 return false;
5245 }
5246 }
5247
5254 public function qtiMaterialToArray($a_material): array
5255 {
5256 $result = '';
5257 $mobs = [];
5258 for ($i = 0; $i < $a_material->getMaterialCount(); $i++) {
5259 $material = $a_material->getMaterial($i);
5260 if ($material['type'] === 'mattext') {
5261 $result .= $material['material']->getContent();
5262 }
5263 if ($material['type'] === 'matimage') {
5264 $matimage = $material['material'];
5265 if (preg_match('/(il_([0-9]+)_mob_([0-9]+))/', $matimage->getLabel(), $matches)) {
5266 $mobs[] = [
5267 'mob' => $matimage->getLabel(),
5268 'uri' => $matimage->getUri()
5269 ];
5270 }
5271 }
5272 }
5273
5274 $decoded_result = base64_decode($result);
5275 if (str_starts_with($decoded_result, '<PageObject>')) {
5276 $result = $decoded_result;
5277 }
5278
5279 return [
5280 'text' => $result,
5281 'mobs' => $mobs
5282 ];
5283 }
5284
5285 public function addQTIMaterial(ilXmlWriter &$xml_writer, ?int $page_id, string $material = ''): void
5286 {
5287 $xml_writer->xmlStartTag('material');
5288 $attrs = [
5289 'texttype' => 'text/plain'
5290 ];
5291 $file_ids = [];
5292 $mobs = [];
5293 if ($page_id !== null) {
5294 $attrs['texttype'] = 'text/xml';
5295 $mobs = ilObjMediaObject::_getMobsOfObject('tst:pg', $page_id);
5296 $page_object = new ilTestPage($page_id);
5297 $page_object->buildDom();
5298 $page_object->insertInstIntoIDs((string) IL_INST_ID);
5299 $material = base64_encode($page_object->getXMLFromDom());
5300 $file_ids = ilPCFileList::collectFileItems($page_object, $page_object->getDomDoc());
5301 foreach ($file_ids as $file_id) {
5302 $this->file_ids[] = (int) $file_id;
5303 };
5304 $mob_string = 'il_' . IL_INST_ID . '_mob_';
5305 } elseif ($this->isHTML($material)) {
5306 $attrs['texttype'] = 'text/xhtml';
5307 $mobs = ilObjMediaObject::_getMobsOfObject('tst:html', $this->getId());
5308 $mob_string = 'mm_';
5309 }
5310
5311 $xml_writer->xmlElement('mattext', $attrs, $material);
5312 foreach ($mobs as $mob) {
5313 $mob_id_string = (string) $mob;
5314 $moblabel = 'il_' . IL_INST_ID . '_mob_' . $mob_id_string;
5315 if (strpos($material, $mob_string . $mob_id_string) !== false) {
5316 if (ilObjMediaObject::_exists($mob)) {
5317 $mob_obj = new ilObjMediaObject($mob);
5318 $imgattrs = [
5319 'label' => $moblabel,
5320 'uri' => 'objects/' . 'il_' . IL_INST_ID . '_mob_' . $mob_id_string . '/' . $mob_obj->getTitle()
5321 ];
5322 }
5323 $xml_writer->xmlElement('matimage', $imgattrs, null);
5324 }
5325 }
5326 $xml_writer->xmlEndTag('material');
5327 }
5328
5335 public function prepareTextareaOutput($txt_output, $prepare_for_latex_output = false, $omitNl2BrWhenTextArea = false)
5336 {
5337 if ($txt_output == null) {
5338 $txt_output = '';
5339 }
5341 $txt_output,
5342 $prepare_for_latex_output,
5343 $omitNl2BrWhenTextArea
5344 );
5345 }
5346
5347 public function getAnonymity(): bool
5348 {
5349 return $this->getMainSettings()->getGeneralSettings()->getAnonymity();
5350 }
5351
5352 public function getShowCancel(): bool
5353 {
5354 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getSuspendTestAllowed();
5355 }
5356
5357 public function getShowMarker(): bool
5358 {
5359 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getQuestionMarkingEnabled();
5360 }
5361
5362 public function getFixedParticipants(): bool
5363 {
5364 return $this->getMainSettings()->getAccessSettings()->getFixedParticipants();
5365 }
5366
5367 public function lookupQuestionSetTypeByActiveId(int $active_id): string
5368 {
5369 return $this->main_settings_repository->getFor(
5370 self::_getTestIDFromObjectID(self::_getObjectIDFromActiveID($active_id)),
5371 )->getGeneralSettings()->getQuestionSetType();
5372 }
5373
5384 public function userLookupFullName($user_id, $overwrite_anonymity = false, $sorted_order = false, $suffix = ""): string
5385 {
5386 if ($this->getAnonymity() && !$overwrite_anonymity) {
5387 return $this->lng->txt("anonymous") . $suffix;
5388 } else {
5390 if (strlen($uname["firstname"] . $uname["lastname"]) == 0) {
5391 $uname["firstname"] = $this->lng->txt("deleted_user");
5392 }
5393 if ($sorted_order) {
5394 return trim($uname["lastname"] . ", " . $uname["firstname"]) . $suffix;
5395 } else {
5396 return trim($uname["firstname"] . " " . $uname["lastname"]) . $suffix;
5397 }
5398 }
5399 }
5400
5402 DateTimeImmutable|int|null $date_time
5403 ): ?DateTimeImmutable {
5404 if ($date_time === null || $date_time instanceof DateTimeImmutable) {
5405 return $date_time;
5406 }
5407
5408 return DateTimeImmutable::createFromFormat('U', (string) $date_time);
5409 }
5410
5418 public function processPrintoutput2FO($print_output): string
5419 {
5420 if (extension_loaded("tidy")) {
5421 $config = [
5422 "indent" => false,
5423 "output-xml" => true,
5424 "numeric-entities" => true
5425 ];
5426 $tidy = new tidy();
5427 $tidy->parseString($print_output, $config, 'utf8');
5428 $tidy->cleanRepair();
5429 $print_output = tidy_get_output($tidy);
5430 $print_output = preg_replace("/^.*?(<html)/", "\\1", $print_output);
5431 } else {
5432 $print_output = str_replace("&nbsp;", "&#160;", $print_output);
5433 $print_output = str_replace("&otimes;", "X", $print_output);
5434 }
5435 $xsl = file_get_contents("./components/ILIAS/Test/xml/question2fo.xsl");
5436
5437 // additional font support
5438
5439 $xsl = str_replace(
5440 'font-family="Helvetica, unifont"',
5441 'font-family="' . $this->settings->get('rpc_pdf_font', 'Helvetica, unifont') . '"',
5442 $xsl
5443 );
5444
5445 $args = [ '/_xml' => $print_output, '/_xsl' => $xsl ];
5446 $xh = xslt_create();
5447 $params = [];
5448 $output = xslt_process($xh, "arg:/_xml", "arg:/_xsl", null, $args, $params);
5449 xslt_error($xh);
5450 xslt_free($xh);
5451 return $output;
5452 }
5453
5460 public function deliverPDFfromHTML($content, $title = null)
5461 {
5462 $content = preg_replace("/href=\".*?\"/", "", $content);
5463 $printbody = new ilTemplate("tpl.il_as_tst_print_body.html", true, true, "components/ILIAS/Test");
5464 $printbody->setVariable("TITLE", ilLegacyFormElementsUtil::prepareFormOutput($this->getTitle()));
5465 $printbody->setVariable("ADM_CONTENT", $content);
5466 $printbody->setCurrentBlock("css_file");
5467 $printbody->setVariable("CSS_FILE", ilUtil::getStyleSheetLocation("filesystem", "delos.css"));
5468 $printbody->parseCurrentBlock();
5469 $printoutput = $printbody->get();
5470 $html = str_replace("href=\"./", "href=\"" . ILIAS_HTTP_PATH . "/", $printoutput);
5471 $html = preg_replace("/<div id=\"dontprint\">.*?<\\/div>/ims", "", $html);
5472 if (extension_loaded("tidy")) {
5473 $config = [
5474 "indent" => false,
5475 "output-xml" => true,
5476 "numeric-entities" => true
5477 ];
5478 $tidy = new tidy();
5479 $tidy->parseString($html, $config, 'utf8');
5480 $tidy->cleanRepair();
5481 $html = tidy_get_output($tidy);
5482 $html = preg_replace("/^.*?(<html)/", "\\1", $html);
5483 } else {
5484 $html = str_replace("&nbsp;", "&#160;", $html);
5485 $html = str_replace("&otimes;", "X", $html);
5486 }
5487 $html = preg_replace("/src=\".\\//ims", "src=\"" . ILIAS_HTTP_PATH . "/", $html);
5488 $this->deliverPDFfromFO($this->processPrintoutput2FO($html), $title);
5489 }
5490
5496 public function deliverPDFfromFO($fo, $title = null): bool
5497 {
5498 $fo_file = ilFileUtils::ilTempnam() . ".fo";
5499 $fp = fopen($fo_file, "w");
5500 fwrite($fp, $fo);
5501 fclose($fp);
5502
5503 try {
5504 $pdf_base64 = ilRpcClientFactory::factory('RPCTransformationHandler')->ilFO2PDF($fo);
5505 $filename = (strlen($title)) ? $title : $this->getTitle();
5508 $pdf_base64->scalar,
5510 "application/pdf"
5511 );
5512 return true;
5513 } catch (Exception $e) {
5514 $this->logger->info(__METHOD__ . ': ' . $e->getMessage());
5515 return false;
5516 }
5517 }
5518
5528 public static function getManualFeedback(int $active_id, int $question_id, ?int $pass): string
5529 {
5530 if ($pass === null) {
5531 return '';
5532 }
5533 $feedback = '';
5534 $row = self::getSingleManualFeedback((int) $active_id, (int) $question_id, (int) $pass);
5535
5536 if ($row !== [] && ($row['finalized_evaluation'] || \ilTestService::isManScoringDone((int) $active_id))) {
5537 $feedback = $row['feedback'] ?? '';
5538 }
5539
5540 return $feedback;
5541 }
5542
5543 public static function getSingleManualFeedback(int $active_id, int $question_id, int $pass): array
5544 {
5545 global $DIC;
5546 $ilDB = $DIC['ilDB'];
5547 $row = [];
5548 $result = $ilDB->queryF(
5549 "SELECT * FROM tst_manual_fb WHERE active_fi = %s AND question_fi = %s AND pass = %s",
5550 ['integer', 'integer', 'integer'],
5551 [$active_id, $question_id, $pass]
5552 );
5553
5554 if ($ilDB->numRows($result) === 1) {
5555 $row = $ilDB->fetchAssoc($result);
5556 $row['feedback'] = ilRTE::_replaceMediaObjectImageSrc($row['feedback'] ?? '', 1);
5557 } elseif ($ilDB->numRows($result) > 1) {
5558 $DIC->logger()->root()->warning(
5559 "WARNING: Multiple feedback entries on tst_manual_fb for " .
5560 "active_fi = $active_id , question_fi = $question_id and pass = $pass"
5561 );
5562 }
5563
5564 return $row;
5565 }
5566
5574 public function getCompleteManualFeedback(int $question_id): array
5575 {
5576 global $DIC;
5577 $ilDB = $DIC['ilDB'];
5578
5579 $feedback = [];
5580 $result = $ilDB->queryF(
5581 "SELECT * FROM tst_manual_fb WHERE question_fi = %s",
5582 ['integer'],
5583 [$question_id]
5584 );
5585
5586 while ($row = $ilDB->fetchAssoc($result)) {
5587 $active = $row['active_fi'];
5588 $pass = $row['pass'];
5589 $question = $row['question_fi'];
5590
5591 $row['feedback'] = ilRTE::_replaceMediaObjectImageSrc($row['feedback'] ?? '', 1);
5592
5593 $feedback[$active][$pass][$question] = $row;
5594 }
5595
5596 return $feedback;
5597 }
5598
5599 public function saveManualFeedback(
5600 int $active_id,
5601 int $question_id,
5602 int $pass,
5603 ?string $feedback,
5604 bool $finalized = false
5605 ): void {
5606 $feedback_old = self::getSingleManualFeedback($active_id, $question_id, $pass);
5607 $this->db->manipulateF(
5608 'DELETE FROM tst_manual_fb WHERE active_fi = %s AND question_fi = %s AND pass = %s',
5609 ['integer', 'integer', 'integer'],
5610 [$active_id, $question_id, $pass]
5611 );
5612
5613 $this->insertManualFeedback($active_id, $question_id, $pass, $feedback, $finalized, $feedback_old);
5614
5615 }
5616
5617 private function insertManualFeedback(
5618 int $active_id,
5619 int $question_id,
5620 int $pass,
5621 ?string $feedback,
5622 bool $finalized,
5623 array $feedback_old
5624 ): void {
5625 $next_id = $this->db->nextId('tst_manual_fb');
5626 $user = $this->user->getId();
5627 $finalized_time = time();
5628
5629 $update_default = [
5630 'manual_feedback_id' => [ 'integer', $next_id],
5631 'active_fi' => [ 'integer', $active_id],
5632 'question_fi' => [ 'integer', $question_id],
5633 'pass' => [ 'integer', $pass],
5634 'feedback' => [ 'clob', $feedback ? ilRTE::_replaceMediaObjectImageSrc($feedback, 0) : null],
5635 'tstamp' => [ 'integer', time()]
5636 ];
5637
5638 if ($feedback_old !== [] && (int) $feedback_old['finalized_evaluation'] === 1) {
5639 $user = $feedback_old['finalized_by_usr_id'];
5640 $finalized_time = $feedback_old['finalized_tstamp'];
5641 }
5642
5643 if ($finalized === false) {
5644 $update_default['finalized_evaluation'] = ['integer', 0];
5645 $update_default['finalized_by_usr_id'] = ['integer', 0];
5646 $update_default['finalized_tstamp'] = ['integer', 0];
5647 } elseif ($finalized === true) {
5648 $update_default['finalized_evaluation'] = ['integer', 1];
5649 $update_default['finalized_by_usr_id'] = ['integer', $user];
5650 $update_default['finalized_tstamp'] = ['integer', $finalized_time];
5651 }
5652
5653 $this->db->insert('tst_manual_fb', $update_default);
5654
5655 if ($this->logger->isLoggingEnabled()) {
5656 $this->logger->logScoringInteraction(
5657 $this->logger->getInteractionFactory()->buildScoringInteraction(
5658 $this->getRefId(),
5659 $question_id,
5660 $this->user->getId(),
5661 self::_getUserIdFromActiveId($active_id),
5662 TestScoringInteractionTypes::QUESTION_GRADED,
5663 [
5664 AdditionalInformationGenerator::KEY_EVAL_FINALIZED => $this->logger
5665 ->getAdditionalInformationGenerator()->getTrueFalseTagForBool($finalized),
5666 AdditionalInformationGenerator::KEY_FEEDBACK => $feedback ? ilRTE::_replaceMediaObjectImageSrc($feedback, 0) : ''
5667 ]
5668 )
5669 );
5670 }
5671 }
5672
5680 public function getJavaScriptOutput(): bool
5681 {
5682 return true;
5683 }
5684
5685 public function &createTestSequence($active_id, $pass, $shuffle)
5686 {
5687 $this->test_sequence = new ilTestSequence($active_id, $pass, $this->isRandomTest(), $this->questionrepository);
5688 }
5689
5695 public function setTestId($a_id)
5696 {
5697 $this->test_id = $a_id;
5698 }
5699
5707 public function getDetailedTestResults($participants): array
5708 {
5709 $results = [];
5710 if (count($participants)) {
5711 foreach ($participants as $active_id => $user_rec) {
5712 $row = [];
5713 $reached_points = 0;
5714 $max_points = 0;
5715 $pass = ilObjTest::_getResultPass($active_id);
5716 foreach ($this->questions as $value) {
5717 $question = ilObjTest::_instanciateQuestion($value);
5718 if (is_object($question)) {
5719 $max_points += $question->getMaximumPoints();
5720 $reached_points += $question->getReachedPoints($active_id, $pass);
5721 if ($max_points > 0) {
5722 $percentvalue = $reached_points / $max_points;
5723 if ($percentvalue < 0) {
5724 $percentvalue = 0.0;
5725 }
5726 } else {
5727 $percentvalue = 0;
5728 }
5729 if ($this->getAnonymity()) {
5730 $user_rec['firstname'] = "";
5731 $user_rec['lastname'] = $this->lng->txt("anonymous");
5732 }
5733 $results[] = [
5734 "user_id" => $user_rec['usr_id'],
5735 "matriculation" => $user_rec['matriculation'],
5736 "lastname" => $user_rec['lastname'],
5737 "firstname" => $user_rec['firstname'],
5738 "login" => $user_rec['login'],
5739 "question_id" => $question->getId(),
5740 "question_title" => $question->getTitle(),
5741 "reached_points" => $reached_points,
5742 "max_points" => $max_points,
5743 "passed" => $user_rec['passed'] ? '1' : '0',
5744 ];
5745 }
5746 }
5747 }
5748 }
5749 return $results;
5750 }
5751
5755 public static function _lookupTestObjIdForQuestionId(int $q_id): ?int
5756 {
5757 global $DIC;
5758 $ilDB = $DIC['ilDB'];
5759
5760 $result = $ilDB->queryF(
5761 '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',
5762 ['integer'],
5763 [$q_id]
5764 );
5765 $rec = $ilDB->fetchAssoc($result);
5766 return $rec['obj_id'] ?? null;
5767 }
5768
5775 public function isPluginActive($a_pname): bool
5776 {
5777 if (!$this->component_repository->getComponentByTypeAndName(
5779 'TestQuestionPool'
5780 )->getPluginSlotById('qst')->hasPluginName($a_pname)) {
5781 return false;
5782 }
5783
5784 return $this->component_repository
5785 ->getComponentByTypeAndName(
5787 'TestQuestionPool'
5788 )
5789 ->getPluginSlotById(
5790 'qst'
5791 )
5792 ->getPluginByName(
5793 $a_pname
5794 )->isActive();
5795 }
5796
5800 public function getParticipantsForTestAndQuestion($test_id, $question_id): array
5801 {
5802 $query = "
5803 SELECT tst_test_result.active_fi, tst_test_result.question_fi, tst_test_result.pass
5804 FROM tst_test_result
5805 INNER JOIN tst_active ON tst_active.active_id = tst_test_result.active_fi AND tst_active.test_fi = %s
5806 INNER JOIN qpl_questions ON qpl_questions.question_id = tst_test_result.question_fi
5807 LEFT JOIN usr_data ON usr_data.usr_id = tst_active.user_fi
5808 WHERE tst_test_result.question_fi = %s
5809 ORDER BY usr_data.lastname ASC, usr_data.firstname ASC
5810 ";
5811
5812 $result = $this->db->queryF(
5813 $query,
5814 ['integer', 'integer'],
5815 [$test_id, $question_id]
5816 );
5817 $foundusers = [];
5819 while ($row = $this->db->fetchAssoc($result)) {
5820 if ($this->getAccessFilteredParticipantList() && !$this->getAccessFilteredParticipantList()->isActiveIdInList($row["active_fi"])) {
5821 continue;
5822 }
5823
5824 if (!array_key_exists($row["active_fi"], $foundusers)) {
5825 $foundusers[$row["active_fi"]] = [];
5826 }
5827 array_push($foundusers[$row["active_fi"]], ["pass" => $row["pass"], "qid" => $row["question_fi"]]);
5828 }
5829 return $foundusers;
5830 }
5831
5832 public function getAggregatedResultsData(): array
5833 {
5834 $data = $this->getCompleteEvaluationData();
5835 $found_participants = $data->getParticipants();
5836 $results = ['overview' => [], 'questions' => []];
5837 if ($found_participants !== []) {
5838 $results['overview']['tst_stat_result_mark_median'] = $data->getStatistics()->getEvaluationDataOfMedianUser()?->getMark()?->getShortName() ?? '';
5839 $results['overview']['tst_stat_result_rank_median'] = $data->getStatistics()->rankMedian();
5840 $results['overview']['tst_stat_result_total_participants'] = $data->getStatistics()->count();
5841 $results['overview']['tst_stat_result_median'] = $data->getStatistics()->median();
5842 $results['overview']['tst_eval_total_persons'] = count($found_participants);
5843 $total_finished = $data->getTotalFinishedParticipants();
5844 $results['overview']['tst_eval_total_finished'] = $total_finished;
5845 $results['overview']['tst_eval_total_finished_average_time'] =
5846 $this->secondsToHoursMinutesSecondsString(
5847 $this->evalTotalStartedAverageTime($data->getParticipantIds())
5848 );
5849 $total_passed = 0;
5850 $total_passed_reached = 0;
5851 $total_passed_max = 0;
5852 $total_passed_time = 0;
5853 foreach ($found_participants as $userdata) {
5854 if ($userdata->getMark()?->getPassed()) {
5855 $total_passed++;
5856 $total_passed_reached += $userdata->getReached();
5857 $total_passed_max += $userdata->getMaxpoints();
5858 $total_passed_time += $userdata->getTimeOnTask();
5859 }
5860 }
5861 $average_passed_reached = $total_passed ? $total_passed_reached / $total_passed : 0;
5862 $average_passed_max = $total_passed ? $total_passed_max / $total_passed : 0;
5863 $average_passed_time = $total_passed ? $total_passed_time / $total_passed : 0;
5864 $results['overview']['tst_eval_total_passed'] = $total_passed;
5865 $results['overview']['tst_eval_total_passed_average_points'] = sprintf('%2.2f', $average_passed_reached)
5866 . ' ' . strtolower('of') . ' ' . sprintf('%2.2f', $average_passed_max);
5867 $results['overview']['tst_eval_total_passed_average_time'] =
5868 $this->secondsToHoursMinutesSecondsString((int) $average_passed_time);
5869 }
5870
5871 foreach ($data->getQuestionTitles() as $question_id => $question_title) {
5872 $answered = 0;
5873 $reached = 0;
5874 $max = 0;
5875 foreach ($found_participants as $userdata) {
5876 for ($i = 0; $i <= $userdata->getLastPass(); $i++) {
5877 if (is_object($userdata->getPass($i))) {
5878 $question = $userdata->getPass($i)->getAnsweredQuestionByQuestionId($question_id);
5879 if (is_array($question)) {
5880 $answered++;
5881 $reached += $question['reached'];
5882 $max += $question['points'];
5883 }
5884 }
5885 }
5886 }
5887 $percent = $max ? $reached / $max * 100.0 : 0;
5888 $results['questions'][$question_id] = [
5889 $question_title,
5890 sprintf('%.2f', $answered ? $reached / $answered : 0) . ' ' . strtolower($this->lng->txt('of')) . ' ' . sprintf('%.2f', $answered ? $max / $answered : 0),
5891 sprintf('%.2f', $percent) . '%',
5892 $answered,
5893 sprintf('%.2f', $answered ? $reached / $answered : 0),
5894 sprintf('%.2f', $answered ? $max / $answered : 0),
5895 $percent / 100.0
5896 ];
5897 }
5898 return $results;
5899 }
5900
5901 protected function secondsToHoursMinutesSecondsString(int $seconds): string
5902 {
5903 $diff_hours = floor($seconds / 3600);
5904 $seconds -= $diff_hours * 3600;
5905 $diff_minutes = floor($seconds / 60);
5906 $seconds -= $diff_minutes * 60;
5907 return sprintf('%02d:%02d:%02d', $diff_hours, $diff_minutes, $seconds);
5908 }
5909
5913 public function getXMLZip(): string
5914 {
5915 return $this->export_factory->getExporter($this, 'xml')
5916 ->write();
5917 }
5918
5919 public function getExportSettings(): int
5920 {
5921 return $this->getScoreSettings()->getResultDetailsSettings()->getExportSettings();
5922 }
5923
5924 public function setTemplate(int $template_id)
5925 {
5926 $this->template_id = $template_id;
5927 }
5928
5929 public function getTemplate(): int
5930 {
5931 return $this->template_id;
5932 }
5933
5935 {
5936 $question_set_config = $this->question_set_config_factory->getQuestionSetConfig();
5937 $reindexed_sequence_position_map = $question_set_config->reindexQuestionOrdering();
5938
5939 $this->loadQuestions();
5940
5941 return $reindexed_sequence_position_map;
5942 }
5943
5944 public function setQuestionOrder(array $order)
5945 {
5946 asort($order);
5947
5948 $i = 0;
5949
5950 foreach (array_keys($order) as $id) {
5951 $i++;
5952
5953 $query = "
5954 UPDATE tst_test_question
5955 SET sequence = %s
5956 WHERE question_fi = %s
5957 ";
5958
5959 $this->db->manipulateF(
5960 $query,
5961 ['integer', 'integer'],
5962 [$i, $id]
5963 );
5964 }
5965
5966 if ($this->logger->isLoggingEnabled()) {
5967 $this->logger->logTestAdministrationInteraction(
5968 $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
5969 $this->getRefId(),
5970 $this->user->getId(),
5971 TestAdministrationInteractionTypes::QUESTION_MOVED,
5972 [
5973 AdditionalInformationGenerator::KEY_QUESTION_ORDER => $order
5974 ]
5975 )
5976 );
5977 }
5978
5979 $this->loadQuestions();
5980 }
5981
5982 public function hasQuestionsWithoutQuestionpool(): bool
5983 {
5984 $questions = $this->getQuestionTitlesAndIndexes();
5985
5986 $IN_questions = $this->db->in('q1.question_id', array_keys($questions), false, 'integer');
5987
5988 $query = "
5989 SELECT count(q1.question_id) cnt
5990
5991 FROM qpl_questions q1
5992
5993 INNER JOIN qpl_questions q2
5994 ON q2.question_id = q1.original_id
5995
5996 WHERE $IN_questions
5997 AND q1.obj_fi = q2.obj_fi
5998 ";
5999 $rset = $this->db->query($query);
6000 $row = $this->db->fetchAssoc($rset);
6001
6002 return $row['cnt'] > 0;
6003 }
6004
6011 public static function _lookupFinishedUserTests($a_user_id): array
6012 {
6013 global $DIC;
6014 $ilDB = $DIC['ilDB'];
6015
6016 $result = $ilDB->queryF(
6017 "SELECT test_fi,MAX(pass) AS pass FROM tst_active" .
6018 " JOIN tst_pass_result ON (tst_pass_result.active_fi = tst_active.active_id)" .
6019 " WHERE user_fi=%s" .
6020 " GROUP BY test_fi",
6021 ['integer', 'integer'],
6022 [$a_user_id, 1]
6023 );
6024 $all = [];
6025 while ($row = $ilDB->fetchAssoc($result)) {
6026 $obj_id = self::_getObjectIDFromTestID($row["test_fi"]);
6027 $all[$obj_id] = (bool) $row["pass"];
6028 }
6029 return $all;
6030 }
6031
6032 public function getQuestions(): array
6033 {
6034 return $this->questions;
6035 }
6036
6037 public function isOnline(): bool
6038 {
6039 return $this->online;
6040 }
6041
6042 public function getIntroductionPageId(): int
6043 {
6044 $page_id = $this->getMainSettings()->getIntroductionSettings()->getIntroductionPageId();
6045 if ($page_id !== null) {
6046 return $page_id;
6047 }
6048
6049 $page_object = new ilTestPage();
6050 $page_object->setParentId($this->getId());
6051 $new_page_id = $page_object->createPageWithNextId();
6052 $settings = $this->getMainSettings()->getIntroductionSettings()
6053 ->withIntroductionPageId($new_page_id);
6054 $this->getMainSettingsRepository()->store(
6055 $this->getMainSettings()->withIntroductionSettings($settings)
6056 );
6057 return $new_page_id;
6058 }
6059
6061 {
6062 $page_id = $this->getMainSettings()->getFinishingSettings()->getConcludingRemarksPageId();
6063 if ($page_id !== null) {
6064 return $page_id;
6065 }
6066
6067 $page_object = new ilTestPage();
6068 $page_object->setParentId($this->getId());
6069 $new_page_id = $page_object->createPageWithNextId();
6070 $settings = $this->getMainSettings()->getFinishingSettings()
6071 ->withConcludingRemarksPageId($new_page_id);
6072 $this->getMainSettingsRepository()->store(
6073 $this->getMainSettings()->withFinishingSettings($settings)
6074 );
6075 return $new_page_id;
6076 }
6077
6078 public function getHighscoreEnabled(): bool
6079 {
6080 return $this->getScoreSettings()->getGamificationSettings()->getHighscoreEnabled();
6081 }
6082
6092 public function getHighscoreAnon(): bool
6093 {
6094 return $this->getScoreSettings()->getGamificationSettings()->getHighscoreAnon();
6095 }
6096
6105 public function isHighscoreAnon(): bool
6106 {
6107 return $this->getAnonymity() == 1 || $this->getHighscoreAnon();
6108 }
6109
6113 public function getHighscoreAchievedTS(): bool
6114 {
6115 return $this->getScoreSettings()->getGamificationSettings()->getHighscoreAchievedTS();
6116 }
6117
6121 public function getHighscoreScore(): bool
6122 {
6123 return $this->getScoreSettings()->getGamificationSettings()->getHighscoreScore();
6124 }
6125
6129 public function getHighscorePercentage(): bool
6130 {
6131 return $this->getScoreSettings()->getGamificationSettings()->getHighscorePercentage();
6132 }
6133
6137 public function getHighscoreWTime(): bool
6138 {
6139 return $this->getScoreSettings()->getGamificationSettings()->getHighscoreWTime();
6140 }
6141
6145 public function getHighscoreOwnTable(): bool
6146 {
6147 return $this->getScoreSettings()->getGamificationSettings()->getHighscoreOwnTable();
6148 }
6149
6153 public function getHighscoreTopTable(): bool
6154 {
6155 return $this->getScoreSettings()->getGamificationSettings()->getHighscoreTopTable();
6156 }
6157
6162 public function getHighscoreTopNum(int $a_retval = 10): int
6163 {
6164 return $this->getScoreSettings()->getGamificationSettings()->getHighscoreTopNum();
6165 }
6166
6167 public function getHighscoreMode(): int
6168 {
6169 return $this->getScoreSettings()->getGamificationSettings()->getHighScoreMode();
6170 }
6171
6172 public function getSpecificAnswerFeedback(): bool
6173 {
6174 return $this->getMainSettings()->getQuestionBehaviourSettings()->getInstantFeedbackSpecificEnabled();
6175 }
6176
6177 public function getAutosave(): bool
6178 {
6179 return $this->getMainSettings()->getQuestionBehaviourSettings()->getAutosaveEnabled();
6180 }
6181
6182 public function isPassDeletionAllowed(): bool
6183 {
6184 return $this->getScoreSettings()->getResultSummarySettings()->getPassDeletionAllowed();
6185 }
6186
6187 public function getEnableExamview(): bool
6188 {
6189 return $this->getMainSettings()->getFinishingSettings()->getShowAnswerOverview();
6190 }
6191
6198 public function getStartingTimeOfParticipants(): array
6199 {
6200 $times = [];
6201 $result = $this->db->queryF(
6202 "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",
6203 ['integer'],
6204 [$this->getTestId()]
6205 );
6206 while ($row = $this->db->fetchAssoc($result)) {
6207 $times[$row['active_fi']] = $row['started'];
6208 }
6209 return $times;
6210 }
6211
6212 public function getTimeExtensionsOfParticipants(): array
6213 {
6214 $times = [];
6215 $result = $this->db->queryF(
6216 "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",
6217 ['integer'],
6218 [$this->getTestId()]
6219 );
6220 while ($row = $this->db->fetchAssoc($result)) {
6221 $times[$row['active_fi']] = $row['additionaltime'];
6222 }
6223 return $times;
6224 }
6225
6226 private function getExtraTime(int $active_id): int
6227 {
6228 if ($active_id === 0) {
6229 return 0;
6230 }
6231 return $this->participant_repository
6232 ->getParticipantByActiveId($this->getTestId(), $active_id)
6233 ?->getExtraTime() ?? 0;
6234 }
6235
6236 public function getMaxPassOfTest(): int
6237 {
6238 $query = '
6239 SELECT MAX(tst_pass_result.pass) + 1 max_res
6240 FROM tst_pass_result
6241 INNER JOIN tst_active ON tst_active.active_id = tst_pass_result.active_fi
6242 WHERE test_fi = ' . $this->db->quote($this->getTestId(), 'integer') . '
6243 ';
6244 $res = $this->db->query($query);
6245 $data = $this->db->fetchAssoc($res);
6246 return (int) $data['max_res'];
6247 }
6248
6249 public static function lookupExamId($active_id, $pass)
6250 {
6251 global $DIC;
6252 $ilDB = $DIC['ilDB'];
6253
6254 $exam_id_query = 'SELECT exam_id FROM tst_pass_result WHERE active_fi = %s AND pass = %s';
6255 $exam_id_result = $ilDB->queryF($exam_id_query, [ 'integer', 'integer' ], [ $active_id, $pass ]);
6256 if ($ilDB->numRows($exam_id_result) == 1) {
6257 $exam_id_row = $ilDB->fetchAssoc($exam_id_result);
6258
6259 if ($exam_id_row['exam_id'] != null) {
6260 return $exam_id_row['exam_id'];
6261 }
6262 }
6263
6264 return null;
6265 }
6266
6267 public static function buildExamId($active_id, $pass, $test_obj_id = null): string
6268 {
6269 global $DIC;
6270 $ilSetting = $DIC['ilSetting'];
6271
6272 $inst_id = $ilSetting->get('inst_id', null);
6273
6274 if ($test_obj_id === null) {
6275 $obj_id = self::_getObjectIDFromActiveID($active_id);
6276 } else {
6277 $obj_id = $test_obj_id;
6278 }
6279
6280 $examId = 'I' . $inst_id . '_T' . $obj_id . '_A' . $active_id . '_P' . $pass;
6281
6282 return $examId;
6283 }
6284
6285 public function isShowExamIdInTestPassEnabled(): bool
6286 {
6287 return $this->getMainSettings()->getTestBehaviourSettings()->getExamIdInTestAttemptEnabled();
6288 }
6289
6290 public function isShowExamIdInTestResultsEnabled(): bool
6291 {
6292 return $this->getScoreSettings()->getResultDetailsSettings()->getShowExamIdInTestResults();
6293 }
6294
6295
6296 public function setQuestionSetType(string $question_set_type)
6297 {
6298 $this->main_settings = $this->getMainSettings()->withGeneralSettings(
6299 $this->getMainSettings()->getGeneralSettings()
6300 ->withQuestionSetType($question_set_type)
6301 );
6302 }
6303
6304 public function getQuestionSetType(): string
6305 {
6306 return $this->getMainSettings()->getGeneralSettings()->getQuestionSetType();
6307 }
6308
6314 public function isFixedTest(): bool
6315 {
6316 return $this->getQuestionSetType() == self::QUESTION_SET_TYPE_FIXED;
6317 }
6318
6324 public function isRandomTest(): bool
6325 {
6326 return $this->getQuestionSetType() == self::QUESTION_SET_TYPE_RANDOM;
6327 }
6328
6329 public function getQuestionSetTypeTranslation(ilLanguage $lng, $questionSetType): string
6330 {
6331 switch ($questionSetType) {
6333 return $lng->txt('tst_question_set_type_fixed');
6334
6336 return $lng->txt('tst_question_set_type_random');
6337 }
6338
6339 throw new ilTestException('invalid question set type value given: ' . $questionSetType);
6340 }
6341
6342 public function participantDataExist(): bool
6343 {
6344 if ($this->participantDataExist === null) {
6345 $this->participantDataExist = (bool) $this->evalTotalPersons();
6346 }
6347
6348 return $this->participantDataExist;
6349 }
6350
6351 public function recalculateScores($preserve_manscoring = false)
6352 {
6353 $scoring = new TestScoring(
6354 $this,
6355 $this->user,
6356 $this->db,
6357 $this->test_result_repository
6358 );
6359 $scoring->setPreserveManualScores($preserve_manscoring);
6360 $scoring->recalculateSolutions();
6361 }
6362
6363 public static function getTestObjIdsWithActiveForUserId($userId): array
6364 {
6365 global $DIC;
6366 $ilDB = $DIC['ilDB'];
6367
6368 $query = "
6369 SELECT obj_fi
6370 FROM tst_active
6371 INNER JOIN tst_tests
6372 ON test_id = test_fi
6373 WHERE user_fi = %s
6374 ";
6375
6376 $res = $ilDB->queryF($query, ['integer'], [$userId]);
6377
6378 $objIds = [];
6379
6380 while ($row = $ilDB->fetchAssoc($res)) {
6381 $objIds[] = (int) $row['obj_fi'];
6382 }
6383
6384 return $objIds;
6385 }
6386
6387 public function isSkillServiceEnabled(): bool
6388 {
6389 return $this->getMainSettings()->getAdditionalSettings()->getSkillsServiceEnabled();
6390 }
6391
6403 public function isSkillServiceToBeConsidered(): bool
6404 {
6405 if (!$this->getMainSettings()->getAdditionalSettings()->getSkillsServiceEnabled()) {
6406 return false;
6407 }
6408
6409 if (!self::isSkillManagementGloballyActivated()) {
6410 return false;
6411 }
6412
6413 return true;
6414 }
6415
6416 private static $isSkillManagementGloballyActivated = null;
6417
6418 public static function isSkillManagementGloballyActivated(): ?bool
6419 {
6420 if (self::$isSkillManagementGloballyActivated === null) {
6421 $skmgSet = new ilSkillManagementSettings();
6422
6423 self::$isSkillManagementGloballyActivated = $skmgSet->isActivated();
6424 }
6425
6426 return self::$isSkillManagementGloballyActivated;
6427 }
6428
6429 public function isShowGradingStatusEnabled(): bool
6430 {
6431 return $this->getScoreSettings()->getResultSummarySettings()->getShowGradingStatusEnabled();
6432 }
6433
6434 public function isShowGradingMarkEnabled(): bool
6435 {
6436 return $this->getScoreSettings()->getResultSummarySettings()->getShowGradingMarkEnabled();
6437 }
6438
6440 {
6441 return $this->getMainSettings()->getQuestionBehaviourSettings()->getLockAnswerOnNextQuestionEnabled();
6442 }
6443
6445 {
6446 return $this->getMainSettings()->getQuestionBehaviourSettings()->getLockAnswerOnInstantFeedbackEnabled();
6447 }
6448
6449 public function isForceInstantFeedbackEnabled(): ?bool
6450 {
6451 return $this->getMainSettings()->getQuestionBehaviourSettings()->getForceInstantFeedbackOnNextQuestion();
6452 }
6453
6454
6455 public static function isParticipantsLastPassActive(int $test_ref_id, int $user_id): bool
6456 {
6457 global $DIC;
6458 $ilDB = $DIC['ilDB'];
6459 $ilUser = $DIC['ilUser'];
6460
6461 $test_obj = ilObjectFactory::getInstanceByRefId($test_ref_id, false);
6462
6463 $active_id = $test_obj->getActiveIdOfUser($user_id);
6464
6465 $test_session_factory = new ilTestSessionFactory($test_obj, $ilDB, $ilUser);
6466
6467 // Added temporarily bugfix smeyer
6468 $test_session_factory->reset();
6469
6470 $test_sequence_factory = new ilTestSequenceFactory($test_obj, $ilDB, TestDIC::dic()['question.general_properties.repository']);
6471
6472 $test_session = $test_session_factory->getSession($active_id);
6473 $test_sequence = $test_sequence_factory->getSequenceByActiveIdAndPass($active_id, $test_session->getPass());
6474 $test_sequence->loadFromDb();
6475
6476 return $test_sequence->hasSequence();
6477 }
6478
6479 public function adjustTestSequence()
6480 {
6481 $query = "
6482 SELECT COUNT(test_question_id) cnt
6483 FROM tst_test_question
6484 WHERE test_fi = %s
6485 ORDER BY sequence
6486 ";
6487
6488 $questRes = $this->db->queryF($query, ['integer'], [$this->getTestId()]);
6489
6490 $row = $this->db->fetchAssoc($questRes);
6491 $questCount = $row['cnt'];
6492
6493 if ($this->getShuffleQuestions()) {
6494 $query = "
6495 SELECT tseq.*
6496 FROM tst_active tac
6497 INNER JOIN tst_sequence tseq
6498 ON tseq.active_fi = tac.active_id
6499 WHERE tac.test_fi = %s
6500 ";
6501
6502 $partRes = $this->db->queryF(
6503 $query,
6504 ['integer'],
6505 [$this->getTestId()]
6506 );
6507
6508 while ($row = $this->db->fetchAssoc($partRes)) {
6509 $sequence = @unserialize($row['sequence']);
6510
6511 if (!$sequence) {
6512 $sequence = [];
6513 }
6514
6515 $sequence = array_filter($sequence, function ($value) use ($questCount) {
6516 return $value <= $questCount;
6517 });
6518
6519 $num_seq = count($sequence);
6520 if ($questCount > $num_seq) {
6521 $diff = $questCount - $num_seq;
6522 for ($i = 1; $i <= $diff; $i++) {
6523 $sequence[$num_seq + $i - 1] = $num_seq + $i;
6524 }
6525 }
6526
6527 $new_sequence = serialize($sequence);
6528
6529 $this->db->update('tst_sequence', [
6530 'sequence' => ['clob', $new_sequence]
6531 ], [
6532 'active_fi' => ['integer', $row['active_fi']],
6533 'pass' => ['integer', $row['pass']]
6534 ]);
6535 }
6536 } else {
6537 $new_sequence = serialize($questCount > 0 ? range(1, $questCount) : []);
6538
6539 $query = "
6540 SELECT tseq.*
6541 FROM tst_active tac
6542 INNER JOIN tst_sequence tseq
6543 ON tseq.active_fi = tac.active_id
6544 WHERE tac.test_fi = %s
6545 ";
6546
6547 $part_rest = $this->db->queryF(
6548 $query,
6549 ['integer'],
6550 [$this->getTestId()]
6551 );
6552
6553 while ($row = $this->db->fetchAssoc($part_rest)) {
6554 $this->db->update('tst_sequence', [
6555 'sequence' => ['clob', $new_sequence]
6556 ], [
6557 'active_fi' => ['integer', $row['active_fi']],
6558 'pass' => ['integer', $row['pass']]
6559 ]);
6560 }
6561 }
6562 }
6563
6568 {
6569 return ilHtmlPurifierFactory::getInstanceByType('qpl_usersolution');
6570 }
6571
6573 {
6574 return $this->questionrepository;
6575 }
6576
6578 {
6579 return $this->global_settings_repo->getGlobalSettings();
6580 }
6581
6583 {
6584 if (!$this->main_settings) {
6585 $this->main_settings = $this->getMainSettingsRepository()
6586 ->getFor($this->getTestId());
6587 }
6588 return $this->main_settings;
6589 }
6590
6592 {
6593 return $this->main_settings_repository;
6594 }
6595
6597 {
6598 if (!$this->score_settings) {
6599 $this->score_settings = $this->getScoreSettingsRepository()
6600 ->getFor($this->getTestId());
6601 }
6602 return $this->score_settings;
6603 }
6604
6606 {
6607 return $this->score_settings_repository;
6608 }
6609
6610 public function addToNewsOnOnline(
6611 bool $old_online_status,
6612 bool $new_online_status
6613 ): void {
6614 if (!$old_online_status && $new_online_status) {
6615 $newsItem = new ilNewsItem();
6616 $newsItem->setContext($this->getId(), 'tst');
6617 $newsItem->setPriority(NEWS_NOTICE);
6618 $newsItem->setTitle('new_test_online');
6619 $newsItem->setContentIsLangVar(true);
6620 $newsItem->setContent('');
6621 $newsItem->setUserId($this->user->getId());
6622 $newsItem->setVisibility(NEWS_USERS);
6623 $newsItem->create();
6624 return;
6625 }
6626
6627 if ($old_online_status && !$new_online_status) {
6628 ilNewsItem::deleteNewsOfContext($this->getId(), 'tst');
6629 return;
6630 }
6631
6632 $newsId = ilNewsItem::getFirstNewsIdForContext($this->getId(), 'tst');
6633 if (!$new_online_status && $newsId > 0) {
6634 $newsItem = new ilNewsItem($newsId);
6635 $newsItem->setTitle('new_test_online');
6636 $newsItem->setContentIsLangVar(true);
6637 $newsItem->setContent('');
6638 $newsItem->update();
6639 }
6640 }
6641
6645 public static function _lookupRandomTest(int $obj_id): bool
6646 {
6647 return TestDIC::dic()['settings.main.repository']->getFor(
6649 )->getGeneralSettings()->getQuestionSetType() === self::QUESTION_SET_TYPE_RANDOM;
6650 }
6651
6652 public function getVisitingTimeOfParticipant(int $active_id): array
6653 {
6654 return $this->participant_repository->getFirstAndLastVisitForActiveId($active_id);
6655 }
6656}
$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:37
A class defining marks for assessment test objects.
Definition: Mark.php:37
withGeneralSettings(SettingsGeneral $settings)
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 _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 _getMobsOfObject(string $a_type, int $a_id, int|false $a_usage_hist_nr=0, string $a_lang="-")
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.
isPreviousSolutionReuseEnabled()
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.
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.
MediaObjectRepository $media_object_repository
getPotentialRandomTestQuestions()
getScoreSettingsRepository()
inviteUser($user_id, $client_ip="")
Invites a user to a test.
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)
processPrintoutput2FO($print_output)
Convert a print output to XSL-FO.
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.
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()
ResourceStorage $irss
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.
getPassScoring()
Gets the pass scoring type.
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
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.
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()
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)
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.
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)
MainSettingsRepository $main_settings_repository
string $author
int $activation_starting_time
addConcludingRemarksToSettingsFromImport(SettingsFinishing $settings, array $material, array $mappings)
getHighscoreAchievedTS()
Returns if date and time of the scores achievement should be displayed.
SettingsFactory $settings_factory
getHighscoreTopNum(int $a_retval=10)
Gets the number of entries which are to be shown in the top-rankings table.
Repository $test_result_repository
static _getCountSystem(int $active_id)
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 ...
static _getScoreSettingsByActiveId(int $active_id)
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)
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
ScoreSettingsRepository $score_settings_repository
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
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.
addIntroductionToSettingsFromImport(SettingsIntroduction $settings, array $material, array $mappings)
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.
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 $package, int $timeout=0)
static get(string $a_var)
ILIAS Setting Class.
special template class to simplify handling of ITX/PEAR
Base Exception for all Exceptions relating to Modules/Test.
@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...
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
$res
Definition: ltiservices.php:69
__construct(Container $dic, ilPlugin $plugin)
@inheritDoc
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()
$text
Definition: xapiexit.php:21