ILIAS  trunk Revision v12.0_alpha-1540-g00f839d5fa1
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 continue;
3738 }
3739
3740 $target_dir = $a_target_dir . DIRECTORY_SEPARATOR . 'objects'
3741 . DIRECTORY_SEPARATOR . 'il_' . IL_INST_ID . '_mob_' . $mob_id;
3742 ilFileUtils::createDirectory($target_dir);
3743 $media_obj = new ilObjMediaObject((int) $mob_id);
3744 $media_obj->exportXML($a_xml_writer, (int) $a_inst);
3746 foreach ($media_obj->getMediaItems() as $item) {
3747 $rid = $this->media_object_repository->getById($item->getMobId())['rid'] ?? null;
3748 if ($rid === null || $this->irss->manage()->find($rid) === null) {
3749 $expLog->write(date('[y-m-d H:i:s] ') . "The resource for Media Object {$item->getMobId()} does not exist (skipping)");
3750 continue;
3751 }
3752 $stream = $item->getLocationStream();
3753 file_put_contents($target_dir . DIRECTORY_SEPARATOR . $item->getLocation(), $stream);
3754 $stream->close();
3755 }
3756 unset($media_obj);
3757 }
3758 }
3759
3764 public function exportFileItems($target_dir, &$expLog)
3765 {
3766 foreach ($this->file_ids as $file_id) {
3767 $expLog->write(date("[y-m-d H:i:s] ") . "File Item " . $file_id);
3768 $file_dir = $target_dir . '/objects/il_' . IL_INST_ID . '_file_' . $file_id;
3769 ilFileUtils::makeDir($file_dir);
3770 $file_obj = new ilObjFile((int) $file_id, false);
3771 $source_file = $file_obj->getFile($file_obj->getVersion());
3772 if (!is_file($source_file)) {
3773 $source_file = $file_obj->getFile();
3774 }
3775 if (is_file($source_file)) {
3776 copy($source_file, $file_dir . '/' . $file_obj->getFileName());
3777 }
3778 unset($file_obj);
3779 }
3780 }
3781
3786 public function getImportMapping(): array
3787 {
3788 return [];
3789 }
3790
3793 public function onMarkSchemaSaved(): void
3794 {
3795 $this->saveCompleteStatus($this->question_set_config_factory->getQuestionSetConfig());
3796
3797 if ($this->participantDataExist()) {
3798 $this->recalculateScores(true);
3799 }
3800 }
3801
3805 public function marksEditable(): bool
3806 {
3807 $total = $this->evalTotalPersons();
3808 $results_summary_settings = $this->getScoreSettings()->getResultSummarySettings();
3809 if ($total === 0
3810 || $results_summary_settings->getScoreReporting()->isReportingEnabled() === false) {
3811 return true;
3812 }
3813
3814 if ($results_summary_settings->getScoreReporting() === ScoreReportingTypes::SCORE_REPORTING_DATE) {
3815 return $results_summary_settings->getReportingDate()
3816 >= new DateTimeImmutable('now', new DateTimeZone('UTC'));
3817 }
3818
3819 return false;
3820 }
3821
3831 public function saveAuthorToMetadata($author = "")
3832 {
3833 $path_to_lifecycle = $this->lo_metadata->paths()->custom()->withNextStep('lifeCycle')->get();
3834 $path_to_authors = $this->lo_metadata->paths()->authors();
3835
3836 $reader = $this->lo_metadata->read($this->getId(), 0, $this->getType(), $path_to_lifecycle);
3837 if (!is_null($reader->allData($path_to_lifecycle)->current())) {
3838 return;
3839 }
3840
3841 if ($author === '') {
3842 $author = $this->user->getFullname();
3843 }
3844 $this->lo_metadata->manipulate($this->getId(), 0, $this->getType())
3845 ->prepareCreateOrUpdate($path_to_authors, $author)
3846 ->execute();
3847 }
3848
3852 protected function doCreateMetaData(): void
3853 {
3854 $this->saveAuthorToMetadata();
3855 }
3856
3864 public function getAuthor(): string
3865 {
3866 $path_to_authors = $this->lo_metadata->paths()->authors();
3867 $author_data = $this->lo_metadata->read($this->getId(), 0, $this->getType(), $path_to_authors)
3868 ->allData($path_to_authors);
3869
3870 return $this->lo_metadata->dataHelper()->makePresentableAsList(', ', ...$author_data);
3871 }
3872
3880 public static function _lookupAuthor($obj_id): string
3881 {
3882 global $DIC;
3883
3884 $lo_metadata = $DIC->learningObjectMetadata();
3885
3886 $path_to_authors = $lo_metadata->paths()->authors();
3887 $author_data = $lo_metadata->read($obj_id, 0, "tst", $path_to_authors)
3888 ->allData($path_to_authors);
3889
3890 return $lo_metadata->dataHelper()->makePresentableAsList(',', ...$author_data);
3891 }
3892
3899 public static function _getAvailableTests($use_object_id = false): array
3900 {
3901 global $DIC;
3902 $ilUser = $DIC['ilUser'];
3903
3904 $result_array = [];
3905 $tests = array_slice(
3906 array_reverse(
3907 ilUtil::_getObjectsByOperations("tst", "write", $ilUser->getId(), PHP_INT_MAX)
3908 ),
3909 0,
3910 10000
3911 );
3912
3913 if (count($tests)) {
3914 $titles = ilObject::_prepareCloneSelection($tests, "tst");
3915 foreach ($tests as $ref_id) {
3916 if ($use_object_id) {
3918 $result_array[$obj_id] = $titles[$ref_id];
3919 } else {
3920 $result_array[$ref_id] = $titles[$ref_id];
3921 }
3922 }
3923 }
3924 return $result_array;
3925 }
3926
3935 public function cloneObject(int $target_id, int $copy_id = 0, bool $omit_tree = false): ?ilObject
3936 {
3937 $this->loadFromDb();
3938
3939 $new_obj = parent::cloneObject($target_id, $copy_id, $omit_tree);
3940 $new_obj->setTmpCopyWizardCopyId($copy_id);
3941 $this->cloneMetaData($new_obj);
3942
3943 $new_obj->saveToDb();
3944 $new_obj->addToNewsOnOnline(false, $new_obj->getObjectProperties()->getPropertyIsOnline()->getIsOnline());
3945
3946 $new_main_settings = $this->getMainSettings()
3947 ->withIntroductionSettings(
3948 $this->getMainSettings()->getIntroductionSettings()->withIntroductionPageId(
3949 $this->cloneIntroduction()
3950 )
3951 )->withFinishingSettings(
3952 $this->getMainSettings()->getFinishingSettings()->withConcludingRemarksPageId(
3953 $this->cloneConcludingRemarks()
3954 )
3955 )->withId(0);
3956
3957 $new_main_settings = $this->getMainSettingsRepository()->store($new_main_settings, $new_obj->getTestId());
3958 $this->getScoreSettingsRepository()->store(
3959 $this->getScoreSettings()->withId($new_main_settings->getId())
3960 );
3961 $this->marks_repository->storeMarkSchema(
3962 $this->getMarkSchema()->withTestId($new_obj->getTestId())
3963 );
3964
3965 $new_obj->setTemplate($this->getTemplate());
3966
3967 // clone certificate
3968 $pathFactory = new ilCertificatePathFactory();
3969 $templateRepository = new ilCertificateTemplateDatabaseRepository($this->db);
3970
3971 $cloneAction = new ilCertificateCloneAction(
3972 $this->db,
3973 $pathFactory,
3974 $templateRepository,
3976 );
3977
3978 $cloneAction->cloneCertificate($this, $new_obj);
3979
3980 $this->question_set_config_factory->getQuestionSetConfig()->cloneQuestionSetRelatedData($new_obj);
3981 $new_obj->saveQuestionsToDb();
3982
3983 $skillLevelThresholdList = new ilTestSkillLevelThresholdList($this->db);
3984 $skillLevelThresholdList->setTestId($this->getTestId());
3985 $skillLevelThresholdList->loadFromDb();
3986 $skillLevelThresholdList->cloneListForTest($new_obj->getTestId());
3987
3988 $obj_settings = new ilLPObjSettings($this->getId());
3989 $obj_settings->cloneSettings($new_obj->getId());
3990
3991 if ($new_obj->getTestLogger()->isLoggingEnabled()) {
3992 $new_obj->getTestLogger()->logTestAdministrationInteraction(
3993 $new_obj->getTestLogger()->getInteractionFactory()->buildTestAdministrationInteraction(
3994 $new_obj->getRefId(),
3995 $this->user->getId(),
3996 TestAdministrationInteractionTypes::NEW_TEST_CREATED,
3997 []
3998 )
3999 );
4000 }
4001
4002 return $new_obj;
4003 }
4004
4005 public function getQuestionCount(): int
4006 {
4007 $num = 0;
4008
4009 if ($this->isRandomTest()) {
4010 $questionSetConfig = new ilTestRandomQuestionSetConfig(
4011 $this->tree,
4012 $this->db,
4013 $this->lng,
4014 $this->logger,
4015 $this->component_repository,
4016 $this,
4017 $this->questionrepository
4018 );
4019
4020 $questionSetConfig->loadFromDb();
4021
4022 if ($questionSetConfig->isQuestionAmountConfigurationModePerPool()) {
4023 $sourcePoolDefinitionList = new ilTestRandomQuestionSetSourcePoolDefinitionList(
4024 $this->db,
4025 $this,
4027 );
4028
4029 $sourcePoolDefinitionList->loadDefinitions();
4030
4031 if (is_int($sourcePoolDefinitionList->getQuestionAmount())) {
4032 $num = $sourcePoolDefinitionList->getQuestionAmount();
4033 }
4034 } elseif (is_int($questionSetConfig->getQuestionAmountPerTest())) {
4035 $num = $questionSetConfig->getQuestionAmountPerTest();
4036 }
4037 } else {
4038 $this->loadQuestions();
4039 $num = count($this->questions);
4040 }
4041
4042 return $num;
4043 }
4044
4046 {
4047 if ($this->isRandomTest()) {
4048 return $this->getQuestionCount();
4049 }
4050 return count($this->questions);
4051 }
4052
4060 public static function _getObjectIDFromTestID($test_id)
4061 {
4062 global $DIC;
4063 $ilDB = $DIC['ilDB'];
4064 $object_id = false;
4065 $result = $ilDB->queryF(
4066 "SELECT obj_fi FROM tst_tests WHERE test_id = %s",
4067 ['integer'],
4068 [$test_id]
4069 );
4070 if ($result->numRows()) {
4071 $row = $ilDB->fetchAssoc($result);
4072 $object_id = $row["obj_fi"];
4073 }
4074 return $object_id;
4075 }
4076
4084 public static function _getObjectIDFromActiveID($active_id)
4085 {
4086 global $DIC;
4087 $ilDB = $DIC['ilDB'];
4088 $object_id = false;
4089 $result = $ilDB->queryF(
4090 "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",
4091 ['integer'],
4092 [$active_id]
4093 );
4094 if ($result->numRows()) {
4095 $row = $ilDB->fetchAssoc($result);
4096 $object_id = $row["obj_fi"];
4097 }
4098 return $object_id;
4099 }
4100
4108 public static function _getTestIDFromObjectID($object_id)
4109 {
4110 global $DIC;
4111 $ilDB = $DIC['ilDB'];
4112 $test_id = false;
4113 $result = $ilDB->queryF(
4114 "SELECT test_id FROM tst_tests WHERE obj_fi = %s",
4115 ['integer'],
4116 [$object_id]
4117 );
4118 if ($result->numRows()) {
4119 $row = $ilDB->fetchAssoc($result);
4120 $test_id = $row["test_id"];
4121 }
4122 return $test_id;
4123 }
4124
4133 public function getTextAnswer($active_id, $question_id, $pass = null): string
4134 {
4135 if (($active_id) && ($question_id)) {
4136 if ($pass === null) {
4137 $pass = assQuestion::_getSolutionMaxPass($question_id, $active_id);
4138 }
4139 if ($pass === null) {
4140 return '';
4141 }
4142 $query = $this->db->queryF(
4143 "SELECT value1 FROM tst_solutions WHERE active_fi = %s AND question_fi = %s AND pass = %s",
4144 ['integer', 'integer', 'integer'],
4145 [$active_id, $question_id, $pass]
4146 );
4147 $result = $this->db->fetchAll($query);
4148 if (count($result) == 1) {
4149 return $result[0]["value1"];
4150 }
4151 }
4152 return '';
4153 }
4154
4162 public function getQuestiontext($question_id): string
4163 {
4164 $res = "";
4165 if ($question_id) {
4166 $result = $this->db->queryF(
4167 "SELECT question_text FROM qpl_questions WHERE question_id = %s",
4168 ['integer'],
4169 [$question_id]
4170 );
4171 if ($result->numRows() == 1) {
4172 $row = $this->db->fetchAssoc($result);
4173 $res = $row["question_text"];
4174 }
4175 }
4176 return $res;
4177 }
4178
4180 {
4181 $participant_list = new ilTestParticipantList($this, $this->user, $this->lng, $this->db);
4182 $participant_list->initializeFromDbRows($this->getTestParticipants());
4183
4184 return $participant_list;
4185 }
4186
4193 public function &getInvitedUsers(int $user_id = 0, $order = "login, lastname, firstname"): array
4194 {
4195 $result_array = [];
4196
4197 if ($this->getAnonymity()) {
4198 if ($user_id !== 0) {
4199 $result = $this->db->queryF(
4200 "SELECT tst_active.active_id, tst_active.tries, usr_id, %s login, %s lastname, %s firstname, " .
4201 "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 " .
4202 "LEFT JOIN tst_active ON tst_active.user_fi = tst_invited_user.user_fi AND tst_active.test_fi = tst_invited_user.test_fi " .
4203 "WHERE tst_invited_user.test_fi = %s and tst_invited_user.user_fi=usr_data.usr_id AND usr_data.usr_id=%s " .
4204 "ORDER BY $order",
4205 ['text', 'text', 'text', 'integer', 'integer'],
4206 ['', $this->lng->txt('anonymous'), '', $this->getTestId(), $user_id]
4207 );
4208 } else {
4209 $result = $this->db->queryF(
4210 "SELECT tst_active.active_id, tst_active.tries, usr_id, %s login, %s lastname, %s firstname, " .
4211 "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 " .
4212 "LEFT JOIN tst_active ON tst_active.user_fi = tst_invited_user.user_fi AND tst_active.test_fi = tst_invited_user.test_fi " .
4213 "WHERE tst_invited_user.test_fi = %s and tst_invited_user.user_fi=usr_data.usr_id " .
4214 "ORDER BY $order",
4215 ['text', 'text', 'text', 'integer'],
4216 ['', $this->lng->txt('anonymous'), '', $this->getTestId()]
4217 );
4218 }
4219 } else {
4220 if ($user_id !== 0) {
4221 $result = $this->db->queryF(
4222 "SELECT tst_active.active_id, tst_active.tries, usr_id, login, lastname, firstname, " .
4223 "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 " .
4224 "LEFT JOIN tst_active ON tst_active.user_fi = tst_invited_user.user_fi AND tst_active.test_fi = tst_invited_user.test_fi " .
4225 "WHERE tst_invited_user.test_fi = %s and tst_invited_user.user_fi=usr_data.usr_id AND usr_data.usr_id=%s " .
4226 "ORDER BY $order",
4227 ['integer', 'integer'],
4228 [$this->getTestId(), $user_id]
4229 );
4230 } else {
4231 $result = $this->db->queryF(
4232 "SELECT tst_active.active_id, tst_active.tries, usr_id, login, lastname, firstname, " .
4233 "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 " .
4234 "LEFT JOIN tst_active ON tst_active.user_fi = tst_invited_user.user_fi AND tst_active.test_fi = tst_invited_user.test_fi " .
4235 "WHERE tst_invited_user.test_fi = %s and tst_invited_user.user_fi=usr_data.usr_id " .
4236 "ORDER BY $order",
4237 ['integer'],
4238 [$this->getTestId()]
4239 );
4240 }
4241 }
4242 $result_array = [];
4243 while ($row = $this->db->fetchAssoc($result)) {
4244 $result_array[$row['usr_id']] = $row;
4245 }
4246 return $result_array;
4247 }
4248
4249 public function getTestParticipants(): array
4250 {
4251 if ($this->getMainSettings()->getGeneralSettings()->getAnonymity()) {
4252 $query = "
4253 SELECT tst_active.active_id,
4254 tst_active.tries,
4255 tst_active.user_fi usr_id,
4256 %s login,
4257 %s lastname,
4258 %s firstname,
4259 tst_active.submitted test_finished,
4260 usr_data.matriculation,
4261 usr_data.active,
4262 tst_active.lastindex,
4263 COALESCE(tst_active.last_finished_pass, -1) <> tst_active.last_started_pass unfinished_passes
4264 FROM tst_active
4265 LEFT JOIN usr_data
4266 ON tst_active.user_fi = usr_data.usr_id
4267 WHERE tst_active.test_fi = %s
4268 ORDER BY usr_data.lastname
4269 ";
4270 $result = $this->db->queryF(
4271 $query,
4272 ['text', 'text', 'text', 'integer'],
4273 ['', $this->lng->txt("anonymous"), "", $this->getTestId()]
4274 );
4275 } else {
4276 $query = "
4277 SELECT tst_active.active_id,
4278 tst_active.tries,
4279 tst_active.user_fi usr_id,
4280 usr_data.login,
4281 usr_data.lastname,
4282 usr_data.firstname,
4283 tst_active.submitted test_finished,
4284 usr_data.matriculation,
4285 usr_data.active,
4286 tst_active.lastindex,
4287 COALESCE(tst_active.last_finished_pass, -1) <> tst_active.last_started_pass unfinished_passes
4288 FROM tst_active
4289 LEFT JOIN usr_data
4290 ON tst_active.user_fi = usr_data.usr_id
4291 WHERE tst_active.test_fi = %s
4292 ORDER BY usr_data.lastname
4293 ";
4294 $result = $this->db->queryF(
4295 $query,
4296 ['integer'],
4297 [$this->getTestId()]
4298 );
4299 }
4300 $data = [];
4301 while ($row = $this->db->fetchAssoc($result)) {
4302 $data[$row['active_id']] = $row;
4303 }
4304 foreach ($data as $index => $participant) {
4305 if (strlen(trim($participant["firstname"] . $participant["lastname"])) == 0) {
4306 $data[$index]["lastname"] = $this->lng->txt("deleted_user");
4307 }
4308 }
4309 return $data;
4310 }
4311
4312 public function getTestParticipantsForManualScoring($filter = null): array
4313 {
4314 if (!$this->getGlobalSettings()->isManualScoringEnabled()) {
4315 return [];
4316 }
4317
4318 $filtered_participants = [];
4319 foreach ($this->getTestParticipants() as $active_id => $participant) {
4320 if ($participant['tries'] > 0) {
4321 switch ($filter) {
4322 case 4:
4323 if ($this->test_man_scoring_done_helper->isDone((int) $active_id)) {
4324 $filtered_participants[$active_id] = $participant;
4325 }
4326 break;
4327 case 5:
4328 if (!$this->test_man_scoring_done_helper->isDone((int) $active_id)) {
4329 $filtered_participants[$active_id] = $participant;
4330 }
4331 break;
4332 default:
4333 $filtered_participants[$active_id] = $participant;
4334 }
4335 }
4336 }
4337 return $filtered_participants;
4338 }
4339
4346 public function getUserData($ids): array
4347 {
4348 if (!is_array($ids) || count($ids) == 0) {
4349 return [];
4350 }
4351
4352 if ($this->getAnonymity()) {
4353 $result = $this->db->queryF(
4354 "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",
4355 ['text', 'text', 'text'],
4356 ["", $this->lng->txt("anonymous"), ""]
4357 );
4358 } else {
4359 $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");
4360 }
4361
4362 $result_array = [];
4363 while ($row = $this->db->fetchAssoc($result)) {
4364 $result_array[$row["usr_id"]] = $row;
4365 }
4366 return $result_array;
4367 }
4368
4369 public function getGroupData($ids): array
4370 {
4371 if (!is_array($ids) || count($ids) == 0) {
4372 return [];
4373 }
4374 $result = [];
4375 foreach ($ids as $ref_id) {
4377 $result[$ref_id] = ["ref_id" => $ref_id, "title" => ilObject::_lookupTitle($obj_id), "description" => ilObject::_lookupDescription($obj_id)];
4378 }
4379 return $result;
4380 }
4381
4382 public function getRoleData($ids): array
4383 {
4384 if (!is_array($ids) || count($ids) == 0) {
4385 return [];
4386 }
4387 $result = [];
4388 foreach ($ids as $obj_id) {
4389 $result[$obj_id] = ["obj_id" => $obj_id, "title" => ilObject::_lookupTitle($obj_id), "description" => ilObject::_lookupDescription($obj_id)];
4390 }
4391 return $result;
4392 }
4393
4400 public function inviteUser($user_id, $client_ip = "")
4401 {
4402 $this->db->manipulateF(
4403 "DELETE FROM tst_invited_user WHERE test_fi = %s AND user_fi = %s",
4404 ['integer', 'integer'],
4405 [$this->getTestId(), $user_id]
4406 );
4407 $this->db->manipulateF(
4408 "INSERT INTO tst_invited_user (test_fi, user_fi, ip_range_from, ip_range_to, tstamp) VALUES (%s, %s, %s, %s, %s)",
4409 ['integer', 'integer', 'text', 'text', 'integer'],
4410 [$this->getTestId(), $user_id, (strlen($client_ip)) ? $client_ip : null, (strlen($client_ip)) ? $client_ip : null,time()]
4411 );
4412 }
4413
4419 public static function _getSolvedQuestions($active_id, $question_fi = null): array
4420 {
4421 global $DIC;
4422 $ilDB = $DIC['ilDB'];
4423 if (is_numeric($question_fi)) {
4424 $result = $ilDB->queryF(
4425 "SELECT question_fi, solved FROM tst_qst_solved WHERE active_fi = %s AND question_fi=%s",
4426 ['integer', 'integer'],
4427 [$active_id, $question_fi]
4428 );
4429 } else {
4430 $result = $ilDB->queryF(
4431 "SELECT question_fi, solved FROM tst_qst_solved WHERE active_fi = %s",
4432 ['integer'],
4433 [$active_id]
4434 );
4435 }
4436 $result_array = [];
4437 while ($row = $ilDB->fetchAssoc($result)) {
4438 $result_array[$row["question_fi"]] = $row;
4439 }
4440 return $result_array;
4441 }
4442
4443
4447 public function setQuestionSetSolved($value, $question_id, $user_id)
4448 {
4449 $active_id = $this->getActiveIdOfUser($user_id);
4450 $this->db->manipulateF(
4451 "DELETE FROM tst_qst_solved WHERE active_fi = %s AND question_fi = %s",
4452 ['integer', 'integer'],
4453 [$active_id, $question_id]
4454 );
4455 $this->db->manipulateF(
4456 "INSERT INTO tst_qst_solved (solved, question_fi, active_fi) VALUES (%s, %s, %s)",
4457 ['integer', 'integer', 'integer'],
4458 [$value, $question_id, $active_id]
4459 );
4460 }
4461
4465 public function isTestFinished($active_id): bool
4466 {
4467 $result = $this->db->queryF(
4468 "SELECT submitted FROM tst_active WHERE active_id=%s AND submitted=%s",
4469 ['integer', 'integer'],
4470 [$active_id, 1]
4471 );
4472 return $result->numRows() == 1;
4473 }
4474
4478 public function isActiveTestSubmitted($user_id = null): bool
4479 {
4480 if (!is_numeric($user_id)) {
4481 $user_id = $this->user->getId();
4482 }
4483
4484 $result = $this->db->queryF(
4485 "SELECT submitted FROM tst_active WHERE test_fi=%s AND user_fi=%s AND submitted=%s",
4486 ['integer', 'integer', 'integer'],
4487 [$this->getTestId(), $user_id, 1]
4488 );
4489 return $result->numRows() == 1;
4490 }
4491
4495 public function hasNrOfTriesRestriction(): bool
4496 {
4497 return $this->getNrOfTries() != 0;
4498 }
4499
4500
4506 public function isNrOfTriesReached($tries): bool
4507 {
4508 return $tries >= $this->getNrOfTries();
4509 }
4510
4511
4519 public function getAllTestResults($participants): array
4520 {
4521 $results = [];
4522 $row = [
4523 "user_id" => $this->lng->txt("user_id"),
4524 "matriculation" => $this->lng->txt("matriculation"),
4525 "lastname" => $this->lng->txt("lastname"),
4526 "firstname" => $this->lng->txt("firstname"),
4527 "login" => $this->lng->txt("login"),
4528 "reached_points" => $this->lng->txt("tst_reached_points"),
4529 "max_points" => $this->lng->txt("tst_maximum_points"),
4530 "percent_value" => $this->lng->txt("tst_percent_solved"),
4531 "mark" => $this->lng->txt("tst_mark"),
4532 "passed" => $this->lng->txt("tst_mark_passed"),
4533 ];
4534 $results[] = $row;
4535 if (count($participants)) {
4536 foreach ($participants as $active_id => $user_rec) {
4537 $mark = '';
4538 $row = [];
4539 $reached_points = 0;
4540 $max_points = 0;
4541 $pass = ilObjTest::_getResultPass($active_id);
4542 // abort if no valid pass can be found
4543 if (!is_int($pass)) {
4544 continue;
4545 }
4546 foreach ($this->questions as $value) {
4547 $question = ilObjTest::_instanciateQuestion($value);
4548 if (is_object($question)) {
4549 $max_points += $question->getMaximumPoints();
4550 $reached_points += $question->getReachedPoints($active_id, $pass);
4551 }
4552 }
4553 if ($max_points > 0) {
4554 $percentvalue = $reached_points / $max_points;
4555 if ($percentvalue < 0) {
4556 $percentvalue = 0.0;
4557 }
4558 } else {
4559 $percentvalue = 0;
4560 }
4561 $mark_obj = $this->getMarkSchema()->getMatchingMark($percentvalue * 100);
4562 $passed = "";
4563 if ($mark_obj !== null) {
4564 $mark = $mark_obj->getOfficialName();
4565 }
4566 if ($this->getAnonymity()) {
4567 $user_rec['firstname'] = "";
4568 $user_rec['lastname'] = $this->lng->txt("anonymous");
4569 }
4570 $results[] = [
4571 "user_id" => $user_rec['usr_id'],
4572 "matriculation" => $user_rec['matriculation'],
4573 "lastname" => $user_rec['lastname'],
4574 "firstname" => $user_rec['firstname'],
4575 "login" => $user_rec['login'],
4576 "reached_points" => $reached_points,
4577 "max_points" => $max_points,
4578 "percent_value" => $percentvalue,
4579 "mark" => $mark,
4580 "passed" => $user_rec['passed'] ? '1' : '0',
4581 ];
4582 }
4583 }
4584 return $results;
4585 }
4586
4595 public static function _getPass($active_id): int
4596 {
4597 global $DIC;
4598 $ilDB = $DIC['ilDB'];
4599 $result = $ilDB->queryF(
4600 "SELECT tries FROM tst_active WHERE active_id = %s",
4601 ['integer'],
4602 [$active_id]
4603 );
4604 if ($result->numRows()) {
4605 $row = $ilDB->fetchAssoc($result);
4606 return $row["tries"];
4607 } else {
4608 return 0;
4609 }
4610 }
4611
4621 public static function _getMaxPass($active_id): ?int
4622 {
4623 global $DIC;
4624 $ilDB = $DIC['ilDB'];
4625 $result = $ilDB->queryF(
4626 "SELECT MAX(pass) maxpass FROM tst_pass_result WHERE active_fi = %s",
4627 ['integer'],
4628 [$active_id]
4629 );
4630
4631 if ($result->numRows()) {
4632 $row = $ilDB->fetchAssoc($result);
4633 return $row["maxpass"];
4634 }
4635
4636 return null;
4637 }
4638
4644 public static function _getBestPass($active_id): ?int
4645 {
4646 global $DIC;
4647 $ilDB = $DIC['ilDB'];
4648
4649 $result = $ilDB->queryF(
4650 "SELECT * FROM tst_pass_result WHERE active_fi = %s",
4651 ['integer'],
4652 [$active_id]
4653 );
4654
4655 if (!$result->numRows()) {
4656 return null;
4657 }
4658
4659 $bestrow = null;
4660 $bestfactor = 0.0;
4661 while ($row = $ilDB->fetchAssoc($result)) {
4662 if ($row["maxpoints"] > 0.0) {
4663 $factor = (float) ($row["points"] / $row["maxpoints"]);
4664 } else {
4665 $factor = 0.0;
4666 }
4667 if ($factor === 0.0 && $bestfactor === 0.0
4668 || $factor > $bestfactor) {
4669 $bestrow = $row;
4670 $bestfactor = $factor;
4671 }
4672 }
4673
4674 if (is_array($bestrow)) {
4675 return $bestrow["pass"];
4676 }
4677
4678 return null;
4679 }
4680
4689 public static function _getResultPass($active_id): ?int
4690 {
4691 $counted_pass = null;
4692 if (ilObjTest::_getPassScoring($active_id) == self::SCORE_BEST_PASS) {
4693 $counted_pass = ilObjTest::_getBestPass($active_id);
4694 } else {
4695 $counted_pass = ilObjTest::_getMaxPass($active_id);
4696 }
4697 return $counted_pass;
4698 }
4699
4709 public function getAnsweredQuestionCount($active_id, $pass = null): int
4710 {
4711 if ($this->isRandomTest()) {
4712 $this->loadQuestions($active_id, $pass);
4713 }
4714 $workedthrough = 0;
4715 foreach ($this->questions as $value) {
4716 if ($this->questionrepository->lookupResultRecordExist($active_id, $value, $pass)) {
4717 $workedthrough += 1;
4718 }
4719 }
4720 return $workedthrough;
4721 }
4722
4729 public static function lookupPassResultsUpdateTimestamp($active_id, $pass): int
4730 {
4731 global $DIC;
4732 $ilDB = $DIC['ilDB'];
4733
4734 if (is_null($pass)) {
4735 $pass = 0;
4736 }
4737
4738 $query = "
4739 SELECT tst_pass_result.tstamp pass_res_tstamp,
4740 tst_test_result.tstamp quest_res_tstamp
4741
4742 FROM tst_pass_result
4743
4744 LEFT JOIN tst_test_result
4745 ON tst_test_result.active_fi = tst_pass_result.active_fi
4746 AND tst_test_result.pass = tst_pass_result.pass
4747
4748 WHERE tst_pass_result.active_fi = %s
4749 AND tst_pass_result.pass = %s
4750
4751 ORDER BY tst_test_result.tstamp DESC
4752 ";
4753
4754 $result = $ilDB->queryF(
4755 $query,
4756 ['integer', 'integer'],
4757 [$active_id, $pass]
4758 );
4759
4760 while ($row = $ilDB->fetchAssoc($result)) {
4761 if ($row['quest_res_tstamp']) {
4762 return $row['quest_res_tstamp'];
4763 }
4764
4765 return $row['pass_res_tstamp'];
4766 }
4767
4768 return 0;
4769 }
4770
4779 public function isExecutable($test_session, $user_id, $allow_pass_increase = false): array
4780 {
4781 $result = [
4782 "executable" => true,
4783 "errormessage" => ""
4784 ];
4785
4786 if (!$this->getObjectProperties()->getPropertyIsOnline()->getIsOnline()) {
4787 $result["executable"] = false;
4788 $result["errormessage"] = $this->lng->txt('autosave_failed') . ': ' . $this->lng->txt('offline');
4789 return $result;
4790 }
4791
4792 if (!$this->startingTimeReached()) {
4793 $result["executable"] = false;
4794 $result["errormessage"] = sprintf($this->lng->txt("detail_starting_time_not_reached"), ilDatePresentation::formatDate(new ilDateTime($this->getStartingTime(), IL_CAL_UNIX)));
4795 return $result;
4796 }
4797 if ($this->endingTimeReached()) {
4798 $result["executable"] = false;
4799 $result["errormessage"] = sprintf($this->lng->txt("detail_ending_time_reached"), ilDatePresentation::formatDate(new ilDateTime($this->getEndingTime(), IL_CAL_UNIX)));
4800 return $result;
4801 }
4802
4803 $active_id = $this->getActiveIdOfUser($user_id);
4804
4805 if ($this->getEnableProcessingTime()
4806 && $active_id > 0
4807 && ($starting_time = $this->getStartingTimeOfUser($active_id)) !== false
4808 && $this->isMaxProcessingTimeReached($starting_time, $active_id)) {
4809 $result["executable"] = false;
4810 $result["errormessage"] = $this->lng->txt("detail_max_processing_time_reached");
4811 return $result;
4812 }
4813
4814 $testPassesSelector = new ilTestPassesSelector($this->db, $this);
4815 $testPassesSelector->setActiveId($active_id);
4816 $testPassesSelector->setLastFinishedPass($test_session->getLastFinishedPass());
4817
4818 if ($this->hasNrOfTriesRestriction() && ($active_id > 0)) {
4819 $closedPasses = $testPassesSelector->getClosedPasses();
4820
4821 if (count($closedPasses) >= $this->getNrOfTries()) {
4822 $result["executable"] = false;
4823 $result["errormessage"] = $this->lng->txt("maximum_nr_of_tries_reached");
4824 return $result;
4825 }
4826
4827 if ($this->isBlockPassesAfterPassedEnabled() && !$testPassesSelector->openPassExists()) {
4828 if ($this->test_result_repository->isPassed($user_id, $this->getId())) {
4829 $result['executable'] = false;
4830 $result['errormessage'] = $this->lng->txt("tst_addit_passes_blocked_after_passed_msg");
4831 return $result;
4832 }
4833 }
4834 }
4835
4836 $next_pass_allowed_timestamp = 0;
4837 if (!$this->isNextPassAllowed($testPassesSelector, $next_pass_allowed_timestamp)) {
4838 $date = ilDatePresentation::formatDate(new ilDateTime($next_pass_allowed_timestamp, IL_CAL_UNIX));
4839
4840 $result['executable'] = false;
4841 $result['errormessage'] = sprintf($this->lng->txt('wait_for_next_pass_hint_msg'), $date);
4842 return $result;
4843 }
4844 return $result;
4845 }
4846
4847 public function isNextPassAllowed(ilTestPassesSelector $testPassesSelector, int &$next_pass_allowed_timestamp): bool
4848 {
4849 $waiting_between_passes = $this->getMainSettings()->getTestBehaviourSettings()->getPassWaiting();
4850 $last_finished_pass_timestamp = $testPassesSelector->getLastFinishedPassTimestamp();
4851
4852 if (
4853 $this->getMainSettings()->getTestBehaviourSettings()->getPassWaitingEnabled()
4854 && ($waiting_between_passes !== '')
4855 && ($testPassesSelector->getLastFinishedPass() !== null)
4856 && ($last_finished_pass_timestamp !== null)
4857 ) {
4858 $time_values = explode(':', $waiting_between_passes);
4859 $next_pass_allowed_timestamp = strtotime('+ ' . $time_values[0] . ' Days + ' . $time_values[1] . ' Hours' . $time_values[2] . ' Minutes', $last_finished_pass_timestamp);
4860 return (time() > $next_pass_allowed_timestamp);
4861 }
4862
4863 return true;
4864 }
4865
4866 public function canShowTestResults(ilTestSession $test_session): bool
4867 {
4868 $pass_selector = new ilTestPassesSelector($this->db, $this);
4869
4870 $pass_selector->setActiveId($test_session->getActiveId());
4871 $pass_selector->setLastFinishedPass($test_session->getLastFinishedPass());
4872
4873 return $pass_selector->hasReportablePasses();
4874 }
4875
4876 public function hasAnyTestResult(ilTestSession $test_session): bool
4877 {
4878 $pass_selector = new ilTestPassesSelector($this->db, $this);
4879
4880 $pass_selector->setActiveId($test_session->getActiveId());
4881 $pass_selector->setLastFinishedPass($test_session->getLastFinishedPass());
4882
4883 return $pass_selector->hasExistingPasses();
4884 }
4885
4893 public function getStartingTimeOfUser($active_id, $pass = null)
4894 {
4895 if ($active_id < 1) {
4896 return false;
4897 }
4898 if ($pass === null) {
4899 $pass = ($this->getResetProcessingTime()) ? self::_getPass($active_id) : 0;
4900 }
4901 $result = $this->db->queryF(
4902 "SELECT tst_times.started FROM tst_times WHERE tst_times.active_fi = %s AND tst_times.pass = %s ORDER BY tst_times.started",
4903 ['integer', 'integer'],
4904 [$active_id, $pass]
4905 );
4906 if ($result->numRows()) {
4907 $row = $this->db->fetchAssoc($result);
4908 if (preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["started"], $matches)) {
4909 return mktime(
4910 (int) $matches[4],
4911 (int) $matches[5],
4912 (int) $matches[6],
4913 (int) $matches[2],
4914 (int) $matches[3],
4915 (int) $matches[1]
4916 );
4917 } else {
4918 return time();
4919 }
4920 } else {
4921 return time();
4922 }
4923 }
4924
4931 public function isMaxProcessingTimeReached(int $starting_time, int $active_id): bool
4932 {
4933 if (!$this->getEnableProcessingTime()) {
4934 return false;
4935 }
4936
4937 $processing_time = $this->getProcessingTimeInSeconds($active_id);
4938 $now = time();
4939 if ($now > ($starting_time + $processing_time)) {
4940 return true;
4941 }
4942
4943 return false;
4944 }
4945
4946 public function getTestQuestions(): array
4947 {
4948 $tags_trafo = $this->refinery->string()->stripTags();
4949
4950 $query = "
4951 SELECT questions.*,
4952 questtypes.type_tag,
4953 tstquest.sequence,
4954 origquest.obj_fi orig_obj_fi
4955
4956 FROM qpl_questions questions
4957
4958 INNER JOIN qpl_qst_type questtypes
4959 ON questtypes.question_type_id = questions.question_type_fi
4960
4961 INNER JOIN tst_test_question tstquest
4962 ON tstquest.question_fi = questions.question_id
4963
4964 LEFT JOIN qpl_questions origquest
4965 ON origquest.question_id = questions.original_id
4966
4967 WHERE tstquest.test_fi = %s
4968
4969 ORDER BY tstquest.sequence
4970 ";
4971
4972 $query_result = $this->db->queryF(
4973 $query,
4974 ['integer'],
4975 [$this->getTestId()]
4976 );
4977
4978 $questions = [];
4979
4980 while ($row = $this->db->fetchAssoc($query_result)) {
4981 $row['title'] = $tags_trafo->transform($row['title']);
4982 $row['description'] = $tags_trafo->transform($row['description'] !== '' && $row['description'] !== null ? $row['description'] : '&nbsp;');
4983 $row['author'] = $tags_trafo->transform($row['author']);
4984
4985 $questions[] = $row;
4986 }
4987
4988 return $questions;
4989 }
4990
4991 public function isTestQuestion(int $question_id): bool
4992 {
4993 foreach ($this->getTestQuestions() as $questionData) {
4994 if ($questionData['question_id'] != $question_id) {
4995 continue;
4996 }
4997
4998 return true;
4999 }
5000
5001 return false;
5002 }
5003
5004 public function checkQuestionParent(int $question_id): bool
5005 {
5006 $row = $this->db->fetchAssoc($this->db->queryF(
5007 "SELECT COUNT(question_id) cnt FROM qpl_questions WHERE question_id = %s AND obj_fi = %s",
5008 ['integer', 'integer'],
5009 [$question_id, $this->getId()]
5010 ));
5011
5012 return (bool) $row['cnt'];
5013 }
5014
5015 public function getFixedQuestionSetTotalPoints(): float
5016 {
5017 $points = 0;
5018
5019 foreach ($this->getTestQuestions() as $question_data) {
5020 $points += $question_data['points'];
5021 }
5022
5023 return $points;
5024 }
5025
5029 public function getPotentialRandomTestQuestions(): array
5030 {
5031 $query = "
5032 SELECT questions.*,
5033 questtypes.type_tag,
5034 origquest.obj_fi orig_obj_fi
5035
5036 FROM qpl_questions questions
5037
5038 INNER JOIN qpl_qst_type questtypes
5039 ON questtypes.question_type_id = questions.question_type_fi
5040
5041 INNER JOIN tst_rnd_cpy tstquest
5042 ON tstquest.qst_fi = questions.question_id
5043
5044 LEFT JOIN qpl_questions origquest
5045 ON origquest.question_id = questions.original_id
5046
5047 WHERE tstquest.tst_fi = %s
5048 ";
5049
5050 $query_result = $this->db->queryF(
5051 $query,
5052 ['integer'],
5053 [$this->getTestId()]
5054 );
5055
5056 return $this->db->fetchAll($query_result);
5057 }
5058
5059 public function getShuffleQuestions(): bool
5060 {
5061 return $this->getMainSettings()->getQuestionBehaviourSettings()->getShuffleQuestions();
5062 }
5063
5076 {
5077 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getUsrPassOverviewMode();
5078 }
5079
5080 public function getListOfQuestions(): bool
5081 {
5082 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getQuestionListEnabled();
5083 }
5084
5085 public function getUsrPassOverviewEnabled(): bool
5086 {
5087 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getUsrPassOverviewEnabled();
5088 }
5089
5090 public function getListOfQuestionsStart(): bool
5091 {
5092 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getShownQuestionListAtBeginning();
5093 }
5094
5095 public function getListOfQuestionsEnd(): bool
5096 {
5097 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getShownQuestionListAtEnd();
5098 }
5099
5100 public function getListOfQuestionsDescription(): bool
5101 {
5102 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getShowDescriptionInQuestionList();
5103 }
5104
5108 public function getShowPassDetails(): bool
5109 {
5110 return $this->getScoreSettings()->getResultDetailsSettings()->getShowPassDetails();
5111 }
5112
5116 public function getShowSolutionPrintview(): bool
5117 {
5118 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionPrintview();
5119 }
5123 public function canShowSolutionPrintview($user_id = null): bool
5124 {
5125 return $this->getShowSolutionPrintview();
5126 }
5127
5131 public function getShowSolutionFeedback(): bool
5132 {
5133 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionFeedback();
5134 }
5135
5139 public function getShowSolutionAnswersOnly(): bool
5140 {
5141 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionAnswersOnly();
5142 }
5143
5147 public function getShowSolutionSignature(): bool
5148 {
5149 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionSignature();
5150 }
5151
5155 public function getShowSolutionSuggested(): bool
5156 {
5157 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionSuggested();
5158 }
5159
5164 public function getShowSolutionListComparison(): bool
5165 {
5166 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionListComparison();
5167 }
5168
5169 public function getShowSolutionListOwnAnswers(): bool
5170 {
5171 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionListOwnAnswers();
5172 }
5173
5177 public static function _getUserIdFromActiveId(int $active_id): int
5178 {
5179 global $DIC;
5180 $ilDB = $DIC['ilDB'];
5181 $result = $ilDB->queryF(
5182 "SELECT user_fi FROM tst_active WHERE active_id = %s",
5183 ['integer'],
5184 [$active_id]
5185 );
5186 if ($result->numRows()) {
5187 $row = $ilDB->fetchAssoc($result);
5188 return $row["user_fi"];
5189 } else {
5190 return -1;
5191 }
5192 }
5193
5194 public function _getLastAccess(int $active_id): string
5195 {
5196 $result = $this->db->queryF(
5197 "SELECT finished FROM tst_times WHERE active_fi = %s ORDER BY finished DESC",
5198 ['integer'],
5199 [$active_id]
5200 );
5201 if ($result->numRows()) {
5202 $row = $this->db->fetchAssoc($result);
5203 return $row["finished"];
5204 }
5205 return "";
5206 }
5207
5208 public static function lookupLastTestPassAccess(int $active_id, int $pass_index): ?int
5209 {
5211 global $DIC;
5212 $ilDB = $DIC['ilDB'];
5213
5214 $query = "
5215 SELECT MAX(tst_times.tstamp) as last_pass_access
5216 FROM tst_times
5217 WHERE active_fi = %s
5218 AND pass = %s
5219 ";
5220
5221 $res = $ilDB->queryF(
5222 $query,
5223 ['integer', 'integer'],
5224 [$active_id, $pass_index]
5225 );
5226
5227 while ($row = $ilDB->fetchAssoc($res)) {
5228 return $row['last_pass_access'];
5229 }
5230
5231 return null;
5232 }
5233
5241 public function isHTML($a_text): bool
5242 {
5243 if (preg_match("/<[^>]*?>/", $a_text)) {
5244 return true;
5245 } else {
5246 return false;
5247 }
5248 }
5249
5256 public function qtiMaterialToArray($a_material): array
5257 {
5258 $result = '';
5259 $mobs = [];
5260 for ($i = 0; $i < $a_material->getMaterialCount(); $i++) {
5261 $material = $a_material->getMaterial($i);
5262 if ($material['type'] === 'mattext') {
5263 $result .= $material['material']->getContent();
5264 }
5265 if ($material['type'] === 'matimage') {
5266 $matimage = $material['material'];
5267 if (preg_match('/(il_([0-9]+)_mob_([0-9]+))/', $matimage->getLabel(), $matches)) {
5268 $mobs[] = [
5269 'mob' => $matimage->getLabel(),
5270 'uri' => $matimage->getUri()
5271 ];
5272 }
5273 }
5274 }
5275
5276 $decoded_result = base64_decode($result);
5277 if (str_starts_with($decoded_result, '<PageObject>')) {
5278 $result = $decoded_result;
5279 }
5280
5281 return [
5282 'text' => $result,
5283 'mobs' => $mobs
5284 ];
5285 }
5286
5287 public function addQTIMaterial(ilXmlWriter &$xml_writer, ?int $page_id, string $material = ''): void
5288 {
5289 $xml_writer->xmlStartTag('material');
5290 $attrs = [
5291 'texttype' => 'text/plain'
5292 ];
5293 $file_ids = [];
5294 $mobs = [];
5295 if ($page_id !== null) {
5296 $attrs['texttype'] = 'text/xml';
5297 $mobs = ilObjMediaObject::_getMobsOfObject('tst:pg', $page_id);
5298 $page_object = new ilTestPage($page_id);
5299 $page_object->buildDom();
5300 $page_object->insertInstIntoIDs((string) IL_INST_ID);
5301 $material = base64_encode($page_object->getXMLFromDom());
5302 $file_ids = ilPCFileList::collectFileItems($page_object, $page_object->getDomDoc());
5303 foreach ($file_ids as $file_id) {
5304 $this->file_ids[] = (int) $file_id;
5305 };
5306 $mob_string = 'il_' . IL_INST_ID . '_mob_';
5307 } elseif ($this->isHTML($material)) {
5308 $attrs['texttype'] = 'text/xhtml';
5309 $mobs = ilObjMediaObject::_getMobsOfObject('tst:html', $this->getId());
5310 $mob_string = 'mm_';
5311 }
5312
5313 $xml_writer->xmlElement('mattext', $attrs, $material);
5314 foreach ($mobs as $mob) {
5315 $mob_id_string = (string) $mob;
5316 $moblabel = 'il_' . IL_INST_ID . '_mob_' . $mob_id_string;
5317 if (strpos($material, $mob_string . $mob_id_string) !== false) {
5318 if (ilObjMediaObject::_exists($mob)) {
5319 $mob_obj = new ilObjMediaObject($mob);
5320 $imgattrs = [
5321 'label' => $moblabel,
5322 'uri' => 'objects/' . 'il_' . IL_INST_ID . '_mob_' . $mob_id_string . '/' . $mob_obj->getTitle()
5323 ];
5324 }
5325 $xml_writer->xmlElement('matimage', $imgattrs, null);
5326 }
5327 }
5328 $xml_writer->xmlEndTag('material');
5329 }
5330
5337 public function prepareTextareaOutput($txt_output, $prepare_for_latex_output = false, $omitNl2BrWhenTextArea = false)
5338 {
5339 if ($txt_output == null) {
5340 $txt_output = '';
5341 }
5343 $txt_output,
5344 $prepare_for_latex_output,
5345 $omitNl2BrWhenTextArea
5346 );
5347 }
5348
5349 public function getAnonymity(): bool
5350 {
5351 return $this->getMainSettings()->getGeneralSettings()->getAnonymity();
5352 }
5353
5354 public function getShowCancel(): bool
5355 {
5356 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getSuspendTestAllowed();
5357 }
5358
5359 public function getShowMarker(): bool
5360 {
5361 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getQuestionMarkingEnabled();
5362 }
5363
5364 public function getFixedParticipants(): bool
5365 {
5366 return $this->getMainSettings()->getAccessSettings()->getFixedParticipants();
5367 }
5368
5369 public function lookupQuestionSetTypeByActiveId(int $active_id): string
5370 {
5371 return $this->main_settings_repository->getFor(
5372 self::_getTestIDFromObjectID(self::_getObjectIDFromActiveID($active_id)),
5373 )->getGeneralSettings()->getQuestionSetType();
5374 }
5375
5386 public function userLookupFullName($user_id, $overwrite_anonymity = false, $sorted_order = false, $suffix = ""): string
5387 {
5388 if ($this->getAnonymity() && !$overwrite_anonymity) {
5389 return $this->lng->txt("anonymous") . $suffix;
5390 } else {
5392 if (strlen($uname["firstname"] . $uname["lastname"]) == 0) {
5393 $uname["firstname"] = $this->lng->txt("deleted_user");
5394 }
5395 if ($sorted_order) {
5396 return trim($uname["lastname"] . ", " . $uname["firstname"]) . $suffix;
5397 } else {
5398 return trim($uname["firstname"] . " " . $uname["lastname"]) . $suffix;
5399 }
5400 }
5401 }
5402
5404 DateTimeImmutable|int|null $date_time
5405 ): ?DateTimeImmutable {
5406 if ($date_time === null || $date_time instanceof DateTimeImmutable) {
5407 return $date_time;
5408 }
5409
5410 return DateTimeImmutable::createFromFormat('U', (string) $date_time);
5411 }
5412
5420 public function processPrintoutput2FO($print_output): string
5421 {
5422 if (extension_loaded("tidy")) {
5423 $config = [
5424 "indent" => false,
5425 "output-xml" => true,
5426 "numeric-entities" => true
5427 ];
5428 $tidy = new tidy();
5429 $tidy->parseString($print_output, $config, 'utf8');
5430 $tidy->cleanRepair();
5431 $print_output = tidy_get_output($tidy);
5432 $print_output = preg_replace("/^.*?(<html)/", "\\1", $print_output);
5433 } else {
5434 $print_output = str_replace("&nbsp;", "&#160;", $print_output);
5435 $print_output = str_replace("&otimes;", "X", $print_output);
5436 }
5437 $xsl = file_get_contents("./components/ILIAS/Test/xml/question2fo.xsl");
5438
5439 // additional font support
5440
5441 $xsl = str_replace(
5442 'font-family="Helvetica, unifont"',
5443 'font-family="' . $this->settings->get('rpc_pdf_font', 'Helvetica, unifont') . '"',
5444 $xsl
5445 );
5446
5447 $args = [ '/_xml' => $print_output, '/_xsl' => $xsl ];
5448 $xh = xslt_create();
5449 $params = [];
5450 $output = xslt_process($xh, "arg:/_xml", "arg:/_xsl", null, $args, $params);
5451 xslt_error($xh);
5452 xslt_free($xh);
5453 return $output;
5454 }
5455
5462 public function deliverPDFfromHTML($content, $title = null)
5463 {
5464 $content = preg_replace("/href=\".*?\"/", "", $content);
5465 $printbody = new ilTemplate("tpl.il_as_tst_print_body.html", true, true, "components/ILIAS/Test");
5466 $printbody->setVariable("TITLE", ilLegacyFormElementsUtil::prepareFormOutput($this->getTitle()));
5467 $printbody->setVariable("ADM_CONTENT", $content);
5468 $printbody->setCurrentBlock("css_file");
5469 $printbody->setVariable("CSS_FILE", ilUtil::getStyleSheetLocation("filesystem", "delos.css"));
5470 $printbody->parseCurrentBlock();
5471 $printoutput = $printbody->get();
5472 $html = str_replace("href=\"./", "href=\"" . ILIAS_HTTP_PATH . "/", $printoutput);
5473 $html = preg_replace("/<div id=\"dontprint\">.*?<\\/div>/ims", "", $html);
5474 if (extension_loaded("tidy")) {
5475 $config = [
5476 "indent" => false,
5477 "output-xml" => true,
5478 "numeric-entities" => true
5479 ];
5480 $tidy = new tidy();
5481 $tidy->parseString($html, $config, 'utf8');
5482 $tidy->cleanRepair();
5483 $html = tidy_get_output($tidy);
5484 $html = preg_replace("/^.*?(<html)/", "\\1", $html);
5485 } else {
5486 $html = str_replace("&nbsp;", "&#160;", $html);
5487 $html = str_replace("&otimes;", "X", $html);
5488 }
5489 $html = preg_replace("/src=\".\\//ims", "src=\"" . ILIAS_HTTP_PATH . "/", $html);
5490 $this->deliverPDFfromFO($this->processPrintoutput2FO($html), $title);
5491 }
5492
5498 public function deliverPDFfromFO($fo, $title = null): bool
5499 {
5500 $fo_file = ilFileUtils::ilTempnam() . ".fo";
5501 $fp = fopen($fo_file, "w");
5502 fwrite($fp, $fo);
5503 fclose($fp);
5504
5505 try {
5506 $pdf_base64 = ilRpcClientFactory::factory('RPCTransformationHandler')->ilFO2PDF($fo);
5507 $filename = (strlen($title)) ? $title : $this->getTitle();
5510 $pdf_base64->scalar,
5512 "application/pdf"
5513 );
5514 return true;
5515 } catch (Exception $e) {
5516 $this->logger->info(__METHOD__ . ': ' . $e->getMessage());
5517 return false;
5518 }
5519 }
5520
5530 public static function getManualFeedback(int $active_id, int $question_id, ?int $pass): string
5531 {
5532 if ($pass === null) {
5533 return '';
5534 }
5535 $feedback = '';
5536 $row = self::getSingleManualFeedback((int) $active_id, (int) $question_id, (int) $pass);
5537
5538 if ($row !== [] && ($row['finalized_evaluation'] || \ilTestService::isManScoringDone((int) $active_id))) {
5539 $feedback = $row['feedback'] ?? '';
5540 }
5541
5542 return $feedback;
5543 }
5544
5545 public static function getSingleManualFeedback(int $active_id, int $question_id, int $pass): array
5546 {
5547 global $DIC;
5548 $ilDB = $DIC['ilDB'];
5549 $row = [];
5550 $result = $ilDB->queryF(
5551 "SELECT * FROM tst_manual_fb WHERE active_fi = %s AND question_fi = %s AND pass = %s",
5552 ['integer', 'integer', 'integer'],
5553 [$active_id, $question_id, $pass]
5554 );
5555
5556 if ($ilDB->numRows($result) === 1) {
5557 $row = $ilDB->fetchAssoc($result);
5558 $row['feedback'] = ilRTE::_replaceMediaObjectImageSrc($row['feedback'] ?? '', 1);
5559 } elseif ($ilDB->numRows($result) > 1) {
5560 $DIC->logger()->root()->warning(
5561 "WARNING: Multiple feedback entries on tst_manual_fb for " .
5562 "active_fi = $active_id , question_fi = $question_id and pass = $pass"
5563 );
5564 }
5565
5566 return $row;
5567 }
5568
5576 public function getCompleteManualFeedback(int $question_id): array
5577 {
5578 global $DIC;
5579 $ilDB = $DIC['ilDB'];
5580
5581 $feedback = [];
5582 $result = $ilDB->queryF(
5583 "SELECT * FROM tst_manual_fb WHERE question_fi = %s",
5584 ['integer'],
5585 [$question_id]
5586 );
5587
5588 while ($row = $ilDB->fetchAssoc($result)) {
5589 $active = $row['active_fi'];
5590 $pass = $row['pass'];
5591 $question = $row['question_fi'];
5592
5593 $row['feedback'] = ilRTE::_replaceMediaObjectImageSrc($row['feedback'] ?? '', 1);
5594
5595 $feedback[$active][$pass][$question] = $row;
5596 }
5597
5598 return $feedback;
5599 }
5600
5601 public function saveManualFeedback(
5602 int $active_id,
5603 int $question_id,
5604 int $pass,
5605 ?string $feedback,
5606 bool $finalized = false
5607 ): void {
5608 $feedback_old = self::getSingleManualFeedback($active_id, $question_id, $pass);
5609 $this->db->manipulateF(
5610 'DELETE FROM tst_manual_fb WHERE active_fi = %s AND question_fi = %s AND pass = %s',
5611 ['integer', 'integer', 'integer'],
5612 [$active_id, $question_id, $pass]
5613 );
5614
5615 $this->insertManualFeedback($active_id, $question_id, $pass, $feedback, $finalized, $feedback_old);
5616
5617 }
5618
5619 private function insertManualFeedback(
5620 int $active_id,
5621 int $question_id,
5622 int $pass,
5623 ?string $feedback,
5624 bool $finalized,
5625 array $feedback_old
5626 ): void {
5627 $next_id = $this->db->nextId('tst_manual_fb');
5628 $user = $this->user->getId();
5629 $finalized_time = time();
5630
5631 $update_default = [
5632 'manual_feedback_id' => [ 'integer', $next_id],
5633 'active_fi' => [ 'integer', $active_id],
5634 'question_fi' => [ 'integer', $question_id],
5635 'pass' => [ 'integer', $pass],
5636 'feedback' => [ 'clob', $feedback ? ilRTE::_replaceMediaObjectImageSrc($feedback, 0) : null],
5637 'tstamp' => [ 'integer', time()]
5638 ];
5639
5640 if ($feedback_old !== [] && (int) $feedback_old['finalized_evaluation'] === 1) {
5641 $user = $feedback_old['finalized_by_usr_id'];
5642 $finalized_time = $feedback_old['finalized_tstamp'];
5643 }
5644
5645 if ($finalized === false) {
5646 $update_default['finalized_evaluation'] = ['integer', 0];
5647 $update_default['finalized_by_usr_id'] = ['integer', 0];
5648 $update_default['finalized_tstamp'] = ['integer', 0];
5649 } elseif ($finalized === true) {
5650 $update_default['finalized_evaluation'] = ['integer', 1];
5651 $update_default['finalized_by_usr_id'] = ['integer', $user];
5652 $update_default['finalized_tstamp'] = ['integer', $finalized_time];
5653 }
5654
5655 $this->db->insert('tst_manual_fb', $update_default);
5656 }
5657
5665 public function getJavaScriptOutput(): bool
5666 {
5667 return true;
5668 }
5669
5670 public function &createTestSequence($active_id, $pass, $shuffle)
5671 {
5672 $this->test_sequence = new ilTestSequence($active_id, $pass, $this->isRandomTest(), $this->questionrepository);
5673 }
5674
5680 public function setTestId($a_id)
5681 {
5682 $this->test_id = $a_id;
5683 }
5684
5692 public function getDetailedTestResults($participants): array
5693 {
5694 $results = [];
5695 if (count($participants)) {
5696 foreach ($participants as $active_id => $user_rec) {
5697 $row = [];
5698 $reached_points = 0;
5699 $max_points = 0;
5700 $pass = ilObjTest::_getResultPass($active_id);
5701 foreach ($this->questions as $value) {
5702 $question = ilObjTest::_instanciateQuestion($value);
5703 if (is_object($question)) {
5704 $max_points += $question->getMaximumPoints();
5705 $reached_points += $question->getReachedPoints($active_id, $pass);
5706 if ($max_points > 0) {
5707 $percentvalue = $reached_points / $max_points;
5708 if ($percentvalue < 0) {
5709 $percentvalue = 0.0;
5710 }
5711 } else {
5712 $percentvalue = 0;
5713 }
5714 if ($this->getAnonymity()) {
5715 $user_rec['firstname'] = "";
5716 $user_rec['lastname'] = $this->lng->txt("anonymous");
5717 }
5718 $results[] = [
5719 "user_id" => $user_rec['usr_id'],
5720 "matriculation" => $user_rec['matriculation'],
5721 "lastname" => $user_rec['lastname'],
5722 "firstname" => $user_rec['firstname'],
5723 "login" => $user_rec['login'],
5724 "question_id" => $question->getId(),
5725 "question_title" => $question->getTitle(),
5726 "reached_points" => $reached_points,
5727 "max_points" => $max_points,
5728 "passed" => $user_rec['passed'] ? '1' : '0',
5729 ];
5730 }
5731 }
5732 }
5733 }
5734 return $results;
5735 }
5736
5740 public static function _lookupTestObjIdForQuestionId(int $q_id): ?int
5741 {
5742 global $DIC;
5743 $ilDB = $DIC['ilDB'];
5744
5745 $result = $ilDB->queryF(
5746 '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',
5747 ['integer'],
5748 [$q_id]
5749 );
5750 $rec = $ilDB->fetchAssoc($result);
5751 return $rec['obj_id'] ?? null;
5752 }
5753
5760 public function isPluginActive($a_pname): bool
5761 {
5762 if (!$this->component_repository->getComponentByTypeAndName(
5764 'TestQuestionPool'
5765 )->getPluginSlotById('qst')->hasPluginName($a_pname)) {
5766 return false;
5767 }
5768
5769 return $this->component_repository
5770 ->getComponentByTypeAndName(
5772 'TestQuestionPool'
5773 )
5774 ->getPluginSlotById(
5775 'qst'
5776 )
5777 ->getPluginByName(
5778 $a_pname
5779 )->isActive();
5780 }
5781
5785 public function getParticipantsForTestAndQuestion($test_id, $question_id): array
5786 {
5787 $query = "
5788 SELECT tst_test_result.active_fi, tst_test_result.question_fi, tst_test_result.pass
5789 FROM tst_test_result
5790 INNER JOIN tst_active ON tst_active.active_id = tst_test_result.active_fi AND tst_active.test_fi = %s
5791 INNER JOIN qpl_questions ON qpl_questions.question_id = tst_test_result.question_fi
5792 LEFT JOIN usr_data ON usr_data.usr_id = tst_active.user_fi
5793 WHERE tst_test_result.question_fi = %s
5794 ORDER BY usr_data.lastname ASC, usr_data.firstname ASC
5795 ";
5796
5797 $result = $this->db->queryF(
5798 $query,
5799 ['integer', 'integer'],
5800 [$test_id, $question_id]
5801 );
5802 $foundusers = [];
5804 while ($row = $this->db->fetchAssoc($result)) {
5805 if ($this->getAccessFilteredParticipantList() && !$this->getAccessFilteredParticipantList()->isActiveIdInList($row["active_fi"])) {
5806 continue;
5807 }
5808
5809 if (!array_key_exists($row["active_fi"], $foundusers)) {
5810 $foundusers[$row["active_fi"]] = [];
5811 }
5812 array_push($foundusers[$row["active_fi"]], ["pass" => $row["pass"], "qid" => $row["question_fi"]]);
5813 }
5814 return $foundusers;
5815 }
5816
5817 public function getAggregatedResultsData(): array
5818 {
5819 $data = $this->getCompleteEvaluationData();
5820 $found_participants = $data->getParticipants();
5821 $results = ['overview' => [], 'questions' => []];
5822 if ($found_participants !== []) {
5823 $results['overview']['tst_stat_result_mark_median'] = $data->getStatistics()->getEvaluationDataOfMedianUser()?->getMark()?->getShortName() ?? '';
5824 $results['overview']['tst_stat_result_rank_median'] = $data->getStatistics()->rankMedian();
5825 $results['overview']['tst_stat_result_total_participants'] = $data->getStatistics()->count();
5826 $results['overview']['tst_stat_result_median'] = $data->getStatistics()->median();
5827 $results['overview']['tst_eval_total_persons'] = count($found_participants);
5828 $total_finished = $data->getTotalFinishedParticipants();
5829 $results['overview']['tst_eval_total_finished'] = $total_finished;
5830 $results['overview']['tst_eval_total_finished_average_time'] =
5831 $this->secondsToHoursMinutesSecondsString(
5832 $this->evalTotalStartedAverageTime($data->getParticipantIds())
5833 );
5834 $total_passed = 0;
5835 $total_passed_reached = 0;
5836 $total_passed_max = 0;
5837 $total_passed_time = 0;
5838 foreach ($found_participants as $userdata) {
5839 if ($userdata->getMark()?->getPassed()) {
5840 $total_passed++;
5841 $total_passed_reached += $userdata->getReached();
5842 $total_passed_max += $userdata->getMaxpoints();
5843 $total_passed_time += $userdata->getTimeOnTask();
5844 }
5845 }
5846 $average_passed_reached = $total_passed ? $total_passed_reached / $total_passed : 0;
5847 $average_passed_max = $total_passed ? $total_passed_max / $total_passed : 0;
5848 $average_passed_time = $total_passed ? $total_passed_time / $total_passed : 0;
5849 $results['overview']['tst_eval_total_passed'] = $total_passed;
5850 $results['overview']['tst_eval_total_passed_average_points'] = sprintf('%2.2f', $average_passed_reached)
5851 . ' ' . strtolower('of') . ' ' . sprintf('%2.2f', $average_passed_max);
5852 $results['overview']['tst_eval_total_passed_average_time'] =
5853 $this->secondsToHoursMinutesSecondsString((int) $average_passed_time);
5854 }
5855
5856 foreach ($data->getQuestionTitles() as $question_id => $question_title) {
5857 $answered = 0;
5858 $reached = 0;
5859 $max = 0;
5860 foreach ($found_participants as $userdata) {
5861 for ($i = 0; $i <= $userdata->getLastPass(); $i++) {
5862 if (is_object($userdata->getPass($i))) {
5863 $question = $userdata->getPass($i)->getAnsweredQuestionByQuestionId($question_id);
5864 if (is_array($question)) {
5865 $answered++;
5866 $reached += $question['reached'];
5867 $max += $question['points'];
5868 }
5869 }
5870 }
5871 }
5872 $percent = $max ? $reached / $max * 100.0 : 0;
5873 $results['questions'][$question_id] = [
5874 $question_title,
5875 sprintf('%.2f', $answered ? $reached / $answered : 0) . ' ' . strtolower($this->lng->txt('of')) . ' ' . sprintf('%.2f', $answered ? $max / $answered : 0),
5876 sprintf('%.2f', $percent) . '%',
5877 $answered,
5878 sprintf('%.2f', $answered ? $reached / $answered : 0),
5879 sprintf('%.2f', $answered ? $max / $answered : 0),
5880 $percent / 100.0
5881 ];
5882 }
5883 return $results;
5884 }
5885
5886 protected function secondsToHoursMinutesSecondsString(int $seconds): string
5887 {
5888 $diff_hours = floor($seconds / 3600);
5889 $seconds -= $diff_hours * 3600;
5890 $diff_minutes = floor($seconds / 60);
5891 $seconds -= $diff_minutes * 60;
5892 return sprintf('%02d:%02d:%02d', $diff_hours, $diff_minutes, $seconds);
5893 }
5894
5898 public function getXMLZip(): string
5899 {
5900 return $this->export_factory->getExporter($this, 'xml')
5901 ->write();
5902 }
5903
5904 public function getExportSettings(): int
5905 {
5906 return $this->getScoreSettings()->getResultDetailsSettings()->getExportSettings();
5907 }
5908
5909 public function setTemplate(int $template_id)
5910 {
5911 $this->template_id = $template_id;
5912 }
5913
5914 public function getTemplate(): int
5915 {
5916 return $this->template_id;
5917 }
5918
5920 {
5921 $question_set_config = $this->question_set_config_factory->getQuestionSetConfig();
5922 $reindexed_sequence_position_map = $question_set_config->reindexQuestionOrdering();
5923
5924 $this->loadQuestions();
5925
5926 return $reindexed_sequence_position_map;
5927 }
5928
5929 public function setQuestionOrder(array $order)
5930 {
5931 asort($order);
5932
5933 $i = 0;
5934
5935 foreach (array_keys($order) as $id) {
5936 $i++;
5937
5938 $query = "
5939 UPDATE tst_test_question
5940 SET sequence = %s
5941 WHERE question_fi = %s
5942 ";
5943
5944 $this->db->manipulateF(
5945 $query,
5946 ['integer', 'integer'],
5947 [$i, $id]
5948 );
5949 }
5950
5951 if ($this->logger->isLoggingEnabled()) {
5952 $this->logger->logTestAdministrationInteraction(
5953 $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
5954 $this->getRefId(),
5955 $this->user->getId(),
5956 TestAdministrationInteractionTypes::QUESTION_MOVED,
5957 [
5958 AdditionalInformationGenerator::KEY_QUESTION_ORDER => $order
5959 ]
5960 )
5961 );
5962 }
5963
5964 $this->loadQuestions();
5965 }
5966
5967 public function hasQuestionsWithoutQuestionpool(): bool
5968 {
5969 $questions = $this->getQuestionTitlesAndIndexes();
5970
5971 $IN_questions = $this->db->in('q1.question_id', array_keys($questions), false, 'integer');
5972
5973 $query = "
5974 SELECT count(q1.question_id) cnt
5975
5976 FROM qpl_questions q1
5977
5978 INNER JOIN qpl_questions q2
5979 ON q2.question_id = q1.original_id
5980
5981 WHERE $IN_questions
5982 AND q1.obj_fi = q2.obj_fi
5983 ";
5984 $rset = $this->db->query($query);
5985 $row = $this->db->fetchAssoc($rset);
5986
5987 return $row['cnt'] > 0;
5988 }
5989
5996 public static function _lookupFinishedUserTests($a_user_id): array
5997 {
5998 global $DIC;
5999 $ilDB = $DIC['ilDB'];
6000
6001 $result = $ilDB->queryF(
6002 "SELECT test_fi,MAX(pass) AS pass FROM tst_active" .
6003 " JOIN tst_pass_result ON (tst_pass_result.active_fi = tst_active.active_id)" .
6004 " WHERE user_fi=%s" .
6005 " GROUP BY test_fi",
6006 ['integer', 'integer'],
6007 [$a_user_id, 1]
6008 );
6009 $all = [];
6010 while ($row = $ilDB->fetchAssoc($result)) {
6011 $obj_id = self::_getObjectIDFromTestID($row["test_fi"]);
6012 $all[$obj_id] = (bool) $row["pass"];
6013 }
6014 return $all;
6015 }
6016
6017 public function getQuestions(): array
6018 {
6019 return $this->questions;
6020 }
6021
6022 public function isOnline(): bool
6023 {
6024 return $this->online;
6025 }
6026
6027 public function getIntroductionPageId(): int
6028 {
6029 $page_id = $this->getMainSettings()->getIntroductionSettings()->getIntroductionPageId();
6030 if ($page_id !== null) {
6031 return $page_id;
6032 }
6033
6034 $page_object = new ilTestPage();
6035 $page_object->setParentId($this->getId());
6036 $new_page_id = $page_object->createPageWithNextId();
6037 $settings = $this->getMainSettings()->getIntroductionSettings()
6038 ->withIntroductionPageId($new_page_id);
6039 $this->getMainSettingsRepository()->store(
6040 $this->getMainSettings()->withIntroductionSettings($settings)
6041 );
6042 return $new_page_id;
6043 }
6044
6046 {
6047 $page_id = $this->getMainSettings()->getFinishingSettings()->getConcludingRemarksPageId();
6048 if ($page_id !== null) {
6049 return $page_id;
6050 }
6051
6052 $page_object = new ilTestPage();
6053 $page_object->setParentId($this->getId());
6054 $new_page_id = $page_object->createPageWithNextId();
6055 $settings = $this->getMainSettings()->getFinishingSettings()
6056 ->withConcludingRemarksPageId($new_page_id);
6057 $this->getMainSettingsRepository()->store(
6058 $this->getMainSettings()->withFinishingSettings($settings)
6059 );
6060 return $new_page_id;
6061 }
6062
6063 public function getHighscoreEnabled(): bool
6064 {
6065 return $this->getScoreSettings()->getGamificationSettings()->getHighscoreEnabled();
6066 }
6067
6077 public function getHighscoreAnon(): bool
6078 {
6079 return $this->getScoreSettings()->getGamificationSettings()->getHighscoreAnon();
6080 }
6081
6090 public function isHighscoreAnon(): bool
6091 {
6092 return $this->getAnonymity() == 1 || $this->getHighscoreAnon();
6093 }
6094
6098 public function getHighscoreAchievedTS(): bool
6099 {
6100 return $this->getScoreSettings()->getGamificationSettings()->getHighscoreAchievedTS();
6101 }
6102
6106 public function getHighscoreScore(): bool
6107 {
6108 return $this->getScoreSettings()->getGamificationSettings()->getHighscoreScore();
6109 }
6110
6114 public function getHighscorePercentage(): bool
6115 {
6116 return $this->getScoreSettings()->getGamificationSettings()->getHighscorePercentage();
6117 }
6118
6122 public function getHighscoreWTime(): bool
6123 {
6124 return $this->getScoreSettings()->getGamificationSettings()->getHighscoreWTime();
6125 }
6126
6130 public function getHighscoreOwnTable(): bool
6131 {
6132 return $this->getScoreSettings()->getGamificationSettings()->getHighscoreOwnTable();
6133 }
6134
6138 public function getHighscoreTopTable(): bool
6139 {
6140 return $this->getScoreSettings()->getGamificationSettings()->getHighscoreTopTable();
6141 }
6142
6147 public function getHighscoreTopNum(int $a_retval = 10): int
6148 {
6149 return $this->getScoreSettings()->getGamificationSettings()->getHighscoreTopNum();
6150 }
6151
6152 public function getHighscoreMode(): int
6153 {
6154 return $this->getScoreSettings()->getGamificationSettings()->getHighScoreMode();
6155 }
6156
6157 public function getSpecificAnswerFeedback(): bool
6158 {
6159 return $this->getMainSettings()->getQuestionBehaviourSettings()->getInstantFeedbackSpecificEnabled();
6160 }
6161
6162 public function getAutosave(): bool
6163 {
6164 return $this->getMainSettings()->getQuestionBehaviourSettings()->getAutosaveEnabled();
6165 }
6166
6167 public function isPassDeletionAllowed(): bool
6168 {
6169 return $this->getScoreSettings()->getResultSummarySettings()->getPassDeletionAllowed();
6170 }
6171
6172 public function getEnableExamview(): bool
6173 {
6174 return $this->getMainSettings()->getFinishingSettings()->getShowAnswerOverview();
6175 }
6176
6183 public function getStartingTimeOfParticipants(): array
6184 {
6185 $times = [];
6186 $result = $this->db->queryF(
6187 "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",
6188 ['integer'],
6189 [$this->getTestId()]
6190 );
6191 while ($row = $this->db->fetchAssoc($result)) {
6192 $times[$row['active_fi']] = $row['started'];
6193 }
6194 return $times;
6195 }
6196
6197 public function getTimeExtensionsOfParticipants(): array
6198 {
6199 $times = [];
6200 $result = $this->db->queryF(
6201 "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",
6202 ['integer'],
6203 [$this->getTestId()]
6204 );
6205 while ($row = $this->db->fetchAssoc($result)) {
6206 $times[$row['active_fi']] = $row['additionaltime'];
6207 }
6208 return $times;
6209 }
6210
6211 private function getExtraTime(int $active_id): int
6212 {
6213 if ($active_id === 0) {
6214 return 0;
6215 }
6216 return $this->participant_repository
6217 ->getParticipantByActiveId($this->getTestId(), $active_id)
6218 ?->getExtraTime() ?? 0;
6219 }
6220
6221 public function getMaxPassOfTest(): int
6222 {
6223 $query = '
6224 SELECT MAX(tst_pass_result.pass) + 1 max_res
6225 FROM tst_pass_result
6226 INNER JOIN tst_active ON tst_active.active_id = tst_pass_result.active_fi
6227 WHERE test_fi = ' . $this->db->quote($this->getTestId(), 'integer') . '
6228 ';
6229 $res = $this->db->query($query);
6230 $data = $this->db->fetchAssoc($res);
6231 return (int) $data['max_res'];
6232 }
6233
6234 public static function lookupExamId($active_id, $pass)
6235 {
6236 global $DIC;
6237 $ilDB = $DIC['ilDB'];
6238
6239 $exam_id_query = 'SELECT exam_id FROM tst_pass_result WHERE active_fi = %s AND pass = %s';
6240 $exam_id_result = $ilDB->queryF($exam_id_query, [ 'integer', 'integer' ], [ $active_id, $pass ]);
6241 if ($ilDB->numRows($exam_id_result) == 1) {
6242 $exam_id_row = $ilDB->fetchAssoc($exam_id_result);
6243
6244 if ($exam_id_row['exam_id'] != null) {
6245 return $exam_id_row['exam_id'];
6246 }
6247 }
6248
6249 return null;
6250 }
6251
6252 public static function buildExamId($active_id, $pass, $test_obj_id = null): string
6253 {
6254 global $DIC;
6255 $ilSetting = $DIC['ilSetting'];
6256
6257 $inst_id = $ilSetting->get('inst_id', null);
6258
6259 if ($test_obj_id === null) {
6260 $obj_id = self::_getObjectIDFromActiveID($active_id);
6261 } else {
6262 $obj_id = $test_obj_id;
6263 }
6264
6265 $examId = 'I' . $inst_id . '_T' . $obj_id . '_A' . $active_id . '_P' . $pass;
6266
6267 return $examId;
6268 }
6269
6270 public function isShowExamIdInTestPassEnabled(): bool
6271 {
6272 return $this->getMainSettings()->getTestBehaviourSettings()->getExamIdInTestAttemptEnabled();
6273 }
6274
6275 public function isShowExamIdInTestResultsEnabled(): bool
6276 {
6277 return $this->getScoreSettings()->getResultDetailsSettings()->getShowExamIdInTestResults();
6278 }
6279
6280
6281 public function setQuestionSetType(string $question_set_type)
6282 {
6283 $this->main_settings = $this->getMainSettings()->withGeneralSettings(
6284 $this->getMainSettings()->getGeneralSettings()
6285 ->withQuestionSetType($question_set_type)
6286 );
6287 }
6288
6289 public function getQuestionSetType(): string
6290 {
6291 return $this->getMainSettings()->getGeneralSettings()->getQuestionSetType();
6292 }
6293
6299 public function isFixedTest(): bool
6300 {
6301 return $this->getQuestionSetType() == self::QUESTION_SET_TYPE_FIXED;
6302 }
6303
6309 public function isRandomTest(): bool
6310 {
6311 return $this->getQuestionSetType() == self::QUESTION_SET_TYPE_RANDOM;
6312 }
6313
6314 public function getQuestionSetTypeTranslation(ilLanguage $lng, $questionSetType): string
6315 {
6316 switch ($questionSetType) {
6318 return $lng->txt('tst_question_set_type_fixed');
6319
6321 return $lng->txt('tst_question_set_type_random');
6322 }
6323
6324 throw new ilTestException('invalid question set type value given: ' . $questionSetType);
6325 }
6326
6327 public function participantDataExist(): bool
6328 {
6329 if ($this->participantDataExist === null) {
6330 $this->participantDataExist = (bool) $this->evalTotalPersons();
6331 }
6332
6333 return $this->participantDataExist;
6334 }
6335
6336 public function recalculateScores($preserve_manscoring = false)
6337 {
6338 $scoring = new TestScoring(
6339 $this,
6340 $this->user,
6341 $this->db,
6342 $this->test_result_repository
6343 );
6344 $scoring->setPreserveManualScores($preserve_manscoring);
6345 $scoring->recalculateSolutions();
6346 }
6347
6348 public static function getTestObjIdsWithActiveForUserId($userId): array
6349 {
6350 global $DIC;
6351 $ilDB = $DIC['ilDB'];
6352
6353 $query = "
6354 SELECT obj_fi
6355 FROM tst_active
6356 INNER JOIN tst_tests
6357 ON test_id = test_fi
6358 WHERE user_fi = %s
6359 ";
6360
6361 $res = $ilDB->queryF($query, ['integer'], [$userId]);
6362
6363 $objIds = [];
6364
6365 while ($row = $ilDB->fetchAssoc($res)) {
6366 $objIds[] = (int) $row['obj_fi'];
6367 }
6368
6369 return $objIds;
6370 }
6371
6372 public function isSkillServiceEnabled(): bool
6373 {
6374 return $this->getMainSettings()->getAdditionalSettings()->getSkillsServiceEnabled();
6375 }
6376
6388 public function isSkillServiceToBeConsidered(): bool
6389 {
6390 if (!$this->getMainSettings()->getAdditionalSettings()->getSkillsServiceEnabled()) {
6391 return false;
6392 }
6393
6394 if (!self::isSkillManagementGloballyActivated()) {
6395 return false;
6396 }
6397
6398 return true;
6399 }
6400
6401 private static $isSkillManagementGloballyActivated = null;
6402
6403 public static function isSkillManagementGloballyActivated(): ?bool
6404 {
6405 if (self::$isSkillManagementGloballyActivated === null) {
6406 $skmgSet = new ilSkillManagementSettings();
6407
6408 self::$isSkillManagementGloballyActivated = $skmgSet->isActivated();
6409 }
6410
6411 return self::$isSkillManagementGloballyActivated;
6412 }
6413
6414 public function isShowGradingStatusEnabled(): bool
6415 {
6416 return $this->getScoreSettings()->getResultSummarySettings()->getShowGradingStatusEnabled();
6417 }
6418
6419 public function isShowGradingMarkEnabled(): bool
6420 {
6421 return $this->getScoreSettings()->getResultSummarySettings()->getShowGradingMarkEnabled();
6422 }
6423
6425 {
6426 return $this->getMainSettings()->getQuestionBehaviourSettings()->getLockAnswerOnNextQuestionEnabled();
6427 }
6428
6430 {
6431 return $this->getMainSettings()->getQuestionBehaviourSettings()->getLockAnswerOnInstantFeedbackEnabled();
6432 }
6433
6434 public function isForceInstantFeedbackEnabled(): ?bool
6435 {
6436 return $this->getMainSettings()->getQuestionBehaviourSettings()->getForceInstantFeedbackOnNextQuestion();
6437 }
6438
6439
6440 public static function isParticipantsLastPassActive(int $test_ref_id, int $user_id): bool
6441 {
6442 global $DIC;
6443 $ilDB = $DIC['ilDB'];
6444 $ilUser = $DIC['ilUser'];
6445
6446 $test_obj = ilObjectFactory::getInstanceByRefId($test_ref_id, false);
6447
6448 $active_id = $test_obj->getActiveIdOfUser($user_id);
6449
6450 $test_session_factory = new ilTestSessionFactory($test_obj, $ilDB, $ilUser);
6451
6452 // Added temporarily bugfix smeyer
6453 $test_session_factory->reset();
6454
6455 $test_sequence_factory = new ilTestSequenceFactory($test_obj, $ilDB, TestDIC::dic()['question.general_properties.repository']);
6456
6457 $test_session = $test_session_factory->getSession($active_id);
6458 $test_sequence = $test_sequence_factory->getSequenceByActiveIdAndPass($active_id, $test_session->getPass());
6459 $test_sequence->loadFromDb();
6460
6461 return $test_sequence->hasSequence();
6462 }
6463
6464 public function adjustTestSequence()
6465 {
6466 $query = "
6467 SELECT COUNT(test_question_id) cnt
6468 FROM tst_test_question
6469 WHERE test_fi = %s
6470 ORDER BY sequence
6471 ";
6472
6473 $questRes = $this->db->queryF($query, ['integer'], [$this->getTestId()]);
6474
6475 $row = $this->db->fetchAssoc($questRes);
6476 $questCount = $row['cnt'];
6477
6478 if ($this->getShuffleQuestions()) {
6479 $query = "
6480 SELECT tseq.*
6481 FROM tst_active tac
6482 INNER JOIN tst_sequence tseq
6483 ON tseq.active_fi = tac.active_id
6484 WHERE tac.test_fi = %s
6485 ";
6486
6487 $partRes = $this->db->queryF(
6488 $query,
6489 ['integer'],
6490 [$this->getTestId()]
6491 );
6492
6493 while ($row = $this->db->fetchAssoc($partRes)) {
6494 $sequence = @unserialize($row['sequence']);
6495
6496 if (!$sequence) {
6497 $sequence = [];
6498 }
6499
6500 $sequence = array_filter($sequence, function ($value) use ($questCount) {
6501 return $value <= $questCount;
6502 });
6503
6504 $num_seq = count($sequence);
6505 if ($questCount > $num_seq) {
6506 $diff = $questCount - $num_seq;
6507 for ($i = 1; $i <= $diff; $i++) {
6508 $sequence[$num_seq + $i - 1] = $num_seq + $i;
6509 }
6510 }
6511
6512 $new_sequence = serialize($sequence);
6513
6514 $this->db->update('tst_sequence', [
6515 'sequence' => ['clob', $new_sequence]
6516 ], [
6517 'active_fi' => ['integer', $row['active_fi']],
6518 'pass' => ['integer', $row['pass']]
6519 ]);
6520 }
6521 } else {
6522 $new_sequence = serialize($questCount > 0 ? range(1, $questCount) : []);
6523
6524 $query = "
6525 SELECT tseq.*
6526 FROM tst_active tac
6527 INNER JOIN tst_sequence tseq
6528 ON tseq.active_fi = tac.active_id
6529 WHERE tac.test_fi = %s
6530 ";
6531
6532 $part_rest = $this->db->queryF(
6533 $query,
6534 ['integer'],
6535 [$this->getTestId()]
6536 );
6537
6538 while ($row = $this->db->fetchAssoc($part_rest)) {
6539 $this->db->update('tst_sequence', [
6540 'sequence' => ['clob', $new_sequence]
6541 ], [
6542 'active_fi' => ['integer', $row['active_fi']],
6543 'pass' => ['integer', $row['pass']]
6544 ]);
6545 }
6546 }
6547 }
6548
6553 {
6554 return ilHtmlPurifierFactory::getInstanceByType('qpl_usersolution');
6555 }
6556
6558 {
6559 return $this->questionrepository;
6560 }
6561
6563 {
6564 return $this->global_settings_repo->getGlobalSettings();
6565 }
6566
6568 {
6569 if (!$this->main_settings) {
6570 $this->main_settings = $this->getMainSettingsRepository()
6571 ->getFor($this->getTestId());
6572 }
6573 return $this->main_settings;
6574 }
6575
6577 {
6578 return $this->main_settings_repository;
6579 }
6580
6582 {
6583 if (!$this->score_settings) {
6584 $this->score_settings = $this->getScoreSettingsRepository()
6585 ->getFor($this->getTestId());
6586 }
6587 return $this->score_settings;
6588 }
6589
6591 {
6592 return $this->score_settings_repository;
6593 }
6594
6595 public function addToNewsOnOnline(
6596 bool $old_online_status,
6597 bool $new_online_status
6598 ): void {
6599 if (!$old_online_status && $new_online_status) {
6600 $newsItem = new ilNewsItem();
6601 $newsItem->setContext($this->getId(), 'tst');
6602 $newsItem->setPriority(NEWS_NOTICE);
6603 $newsItem->setTitle('new_test_online');
6604 $newsItem->setContentIsLangVar(true);
6605 $newsItem->setContent('');
6606 $newsItem->setUserId($this->user->getId());
6607 $newsItem->setVisibility(NEWS_USERS);
6608 $newsItem->create();
6609 return;
6610 }
6611
6612 if ($old_online_status && !$new_online_status) {
6613 ilNewsItem::deleteNewsOfContext($this->getId(), 'tst');
6614 return;
6615 }
6616
6617 $newsId = ilNewsItem::getFirstNewsIdForContext($this->getId(), 'tst');
6618 if (!$new_online_status && $newsId > 0) {
6619 $newsItem = new ilNewsItem($newsId);
6620 $newsItem->setTitle('new_test_online');
6621 $newsItem->setContentIsLangVar(true);
6622 $newsItem->setContent('');
6623 $newsItem->update();
6624 }
6625 }
6626
6630 public static function _lookupRandomTest(int $obj_id): bool
6631 {
6632 return TestDIC::dic()['settings.main.repository']->getFor(
6634 )->getGeneralSettings()->getQuestionSetType() === self::QUESTION_SET_TYPE_RANDOM;
6635 }
6636
6637 public function getVisitingTimeOfParticipant(int $active_id): array
6638 {
6639 return $this->participant_repository->getFirstAndLastVisitForActiveId($active_id);
6640 }
6641}
$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:26
global $ilSetting
Definition: privfeed.php:26
$results
if(!file_exists('../ilias.ini.php'))
global $DIC
Definition: shib_login.php:26
$counter
$objectives
getGeneralSettings()
$text
Definition: xapiexit.php:21