ILIAS  trunk Revision v12.0_alpha-377-g3641b37b9db
class.ilObjTest.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
28use ILIAS\Test\ExportImport\Factory as ExportImportFactory;
37use ILIAS\Test\Settings\GlobalSettings\Repository as GlobalSettingsRepository;
49use ILIAS\Refinery\Factory as Refinery;
52
63class ilObjTest extends ilObject
64{
66
67 public const QUESTION_SET_TYPE_FIXED = 'FIXED_QUEST_SET';
68 public const QUESTION_SET_TYPE_RANDOM = 'RANDOM_QUEST_SET';
69 public const INVITATION_OFF = 0;
70 public const INVITATION_ON = 1;
71 public const SCORE_LAST_PASS = 0;
72 public const SCORE_BEST_PASS = 1;
73
74 private ?bool $activation_limited = null;
75 private array $mob_ids;
76 private array $file_ids = [];
77 private bool $online;
81 private ?MarkSchema $mark_schema = null;
82 public int $test_id = -1;
84 public string $author;
85
89 public $metadata;
90 public array $questions = [];
91
96
100 public $test_sequence = false;
101
102 private int $template_id = 0;
103
104 protected bool $print_best_solution_with_result = true;
105
106 protected bool $activation_visibility = false;
107 protected ?int $activation_starting_time = null;
108 protected ?int $activation_ending_time = null;
109
115 private $participantDataExist = null;
116
117 private ?int $tmpCopyWizardCopyId = null;
118
121 protected Refinery $refinery;
125
126 protected GlobalSettingsRepository $global_settings_repo;
127 protected ?MainSettings $main_settings = null;
132
136
137 protected ExportImportFactory $export_factory;
138
142
146
147 protected LOMetadata $lo_metadata;
148
155 public function __construct(int $id = 0, bool $a_call_by_reference = true)
156 {
157 $this->type = "tst";
158
160 global $DIC;
161 $this->ctrl = $DIC['ilCtrl'];
162 $this->refinery = $DIC['refinery'];
163 $this->settings = $DIC['ilSetting'];
164 $this->bench = $DIC['ilBench'];
165 $this->component_repository = $DIC['component.repository'];
166 $this->component_factory = $DIC['component.factory'];
167 $this->filesystem_web = $DIC->filesystem()->web();
168 $this->lo_metadata = $DIC->learningObjectMetadata();
169
170 $local_dic = $this->getLocalDIC();
171 $this->participant_access_filter = $local_dic['participant.access_filter.factory'];
172 $this->test_man_scoring_done_helper = $local_dic['scoring.manual.done_helper'];
173 $this->logger = $local_dic['logging.logger'];
174 $this->log_viewer = $local_dic['logging.viewer'];
175 $this->global_settings_repo = $local_dic['settings.global.repository'];
176 $this->marks_repository = $local_dic['marks.repository'];
177 $this->settings_factory = $local_dic['settings.factory'];
178 $this->questionrepository = $local_dic['question.general_properties.repository'];
179 $this->testrequest = $local_dic['request_data_collector'];
180 $this->participant_repository = $local_dic['participant.repository'];
181 $this->export_factory = $local_dic['exportimport.factory'];
182 $this->test_result_repository = $local_dic['results.data.repository'];
183 $this->main_settings_repository = $local_dic['settings.main.repository'];
184 $this->score_settings_repository = $local_dic['settings.scoring.repository'];
185
186 parent::__construct($id, $a_call_by_reference);
187
188 $this->lng->loadLanguageModule("assessment");
189 $this->score_settings = null;
190
191 $this->question_set_config_factory = new ilTestQuestionSetConfigFactory(
192 $this->tree,
193 $this->db,
194 $this->lng,
195 $this->logger,
196 $this->component_repository,
197 $this,
198 $this->questionrepository
199 );
200 }
201
202 public function getLocalDIC(): TestDIC
203 {
204 return TestDIC::dic();
205 }
206
207 public function getTestLogger(): TestLogger
208 {
209 return $this->logger;
210 }
211
213 {
214 return $this->log_viewer;
215 }
216
218 {
219 return $this->question_set_config_factory->getQuestionSetConfig();
220 }
221
225 public function getTitleFilenameCompliant(): string
226 {
228 }
229
230 public function getTmpCopyWizardCopyId(): ?int
231 {
233 }
234
235 public function setTmpCopyWizardCopyId(int $tmpCopyWizardCopyId): void
236 {
237 $this->tmpCopyWizardCopyId = $tmpCopyWizardCopyId;
238 }
239
240 public function create(): int
241 {
242 $id = parent::create();
243 $this->createMetaData();
244 return $id;
245 }
246
247 public function update(): bool
248 {
249 if (!parent::update()) {
250 return false;
251 }
252
253 // put here object specific stuff
254 $this->updateMetaData();
255 return true;
256 }
257
258 public function read(): void
259 {
260 parent::read();
261 $this->main_settings = null;
262 $this->score_settings = null;
263 $this->mark_schema = null;
264 $this->loadFromDb();
265 }
266
267 public function delete(): bool
268 {
269 // always call parent delete function first!!
270 if (!parent::delete()) {
271 return false;
272 }
273
274 // delet meta data
275 $this->deleteMetaData();
276
277 //put here your module specific stuff
278 $this->deleteTest();
279
280 $qsaImportFails = new ilAssQuestionSkillAssignmentImportFails($this->getId());
281 $qsaImportFails->deleteRegisteredImportFails();
282 $sltImportFails = new ilTestSkillLevelThresholdImportFails($this->getId());
283 $sltImportFails->deleteRegisteredImportFails();
284
285 if ($this->logger->isLoggingEnabled()) {
286 $this->logger->logTestAdministrationInteraction(
287 $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
288 $this->getRefId(),
289 $this->user->getId(),
291 [
292 AdditionalInformationGenerator::KEY_TEST_TITLE => $test_title = $this->title
293 ]
294 )
295 );
296 }
297
298 return true;
299 }
300
301 public function deleteTest(): void
302 {
303 $participantData = new ilTestParticipantData($this->db, $this->lng);
304 $participantData->load($this->getTestId());
305 $this->removeTestResults($participantData);
306
307 $this->db->manipulateF(
308 "DELETE FROM tst_mark WHERE test_fi = %s",
309 ['integer'],
310 [$this->getTestId()]
311 );
312
313 $this->db->manipulateF(
314 "DELETE FROM tst_tests WHERE test_id = %s",
315 ['integer'],
316 [$this->getTestId()]
317 );
318
319 $this->db->manipulateF(
320 "DELETE FROM tst_test_settings WHERE id = %s",
321 ['integer'],
322 [$this->getMainSettings()->getId()]
323 );
324
325 $tst_data_dir = ilFileUtils::getDataDir() . "/tst_data";
326 $directory = $tst_data_dir . "/tst_" . $this->getId();
327 if (is_dir($directory)) {
328 ilFileUtils::delDir($directory);
329 }
330 $mobs = ilObjMediaObject::_getMobsOfObject("tst:html", $this->getId());
331 // remaining usages are not in text anymore -> delete them
332 // and media objects (note: delete method of ilObjMediaObject
333 // checks whether object is used in another context; if yes,
334 // the object is not deleted!)
335 foreach ($mobs as $mob) {
336 ilObjMediaObject::_removeUsage($mob, "tst:html", $this->getId());
337 if (ilObjMediaObject::_exists($mob)) {
338 $mob_obj = new ilObjMediaObject($mob);
339 $mob_obj->delete();
340 }
341 }
342 }
343
349 public function createExportDirectory(): void
350 {
351 $tst_data_dir = ilFileUtils::getDataDir() . "/tst_data";
352 ilFileUtils::makeDir($tst_data_dir);
353 if (!is_writable($tst_data_dir)) {
354 $this->ilias->raiseError("Test Data Directory (" . $tst_data_dir
355 . ") not writeable.", $this->ilias->error_obj->MESSAGE);
356 }
357
358 // create learning module directory (data_dir/lm_data/lm_<id>)
359 $tst_dir = $tst_data_dir . "/tst_" . $this->getId();
360 ilFileUtils::makeDir($tst_dir);
361 if (!@is_dir($tst_dir)) {
362 $this->ilias->raiseError("Creation of Test Directory failed.", $this->ilias->error_obj->MESSAGE);
363 }
364 // create Export subdirectory (data_dir/lm_data/lm_<id>/Export)
365 $export_dir = $tst_dir . "/export";
366 ilFileUtils::makeDir($export_dir);
367 if (!@is_dir($export_dir)) {
368 $this->ilias->raiseError("Creation of Export Directory failed.", $this->ilias->error_obj->MESSAGE);
369 }
370 }
371
372 public function getExportDirectory(): string
373 {
374 $export_dir = ilFileUtils::getDataDir() . "/tst_data" . "/tst_" . $this->getId() . "/export";
375 return $export_dir;
376 }
377
378 public function getExportFiles(string $dir = ''): array
379 {
380 // quit if import dir not available
381 if (!@is_dir($dir) || !is_writable($dir)) {
382 return [];
383 }
384
385 $files = [];
386 foreach (new DirectoryIterator($dir) as $file) {
390 if ($file->isDir()) {
391 continue;
392 }
393
394 $files[] = $file->getBasename();
395 }
396
397 sort($files);
398
399 return $files;
400 }
401
407 public static function _createImportDirectory(): string
408 {
409 global $DIC;
410 $ilias = $DIC['ilias'];
411 $tst_data_dir = ilFileUtils::getDataDir() . "/tst_data";
412 ilFileUtils::makeDir($tst_data_dir);
413
414 if (!is_writable($tst_data_dir)) {
415 $ilias->raiseError("Test Data Directory (" . $tst_data_dir
416 . ") not writeable.", $ilias->error_obj->FATAL);
417 }
418
419 // create test directory (data_dir/tst_data/tst_import)
420 $tst_dir = $tst_data_dir . "/tst_import";
421 ilFileUtils::makeDir($tst_dir);
422 if (!@is_dir($tst_dir)) {
423 $ilias->raiseError("Creation of test import directory failed.", $ilias->error_obj->FATAL);
424 }
425
426 // assert that this is empty and does not contain old data
427 ilFileUtils::delDir($tst_dir, true);
428
429 return $tst_dir;
430 }
431
432 final public function isComplete(ilTestQuestionSetConfig $test_question_set_config): bool
433 {
434 if ($this->getMarkSchema() === null
435 || $this->getMarkSchema()->getMarkSteps() === []) {
436 return false;
437 }
438
439 if (!$test_question_set_config->isQuestionSetConfigured()) {
440 return false;
441 }
442
443 return true;
444 }
445
446 public function saveCompleteStatus(ilTestQuestionSetConfig $test_question_set_config): void
447 {
448 $complete = 0;
449 if ($this->isComplete($test_question_set_config)) {
450 $complete = 1;
451 }
452 if ($this->getTestId() > 0) {
453 $this->db->manipulateF(
454 'UPDATE tst_tests SET complete = %s WHERE test_id = %s',
455 ['text', 'integer'],
456 [$complete, $this->test_id]
457 );
458 }
459 }
460
461 public function saveToDb(bool $properties_only = false): void
462 {
463 if ($this->test_id === -1) {
464 // Create new dataset
465 $next_id = $this->db->nextId('tst_tests');
466
467 $this->db->insert(
468 'tst_tests',
469 [
470 'test_id' => ['integer', $next_id],
471 'obj_fi' => ['integer', $this->getId()],
472 'created' => ['integer', time()],
473 'tstamp' => ['integer', time()],
474 'template_id' => ['integer', $this->getTemplate()]
475 ]
476 );
477
478 $this->test_id = $next_id;
479
480 $this->getMainSettingsRepository()->store(
481 $this->settings_factory->createDefaultMainSettings(),
482 $this->getTestId()
483 );
484 } else {
485 if ($this->evalTotalPersons() > 0) {
486 // reset the finished status of participants if the nr of test passes did change
487 if ($this->getNrOfTries() > 0) {
488 // set all unfinished tests with nr of passes >= allowed passes finished
489 $aresult = $this->db->queryF(
490 "SELECT active_id FROM tst_active WHERE test_fi = %s AND tries >= %s AND submitted = %s",
491 ['integer', 'integer', 'integer'],
492 [$this->getTestId(), $this->getNrOfTries(), 0]
493 );
494 while ($row = $this->db->fetchAssoc($aresult)) {
495 $this->db->manipulateF(
496 "UPDATE tst_active SET submitted = %s, submittimestamp = %s WHERE active_id = %s",
497 ['integer', 'timestamp', 'integer'],
498 [1, date('Y-m-d H:i:s'), $row["active_id"]]
499 );
500 }
501
502 // set all finished tests with nr of passes < allowed passes not finished
503 $aresult = $this->db->queryF(
504 "SELECT active_id FROM tst_active WHERE test_fi = %s AND tries < %s AND submitted = %s",
505 ['integer', 'integer', 'integer'],
506 [$this->getTestId(), $this->getNrOfTries() - 1, 1]
507 );
508 while ($row = $this->db->fetchAssoc($aresult)) {
509 $this->db->manipulateF(
510 "UPDATE tst_active SET submitted = %s, submittimestamp = %s WHERE active_id = %s",
511 ['integer', 'timestamp', 'integer'],
512 [0, null, $row["active_id"]]
513 );
514 }
515 } else {
516 // set all finished tests with nr of passes >= allowed passes not finished
517 $aresult = $this->db->queryF(
518 "SELECT active_id FROM tst_active WHERE test_fi = %s AND submitted = %s",
519 ['integer', 'integer'],
520 [$this->getTestId(), 1]
521 );
522 while ($row = $this->db->fetchAssoc($aresult)) {
523 $this->db->manipulateF(
524 "UPDATE tst_active SET submitted = %s, submittimestamp = %s WHERE active_id = %s",
525 ['integer', 'timestamp', 'integer'],
526 [0, null, $row["active_id"]]
527 );
528 }
529 }
530 }
531 }
532
533 if ($properties_only) {
534 return;
535 }
536
537 if ($this->getQuestionSetType() == self::QUESTION_SET_TYPE_FIXED) {
538 $this->saveQuestionsToDb();
539 }
540
541 $this->marks_repository->storeMarkSchema($this->getMarkSchema());
542 }
543
544 public function saveQuestionsToDb(): void
545 {
546 $this->db->manipulateF(
547 'DELETE FROM tst_test_question WHERE test_fi = %s',
548 ['integer'],
549 [$this->getTestId()]
550 );
551 foreach ($this->questions as $key => $value) {
552 $next_id = $this->db->nextId('tst_test_question');
553 $this->db->insert('tst_test_question', [
554 'test_question_id' => ['integer', $next_id],
555 'test_fi' => ['integer', $this->getTestId()],
556 'question_fi' => ['integer', $value],
557 'sequence' => ['integer', $key],
558 'tstamp' => ['integer', time()]
559 ]);
560 }
561 }
562
566 public function copyQuestions(array $question_ids): void
567 {
568 $copy_count = 0;
569 $question_titles = $this->getQuestionTitles();
570
571 foreach ($question_ids as $id) {
572 $question = assQuestion::instantiateQuestionGUI($id);
573 if ($question) {
574 $title = $question->getObject()->getTitle();
575 $i = 2;
576 while (in_array($title . ' (' . $i . ')', $question_titles)) {
577 $i++;
578 }
579
580 $title .= ' (' . $i . ')';
581
582 $question_titles[] = $title;
583
584 $new_id = $question->getObject()->duplicate(false, $title);
585
586 $clone = assQuestion::instantiateQuestionGUI($new_id);
587 $question = $clone->getObject();
588 $question->setObjId($this->getId());
589 $clone->setObject($question);
590 $clone->getObject()->saveToDb();
591
592 $this->insertQuestion($new_id, true);
593
594 $copy_count++;
595 }
596 }
597 }
598
602 public function getNrOfResultsForPass($active_id, $pass): int
603 {
604 $result = $this->db->queryF(
605 "SELECT test_result_id FROM tst_test_result WHERE active_fi = %s AND pass = %s",
606 ['integer','integer'],
607 [$active_id, $pass]
608 );
609 return $result->numRows();
610 }
611
612 public function loadFromDb(): void
613 {
614 $result = $this->db->queryF(
615 "SELECT test_id FROM tst_tests WHERE obj_fi = %s",
616 ['integer'],
617 [$this->getId()]
618 );
619 if ($result->numRows() === 1) {
620 $data = $this->db->fetchObject($result);
621 $this->setTestId($data->test_id);
622 $this->loadQuestions();
623 }
624 }
625
630 public function loadQuestions(int $active_id = 0, ?int $pass = null): void
631 {
632 $this->questions = [];
633 if ($this->isRandomTest()) {
634 if ($active_id === 0) {
635 $active_id = $this->getActiveIdOfUser($this->user->getId());
636 }
637 if (is_null($pass)) {
638 $pass = self::_getPass($active_id);
639 }
640 $result = $this->db->queryF(
641 'SELECT tst_test_rnd_qst.* '
642 . 'FROM tst_test_rnd_qst, qpl_questions '
643 . 'WHERE tst_test_rnd_qst.active_fi = %s '
644 . 'AND qpl_questions.question_id = tst_test_rnd_qst.question_fi '
645 . 'AND tst_test_rnd_qst.pass = %s '
646 . 'ORDER BY sequence',
647 ['integer', 'integer'],
648 [$active_id, $pass]
649 );
650 } else {
651 $result = $this->db->queryF(
652 'SELECT tst_test_question.* '
653 . 'FROM tst_test_question, qpl_questions '
654 . 'WHERE tst_test_question.test_fi = %s '
655 . 'AND qpl_questions.question_id = tst_test_question.question_fi '
656 . 'ORDER BY sequence',
657 ['integer'],
658 [$this->test_id]
659 );
660 }
661 $index = 1;
662 if ($this->test_id !== -1) {
663 //Omit loading of questions for non-id'ed test
664 while ($data = $this->db->fetchAssoc($result)) {
665 $this->questions[$index++] = $data["question_fi"];
666 }
667 }
668 }
669
670 public function getIntroduction(): string
671 {
672 $page_id = $this->getMainSettings()->getIntroductionSettings()->getIntroductionPageId();
673 return $page_id !== null
674 ? (new ilTestPageGUI('tst', $page_id))->showPage()
675 : '';
676 }
677
678 private function cloneIntroduction(): ?int
679 {
680 $page_id = $this->getMainSettings()->getIntroductionSettings()->getIntroductionPageId();
681 if ($page_id === null) {
682 return null;
683 }
684 return $this->clonePage($page_id);
685 }
686
687 public function getFinalStatement(): string
688 {
689 $page_id = $this->getMainSettings()->getFinishingSettings()->getConcludingRemarksPageId();
690 return $page_id !== null
691 ? (new ilTestPageGUI('tst', $page_id))->showPage()
692 : '';
693 }
694
695 private function cloneConcludingRemarks(): ?int
696 {
697 $page_id = $this->getMainSettings()->getFinishingSettings()->getConcludingRemarksPageId();
698 if ($page_id === null) {
699 return null;
700 }
701 return $this->clonePage($page_id);
702 }
703
704 private function clonePage(int $source_page_id): int
705 {
706 $page_object = new ilTestPage();
707 $page_object->setParentId($this->getId());
708 $new_page_id = $page_object->createPageWithNextId();
709 (new ilTestPage($source_page_id))->copy($new_page_id);
710 return $new_page_id;
711 }
712
716 public function getTestId(): int
717 {
718 return $this->test_id;
719 }
720
721 public function isPostponingEnabled(): bool
722 {
723 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getPostponedQuestionsMoveToEnd();
724 }
725
726 public function isScoreReportingEnabled(): bool
727 {
728 return $this->getScoreSettings()->getResultSummarySettings()->getScoreReporting()->isReportingEnabled();
729 }
730
731 public function getAnswerFeedbackPoints(): bool
732 {
733 return $this->getMainSettings()->getQuestionBehaviourSettings()->getInstantFeedbackPointsEnabled();
734 }
735
736 public function getGenericAnswerFeedback(): bool
737 {
738 return $this->getMainSettings()->getQuestionBehaviourSettings()->getInstantFeedbackGenericEnabled();
739 }
740
741 public function getInstantFeedbackSolution(): bool
742 {
743 return $this->getMainSettings()->getQuestionBehaviourSettings()->getInstantFeedbackSolutionEnabled();
744 }
745
746 public function getCountSystem(): int
747 {
748 return $this->getScoreSettings()->getScoringSettings()->getCountSystem();
749 }
750
754 private static function _getScoreSettingsByActiveId(int $active_id): ScoreSettings
755 {
756 return TestDIC::dic()['settings.scoring.repository']->getFor(
758 );
759 }
760
764 public static function _getCountSystem(int $active_id): int
765 {
766 return self::_getScoreSettingsByActiveId($active_id)->getScoringSettings()->getCountSystem();
767 }
768
772 public function getScoreCutting(): int
773 {
774 return $this->getScoreSettings()->getScoringSettings()->getScoreCutting();
775 }
776
780 public function getPassScoring(): int
781 {
782 return $this->getScoreSettings()->getScoringSettings()->getPassScoring();
783 }
784
788 public static function _getPassScoring(int $active_id): int
789 {
790 return self::_getScoreSettingsByActiveId($active_id)->getScoringSettings()->getPassScoring();
791 }
792
796 public static function _getScoreCutting(int $active_id): bool
797 {
798 return (bool) self::_getScoreSettingsByActiveId($active_id)->getScoringSettings()->getScoreCutting();
799 }
800
801 public function getMarkSchema(): MarkSchema
802 {
803 if ($this->mark_schema === null) {
804 $this->mark_schema = $this->marks_repository->getMarkSchemaFor($this->getTestId());
805 }
806
807 return $this->mark_schema;
808 }
809
811 {
812 $this->marks_repository->storeMarkSchema($mark_schema);
813 $this->mark_schema = null;
814 }
815
816 public function getNrOfTries(): int
817 {
818 return $this->getMainSettings()->getTestBehaviourSettings()->getNumberOfTries();
819 }
820
821 public function isBlockPassesAfterPassedEnabled(): bool
822 {
823 return $this->getMainSettings()->getTestBehaviourSettings()->getBlockAfterPassedEnabled();
824 }
825
826 public function getKioskMode(): bool
827 {
828 return $this->getMainSettings()->getTestBehaviourSettings()->getKioskModeEnabled();
829 }
830
831 public function getShowKioskModeTitle(): bool
832 {
833 return $this->getMainSettings()->getTestBehaviourSettings()->getShowTitleInKioskMode();
834 }
835 public function getShowKioskModeParticipant(): bool
836 {
837 return $this->getMainSettings()->getTestBehaviourSettings()->getShowParticipantNameInKioskMode();
838 }
839
840 public function getUsePreviousAnswers(): bool
841 {
842 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getUsePreviousAnswerAllowed();
843 }
844
845 public function getTitleOutput(): int
846 {
847 return $this->getMainSettings()->getQuestionBehaviourSettings()->getQuestionTitleOutputMode();
848 }
849
850 public function isPreviousSolutionReuseEnabled(): bool
851 {
852 return $this->getUsePreviousAnswers() && $this->user->getPref('tst_use_previous_answers') === '1';
853 }
854
855 public function getProcessingTime(): ?string
856 {
857 return $this->getMainSettings()->getTestBehaviourSettings()->getProcessingTime();
858 }
859
860 private function getProcessingTimeForXML(): string
861 {
862 $processing_time = $this->getMainSettings()->getTestBehaviourSettings()->getProcessingTime();
863 if ($processing_time === null
864 || $processing_time === ''
865 || !preg_match('/(\d{2}):(\d{2}):(\d{2})/is', $processing_time, $matches)
866 ) {
867 return '';
868 }
869
870 return sprintf(
871 "P0Y0M0DT%dH%dM%dS",
872 $matches[1],
873 $matches[2],
874 $matches[3]
875 );
876 }
877
878 public function getProcessingTimeInSeconds(int $active_id = 0): int
879 {
880 $processing_time = $this->getMainSettings()->getTestBehaviourSettings()->getProcessingTime() ?? '';
881 if (preg_match("/(\d{2}):(\d{2}):(\d{2})/", (string) $processing_time, $matches)) {
882 $extratime = $this->getExtraTime($active_id) * 60;
883 return ($matches[1] * 3600) + ($matches[2] * 60) + $matches[3] + $extratime;
884 } else {
885 return 0;
886 }
887 }
888
889 public function getEnableProcessingTime(): bool
890 {
891 return $this->getMainSettings()->getTestBehaviourSettings()->getProcessingTimeEnabled();
892 }
893
894 public function getResetProcessingTime(): bool
895 {
896 return $this->getMainSettings()->getTestBehaviourSettings()->getResetProcessingTime();
897 }
898
899 public function isStartingTimeEnabled(): bool
900 {
901 return $this->getMainSettings()->getAccessSettings()->getStartTimeEnabled();
902 }
903
904 public function getStartingTime(): int
905 {
906 $start_time = $this->getMainSettings()->getAccessSettings()->getStartTime();
907 return $start_time !== null ? $start_time->getTimestamp() : 0;
908 }
909
910 public function isEndingTimeEnabled(): bool
911 {
912 return $this->getMainSettings()->getAccessSettings()->getEndTimeEnabled();
913 }
914
915 public function getEndingTime(): int
916 {
917 $end_time = $this->getMainSettings()->getAccessSettings()->getEndTime();
918 return $end_time !== null ? $end_time->getTimestamp() : 0;
919 }
920
921 public function isPasswordEnabled(): bool
922 {
923 return $this->getMainSettings()->getAccessSettings()->getPasswordEnabled();
924 }
925
926 public function getPassword(): ?string
927 {
928 return $this->getMainSettings()->getAccessSettings()->getPassword();
929 }
930
931 public function removeQuestionsWithResults(array $question_ids): void
932 {
933 $scoring = new TestScoring(
934 $this,
935 $this->user,
936 $this->db,
937 $this->test_result_repository
938 );
939
940 array_walk(
941 $question_ids,
942 fn(int $v, int $k) => $this->removeQuestionWithResults($v, $scoring)
943 );
944 }
945
946 private function removeQuestionWithResults(int $question_id, TestScoring $scoring): void
947 {
948 $question = \assQuestion::instantiateQuestion($question_id);
949
950 $participant_data = new ilTestParticipantData($this->db, $this->lng);
951 $participant_data->load($this->test_id);
952
953 $question->removeAllExistingSolutions();
954 $scoring->removeAllQuestionResults($question_id);
955
956 $this->removeQuestion($question_id);
957 if (!$this->isRandomTest()) {
959 $question_id,
960 $participant_data->getActiveIds(),
961 $this->reindexFixedQuestionOrdering()
962 );
963 }
964
965 $scoring->updatePassAndTestResults($participant_data->getActiveIds());
966 ilLPStatusWrapper::_refreshStatus($this->getId(), $participant_data->getUserIds());
967 $question->delete($question_id);
968
969 if ($this->getTestQuestions() === []) {
971 $object_properties->storePropertyIsOnline(
972 $object_properties->getPropertyIsOnline()->withOffline()
973 );
974 }
975
976 if ($this->logger->isLoggingEnabled()) {
977 $this->logger->logTestAdministrationInteraction(
978 $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
979 $this->getRefId(),
980 $this->user->getId(),
981 TestAdministrationInteractionTypes::QUESTION_REMOVED_IN_CORRECTIONS,
982 [
983 AdditionalInformationGenerator::KEY_QUESTION_TITLE => $question->getTitleForHTMLOutput(),
984 AdditionalInformationGenerator::KEY_QUESTION_TEXT => $question->getQuestion(),
985 AdditionalInformationGenerator::KEY_QUESTION_ID => $question->getId(),
986 AdditionalInformationGenerator::KEY_QUESTION_TYPE => $question->getQuestionType()
987 ]
988 )
989 );
990 }
991 }
992
997 int $question_id,
998 array $active_ids,
999 ilTestReindexedSequencePositionMap $reindexed_sequence_position_map
1000 ): void {
1001 $test_sequence_factory = new ilTestSequenceFactory(
1002 $this,
1003 $this->db,
1004 $this->questionrepository
1005 );
1006
1007 foreach ($active_ids as $active_id) {
1008 $passSelector = new ilTestPassesSelector($this->db, $this);
1009 $passSelector->setActiveId($active_id);
1010
1011 foreach ($passSelector->getExistingPasses() as $pass) {
1012 $test_sequence = $test_sequence_factory->getSequenceByActiveIdAndPass($active_id, $pass);
1013 $test_sequence->loadFromDb();
1014
1015 $test_sequence->removeQuestion($question_id, $reindexed_sequence_position_map);
1016 $test_sequence->saveToDb();
1017 }
1018 }
1019 }
1020
1024 public function removeQuestions(array $question_ids): void
1025 {
1026 foreach ($question_ids as $question_id) {
1027 $this->removeQuestion((int) $question_id);
1028 }
1029
1030 $this->reindexFixedQuestionOrdering();
1031 }
1032
1033 public function removeQuestion(int $question_id): void
1034 {
1035 try {
1036 $question = self::_instanciateQuestion($question_id);
1037 $question_title = $question->getTitleForHTMLOutput();
1038 $question->delete($question_id);
1039 if ($this->logger->isLoggingEnabled()) {
1040 $this->logger->logTestAdministrationInteraction(
1041 $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
1042 $this->getRefId(),
1043 $this->user->getId(),
1044 TestAdministrationInteractionTypes::QUESTION_REMOVED,
1045 [
1046 AdditionalInformationGenerator::KEY_QUESTION_TITLE => $question_title
1047 ]
1048 )
1049 );
1050 }
1051 } catch (InvalidArgumentException $e) {
1052 $this->logger->error($e->getMessage());
1053 $this->logger->error($e->getTraceAsString());
1054 }
1055 }
1056
1065 public function removeTestResultsFromSoapLpAdministration(array $user_ids)
1066 {
1067 $this->removeTestResultsByUserIds($user_ids);
1068
1069 $participantData = new ilTestParticipantData($this->db, $this->lng);
1070 $participantData->setUserIdsFilter($user_ids);
1071 $participantData->load($this->getTestId());
1072
1073 $this->removeTestActives($participantData->getActiveIds());
1074
1075
1076 if ($this->logger->isLoggingEnabled()) {
1077 $this->logger->logTestAdministrationInteraction(
1078 $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
1079 $this->getRefId(),
1080 $this->user->getId(),
1081 TestAdministrationInteractionTypes::PARTICIPANT_DATA_REMOVED,
1082 [
1083 AdditionalInformationGenerator::KEY_USERS => $participantData->getUserIds()
1084 ]
1085 )
1086 );
1087 }
1088 }
1089
1090 public function removeTestResults(ilTestParticipantData $participant_data): void
1091 {
1092 if ($participant_data->getAnonymousActiveIds() !== []) {
1093 $this->removeTestResultsByActiveIds($participant_data->getAnonymousActiveIds());
1094
1095 $user_ids = array_map(
1096 static fn($active_id) => $participant_data->getUserIdByActiveId($active_id),
1097 $participant_data->getAnonymousActiveIds(),
1098 );
1099 $this->participant_repository->removeExtraTimeByUserId($this->getTestId(), $user_ids);
1100 }
1101
1102 if ($participant_data->getUserIds() !== []) {
1103 /* @var ilTestLP $testLP */
1104 $test_lp = ilObjectLP::getInstance($this->getId());
1105 if ($test_lp instanceof ilTestLP) {
1106 $test_lp->setTestObject($this);
1107 $test_lp->resetLPDataForUserIds($participant_data->getUserIds(), false);
1108 }
1109
1110 $this->participant_repository->removeExtraTimeByUserId($this->getTestId(), $participant_data->getUserIds());
1111 }
1112
1113 if ($participant_data->getActiveIds() !== []) {
1114 $this->removeTestActives($participant_data->getActiveIds());
1115
1116 $user_ids = array_map(
1117 static fn($active_id) => $participant_data->getUserIdByActiveId($active_id),
1118 $participant_data->getActiveIds(),
1119 );
1120 $this->participant_repository->removeExtraTimeByUserId($this->getTestId(), $user_ids);
1121 }
1122
1123 if ($this->logger->isLoggingEnabled()) {
1124 $this->logger->logTestAdministrationInteraction(
1125 $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
1126 $this->getRefId(),
1127 $this->user->getId(),
1128 TestAdministrationInteractionTypes::PARTICIPANT_DATA_REMOVED,
1129 [
1130 AdditionalInformationGenerator::KEY_USERS => $participant_data->getUserIds(),
1131 AdditionalInformationGenerator::KEY_ANON_IDS => $participant_data->getAnonymousActiveIds()
1132 ]
1133 )
1134 );
1135 }
1136 }
1137
1138 public function removeTestResultsByUserIds(array $user_ids): void
1139 {
1140 $participantData = new ilTestParticipantData($this->db, $this->lng);
1141 $participantData->setUserIdsFilter($user_ids);
1142 $participantData->load($this->getTestId());
1143
1144 $in_user_ids = $this->db->in('usr_id', $participantData->getUserIds(), false, 'integer');
1145 $this->db->manipulateF(
1146 "DELETE FROM usr_pref WHERE {$in_user_ids} AND keyword = %s",
1147 ['text'],
1148 ['tst_password_' . $this->getTestId()]
1149 );
1150
1151 if ($participantData->getActiveIds() !== []) {
1152 $this->removeTestResultsByActiveIds($participantData->getActiveIds());
1153 }
1154 }
1155
1156 private function removeTestResultsByActiveIds(array $active_ids): void
1157 {
1158 $in_active_ids = $this->db->in('active_fi', $active_ids, false, 'integer');
1159
1160 $this->db->manipulate("DELETE FROM tst_solutions WHERE {$in_active_ids}");
1161 $this->db->manipulate("DELETE FROM tst_qst_solved WHERE {$in_active_ids}");
1162 $this->db->manipulate("DELETE FROM tst_test_result WHERE {$in_active_ids}");
1163 $this->db->manipulate("DELETE FROM tst_pass_result WHERE {$in_active_ids}");
1164 $this->db->manipulate("DELETE FROM tst_result_cache WHERE {$in_active_ids}");
1165 $this->db->manipulate("DELETE FROM tst_sequence WHERE {$in_active_ids}");
1166 $this->db->manipulate("DELETE FROM tst_times WHERE {$in_active_ids}");
1167 $this->db->manipulate(
1169 . ' WHERE ' . $this->db->in('active_id', $active_ids, false, 'integer')
1170 );
1171
1172 if ($this->isRandomTest()) {
1173 $this->db->manipulate("DELETE FROM tst_test_rnd_qst WHERE {$in_active_ids}");
1174 }
1175
1176 $this->test_result_repository->removeTestResults($active_ids, $this->getId());
1177
1178 foreach ($active_ids as $active_id) {
1179 // remove file uploads
1180 if (is_dir(CLIENT_WEB_DIR . "/assessment/tst_" . $this->getTestId() . "/$active_id")) {
1181 ilFileUtils::delDir(CLIENT_WEB_DIR . "/assessment/tst_" . $this->getTestId() . "/$active_id");
1182 }
1183 }
1184 }
1185
1189 public function removeTestActives(array $active_ids): void
1190 {
1191 $IN_activeIds = $this->db->in('active_id', $active_ids, false, 'integer');
1192 $this->db->manipulate("DELETE FROM tst_active WHERE $IN_activeIds");
1193 }
1194
1202 public function questionMoveUp($question_id)
1203 {
1204 // Move a question up in sequence
1205 $result = $this->db->queryF(
1206 "SELECT * FROM tst_test_question WHERE test_fi=%s AND question_fi=%s",
1207 ['integer', 'integer'],
1208 [$this->getTestId(), $question_id]
1209 );
1210 $data = $this->db->fetchObject($result);
1211 if ($data->sequence > 1) {
1212 // OK, it's not the top question, so move it up
1213 $result = $this->db->queryF(
1214 "SELECT * FROM tst_test_question WHERE test_fi=%s AND sequence=%s",
1215 ['integer','integer'],
1216 [$this->getTestId(), $data->sequence - 1]
1217 );
1218 $data_previous = $this->db->fetchObject($result);
1219 // change previous dataset
1220 $this->db->manipulateF(
1221 "UPDATE tst_test_question SET sequence=%s WHERE test_question_id=%s",
1222 ['integer','integer'],
1223 [$data->sequence, $data_previous->test_question_id]
1224 );
1225 // move actual dataset up
1226 $this->db->manipulateF(
1227 "UPDATE tst_test_question SET sequence=%s WHERE test_question_id=%s",
1228 ['integer','integer'],
1229 [$data->sequence - 1, $data->test_question_id]
1230 );
1231 }
1232 $this->loadQuestions();
1233 }
1234
1242 public function questionMoveDown($question_id)
1243 {
1244 $current_question_result = $this->db->queryF(
1245 "SELECT * FROM tst_test_question WHERE test_fi=%s AND question_fi=%s",
1246 ['integer','integer'],
1247 [$this->getTestId(), $question_id]
1248 );
1249 $current_question_data = $this->db->fetchObject($current_question_result);
1250 $next_question_result = $this->db->queryF(
1251 "SELECT * FROM tst_test_question WHERE test_fi=%s AND sequence=%s",
1252 ['integer','integer'],
1253 [$this->getTestId(), $current_question_data->sequence + 1]
1254 );
1255 if ($this->db->numRows($next_question_result) === 1) {
1256 // OK, it's not the last question, so move it down
1257 $next_question_data = $this->db->fetchObject($next_question_result);
1258 // change next dataset
1259 $this->db->manipulateF(
1260 "UPDATE tst_test_question SET sequence=%s WHERE test_question_id=%s",
1261 ['integer','integer'],
1262 [$current_question_data->sequence, $next_question_data->test_question_id]
1263 );
1264 // move actual dataset down
1265 $this->db->manipulateF(
1266 "UPDATE tst_test_question SET sequence=%s WHERE test_question_id=%s",
1267 ['integer','integer'],
1268 [$current_question_data->sequence + 1, $current_question_data->test_question_id]
1269 );
1270 }
1271 $this->loadQuestions();
1272 }
1273
1280 public function duplicateQuestionForTest($question_id): int
1281 {
1282 $question = ilObjTest::_instanciateQuestion($question_id);
1283 $duplicate_id = $question->duplicate(true, '', '', -1, $this->getId());
1284 return $duplicate_id;
1285 }
1286
1287 public function insertQuestion(int $question_id, bool $link_only = false): int
1288 {
1289 if ($link_only) {
1290 $duplicate_id = $question_id;
1291 } else {
1292 $duplicate_id = $this->duplicateQuestionForTest($question_id);
1293 }
1294
1295 // get maximum sequence index in test
1296 $result = $this->db->queryF(
1297 "SELECT MAX(sequence) seq FROM tst_test_question WHERE test_fi=%s",
1298 ['integer'],
1299 [$this->getTestId()]
1300 );
1301 $sequence = 1;
1302
1303 if ($result->numRows() == 1) {
1304 $data = $this->db->fetchObject($result);
1305 $sequence = $data->seq + 1;
1306 }
1307
1308 $next_id = $this->db->nextId('tst_test_question');
1309 $this->db->manipulateF(
1310 "INSERT INTO tst_test_question (test_question_id, test_fi, question_fi, sequence, tstamp) VALUES (%s, %s, %s, %s, %s)",
1311 ['integer', 'integer','integer','integer','integer'],
1312 [$next_id, $this->getTestId(), $duplicate_id, $sequence, time()]
1313 );
1314 // remove test_active entries, because test has changed
1315 $this->db->manipulateF(
1316 "DELETE FROM tst_active WHERE test_fi = %s",
1317 ['integer'],
1318 [$this->getTestId()]
1319 );
1320 $this->loadQuestions();
1321 $this->saveCompleteStatus($this->question_set_config_factory->getQuestionSetConfig());
1322
1323 if ($this->logger->isLoggingEnabled()) {
1324 $this->logger->logTestAdministrationInteraction(
1325 $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
1326 $this->getRefId(),
1327 $this->user->getId(),
1328 TestAdministrationInteractionTypes::QUESTION_ADDED,
1329 [
1330 AdditionalInformationGenerator::KEY_QUESTION_ID => $question_id
1331 ] + (assQuestion::instantiateQuestion($question_id))
1332 ->toLog($this->logger->getAdditionalInformationGenerator())
1333 )
1334 );
1335 }
1336
1337 return $duplicate_id;
1338 }
1339
1340 private function getQuestionTitles(): array
1341 {
1342 $titles = [];
1343 if ($this->getQuestionSetType() === self::QUESTION_SET_TYPE_FIXED) {
1344 $result = $this->db->queryF(
1345 'SELECT qpl_questions.title FROM tst_test_question, qpl_questions '
1346 . 'WHERE tst_test_question.test_fi = %s AND tst_test_question.question_fi = qpl_questions.question_id '
1347 . 'ORDER BY tst_test_question.sequence',
1348 ['integer'],
1349 [$this->getTestId()]
1350 );
1351 while ($row = $this->db->fetchAssoc($result)) {
1352 array_push($titles, $row['title']);
1353 }
1354 }
1355 return $titles;
1356 }
1357
1365 public function getQuestionTitlesAndIndexes(): array
1366 {
1367 $titles = [];
1368 if ($this->getQuestionSetType() == self::QUESTION_SET_TYPE_FIXED) {
1369 $result = $this->db->queryF(
1370 'SELECT qpl_questions.title, qpl_questions.question_id '
1371 . 'FROM tst_test_question, qpl_questions '
1372 . 'WHERE tst_test_question.test_fi = %s AND tst_test_question.question_fi = qpl_questions.question_id '
1373 . 'ORDER BY tst_test_question.sequence',
1374 ['integer'],
1375 [$this->getTestId()]
1376 );
1377 while ($row = $this->db->fetchAssoc($result)) {
1378 $titles[$row['question_id']] = $row["title"];
1379 }
1380 }
1381 return $titles;
1382 }
1383
1384 // fau: testNav - add number parameter (to show if title should not be shown)
1394 public function getQuestionTitle($title, $nr = null, $points = null): string
1395 {
1396 switch ($this->getTitleOutput()) {
1397 case '0':
1398 case '1':
1399 return $title;
1400 break;
1401 case '2':
1402 if (isset($nr)) {
1403 return $this->lng->txt("ass_question") . ' ' . $nr;
1404 }
1405 return $this->lng->txt("ass_question");
1406 break;
1407 case 3:
1408 if (isset($nr)) {
1409 $txt = $this->lng->txt("ass_question") . ' ' . $nr;
1410 } else {
1411 $txt = $this->lng->txt("ass_question");
1412 }
1413 if ($points != '') {
1414 $lngv = $this->lng->txt('points');
1415 if ($points == 1) {
1416 $lngv = $this->lng->txt('point');
1417 }
1418 $txt .= ' - ' . $points . ' ' . $lngv;
1419 }
1420 return $txt;
1421 break;
1422
1423 }
1424 return $this->lng->txt("ass_question");
1425 }
1426 // fau.
1427
1436 public function getQuestionDataset($question_id): object
1437 {
1438 $result = $this->db->queryF(
1439 "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",
1440 ['integer'],
1441 [$question_id]
1442 );
1443 $row = $this->db->fetchObject($result);
1444 return $row;
1445 }
1446
1453 public function &getExistingQuestions($pass = null): array
1454 {
1455 $existing_questions = [];
1456 $active_id = $this->getActiveIdOfUser($this->user->getId());
1457 if ($this->isRandomTest()) {
1458 if (is_null($pass)) {
1459 $pass = 0;
1460 }
1461 $result = $this->db->queryF(
1462 "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",
1463 ['integer','integer'],
1464 [$active_id, $pass]
1465 );
1466 } else {
1467 $result = $this->db->queryF(
1468 "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",
1469 ['integer'],
1470 [$this->getTestId()]
1471 );
1472 }
1473 while ($data = $this->db->fetchObject($result)) {
1474 if ($data->original_id === null) {
1475 continue;
1476 }
1477
1478 array_push($existing_questions, $data->original_id);
1479 }
1480 return $existing_questions;
1481 }
1482
1490 public function getQuestionType($question_id)
1491 {
1492 if ($question_id < 1) {
1493 return -1;
1494 }
1495 $result = $this->db->queryF(
1496 "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",
1497 ['integer'],
1498 [$question_id]
1499 );
1500 if ($result->numRows() == 1) {
1501 $data = $this->db->fetchObject($result);
1502 return $data->type_tag;
1503 } else {
1504 return "";
1505 }
1506 }
1507
1514 public function startWorkingTime($active_id, $pass)
1515 {
1516 $next_id = $this->db->nextId('tst_times');
1517 $affectedRows = $this->db->manipulateF(
1518 "INSERT INTO tst_times (times_id, active_fi, started, finished, pass, tstamp) VALUES (%s, %s, %s, %s, %s, %s)",
1519 ['integer', 'integer', 'timestamp', 'timestamp', 'integer', 'integer'],
1520 [$next_id, $active_id, date("Y-m-d H:i:s"), date("Y-m-d H:i:s"), $pass, time()]
1521 );
1522 return $next_id;
1523 }
1524
1531 public function updateWorkingTime($times_id)
1532 {
1533 $affectedRows = $this->db->manipulateF(
1534 "UPDATE tst_times SET finished = %s, tstamp = %s WHERE times_id = %s",
1535 ['timestamp', 'integer', 'integer'],
1536 [date('Y-m-d H:i:s'), time(), $times_id]
1537 );
1538 }
1539
1546 public function &getWorkedQuestions($active_id, $pass = null): array
1547 {
1548 if (is_null($pass)) {
1549 $result = $this->db->queryF(
1550 "SELECT question_fi FROM tst_solutions WHERE active_fi = %s AND pass = %s GROUP BY question_fi",
1551 ['integer','integer'],
1552 [$active_id, 0]
1553 );
1554 } else {
1555 $result = $this->db->queryF(
1556 "SELECT question_fi FROM tst_solutions WHERE active_fi = %s AND pass = %s GROUP BY question_fi",
1557 ['integer','integer'],
1558 [$active_id, $pass]
1559 );
1560 }
1561 $result_array = [];
1562 while ($row = $this->db->fetchAssoc($result)) {
1563 array_push($result_array, $row["question_fi"]);
1564 }
1565 return $result_array;
1566 }
1567
1576 public function isTestFinishedToViewResults($active_id, $currentpass): bool
1577 {
1578 $num = ilObjTest::lookupPassResultsUpdateTimestamp($active_id, $currentpass);
1579 return ((($currentpass > 0) && ($num == 0)) || $this->isTestFinished($active_id)) ? true : false;
1580 }
1581
1588 public function getAllQuestions($pass = null): array
1589 {
1590 if ($this->isRandomTest()) {
1591 $active_id = $this->getActiveIdOfUser($this->user->getId());
1592 if ($active_id === null) {
1593 return [];
1594 }
1595 $this->loadQuestions($active_id, $pass);
1596 if (count($this->questions) === 0) {
1597 return [];
1598 }
1599 if (is_null($pass)) {
1600 $pass = self::_getPass($active_id);
1601 }
1602 $result = $this->db->queryF(
1603 "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'),
1604 ['integer','integer'],
1605 [$active_id, $pass]
1606 );
1607 } else {
1608 if (count($this->questions) === 0) {
1609 return [];
1610 }
1611 $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'));
1612 }
1613 $result_array = [];
1614 while ($row = $this->db->fetchAssoc($result)) {
1615 $result_array[$row["question_id"]] = $row;
1616 }
1617 return $result_array;
1618 }
1619
1628 public function getActiveIdOfUser($user_id = "", $anonymous_id = ""): ?int
1629 {
1630 if (!$user_id) {
1631 $user_id = $this->user->getId();
1632 }
1633
1634 $tst_access_code = ilSession::get('tst_access_code');
1635 if (is_array($tst_access_code) &&
1636 $this->user->getId() === ANONYMOUS_USER_ID &&
1637 isset($tst_access_code[$this->getTestId()]) &&
1638 $tst_access_code[$this->getTestId()] !== '') {
1639 $result = $this->db->queryF(
1640 'SELECT active_id FROM tst_active WHERE user_fi = %s AND test_fi = %s AND anonymous_id = %s',
1641 ['integer', 'integer', 'text'],
1642 [$user_id, $this->test_id, $tst_access_code[$this->getTestId()]]
1643 );
1644 } elseif ((string) $anonymous_id !== '') {
1645 $result = $this->db->queryF(
1646 'SELECT active_id FROM tst_active WHERE user_fi = %s AND test_fi = %s AND anonymous_id = %s',
1647 ['integer', 'integer', 'text'],
1648 [$user_id, $this->test_id, $anonymous_id]
1649 );
1650 } else {
1651 if ((int) $user_id === ANONYMOUS_USER_ID) {
1652 return null;
1653 }
1654 $result = $this->db->queryF(
1655 'SELECT active_id FROM tst_active WHERE user_fi = %s AND test_fi = %s',
1656 ['integer', 'integer'],
1657 [$user_id, $this->test_id]
1658 );
1659 }
1660
1661 if ($result->numRows()) {
1662 $row = $this->db->fetchAssoc($result);
1663 return (int) $row['active_id'];
1664 }
1665
1666 return null;
1667 }
1668
1669 public static function _getActiveIdOfUser($user_id = "", $test_id = "")
1670 {
1671 global $DIC;
1672 $ilDB = $DIC['ilDB'];
1673 $ilUser = $DIC['ilUser'];
1674
1675 if (!$user_id) {
1676 $user_id = $ilUser->id;
1677 }
1678 if (!$test_id) {
1679 return "";
1680 }
1681 $result = $ilDB->queryF(
1682 "SELECT tst_active.active_id FROM tst_active WHERE user_fi = %s AND test_fi = %s",
1683 ['integer', 'integer'],
1684 [$user_id, $test_id]
1685 );
1686 if ($result->numRows()) {
1687 $row = $ilDB->fetchAssoc($result);
1688 return $row["active_id"];
1689 } else {
1690 return "";
1691 }
1692 }
1693
1700 public function pcArrayShuffle($array): array
1701 {
1702 $keys = array_keys($array);
1703 shuffle($keys);
1704 $result = [];
1705 foreach ($keys as $key) {
1706 $result[$key] = $array[$key];
1707 }
1708 return $result;
1709 }
1710
1717 public function getTestResult(
1718 int $active_id,
1719 ?int $attempt = null,
1720 bool $ordered_sequence = false,
1721 bool $consider_hidden_questions = true,
1722 bool $consider_optional_questions = true
1723 ): array {
1724 $test_result = $this->test_result_repository->getTestResult($active_id);
1725
1726 if ($test_result === null) {
1727 $test_result = $this->test_result_repository->updateTestResultCache($active_id);
1728 }
1729
1730 if ($attempt === null) {
1731 $attempt = $test_result->getAttempt();
1732 }
1733
1734 $test_sequence_factory = new ilTestSequenceFactory($this, $this->db, $this->questionrepository);
1735 $test_sequence = $test_sequence_factory->getSequenceByActiveIdAndPass($active_id, $attempt);
1736
1737 $test_sequence->setConsiderHiddenQuestionsEnabled($consider_hidden_questions);
1738 $test_sequence->setConsiderOptionalQuestionsEnabled($consider_optional_questions);
1739
1740 $test_sequence->loadFromDb();
1741 $test_sequence->loadQuestions();
1742
1743 if ($ordered_sequence) {
1744 $sequence = $test_sequence->getOrderedSequenceQuestions();
1745 } else {
1746 $sequence = $test_sequence->getUserSequenceQuestions();
1747 }
1748
1749 $arr_results = [];
1750
1751 $query = "
1752 SELECT
1753 tst_test_result.question_fi,
1754 tst_test_result.points reached,
1755 tst_test_result.answered answered,
1756 tst_manual_fb.finalized_evaluation finalized_evaluation
1757
1758 FROM tst_test_result
1759
1760 LEFT JOIN tst_solutions
1761 ON tst_solutions.active_fi = tst_test_result.active_fi
1762 AND tst_solutions.question_fi = tst_test_result.question_fi
1763
1764 LEFT JOIN tst_manual_fb
1765 ON tst_test_result.active_fi = tst_manual_fb.active_fi
1766 AND tst_test_result.question_fi = tst_manual_fb.question_fi
1767
1768 WHERE tst_test_result.active_fi = %s
1769 AND tst_test_result.pass = %s
1770 ";
1771
1772 $solutionresult = $this->db->queryF(
1773 $query,
1774 ['integer', 'integer'],
1775 [$active_id, $attempt]
1776 );
1777
1778 while ($row = $this->db->fetchAssoc($solutionresult)) {
1779 $arr_results[ $row['question_fi'] ] = $row;
1780 }
1781
1782 $result = $this->db->query(
1783 'SELECT qpl_questions.*, qpl_qst_type.type_tag, qpl_sol_sug.question_fi has_sug_sol' . PHP_EOL
1784 . 'FROM qpl_qst_type, qpl_questions' . PHP_EOL
1785 . 'LEFT JOIN qpl_sol_sug' . PHP_EOL
1786 . 'ON qpl_sol_sug.question_fi = qpl_questions.question_id' . PHP_EOL
1787 . 'WHERE qpl_qst_type.question_type_id = qpl_questions.question_type_fi' . PHP_EOL
1788 . 'AND ' . $this->db->in('qpl_questions.question_id', $sequence, false, 'integer')
1789 );
1790
1791 $unordered = [];
1792 $key = 1;
1793 while ($row = $this->db->fetchAssoc($result)) {
1794 if (!isset($arr_results[ $row['question_id'] ])) {
1795 $percentvalue = 0.0;
1796 } else {
1797 $percentvalue = (
1798 $row['points'] ? $arr_results[$row['question_id']]['reached'] / $row['points'] : 0
1799 );
1800 }
1801 if ($percentvalue < 0) {
1802 $percentvalue = 0.0;
1803 }
1804
1805 $data = [
1806 'nr' => $key,
1807 'title' => ilLegacyFormElementsUtil::prepareFormOutput($row['title']),
1808 'max' => round($row['points'], 2),
1809 'reached' => round($arr_results[$row['question_id']]['reached'] ?? 0, 2),
1810 'percent' => sprintf('%2.2f ', ($percentvalue) * 100) . '%',
1811 'solution' => ($row['has_sug_sol']) ? assQuestion::_getSuggestedSolutionOutput($row['question_id']) : '',
1812 'type' => $row['type_tag'],
1813 'qid' => $row['question_id'],
1814 'original_id' => $row['original_id'],
1815 'workedthrough' => isset($arr_results[$row['question_id']]) ? 1 : 0,
1816 'answered' => $arr_results[$row['question_id']]['answered'] ?? 0,
1817 'finalized_evaluation' => $arr_results[$row['question_id']]['finalized_evaluation'] ?? 0,
1818 ];
1819
1820 $unordered[ $row['question_id'] ] = $data;
1821 $key++;
1822 }
1823
1824 $pass_max = 0;
1825 $pass_reached = 0;
1826
1827 $found = [];
1828 foreach ($sequence as $qid) {
1829 // building pass point sums based on prepared data
1830 // for question that exists in users qst sequence
1831 $pass_max += round($unordered[$qid]['max'], 2);
1832 $pass_reached += round($unordered[$qid]['reached'], 2);
1833 $found[] = $unordered[$qid];
1834 }
1835
1836 if ($this->getScoreCutting() == 1) {
1837 if ($pass_reached < 0) {
1838 $pass_reached = 0;
1839 }
1840 }
1841
1842 $found['pass']['total_max_points'] = $pass_max;
1843 $found['pass']['total_reached_points'] = $pass_reached;
1844 $found['pass']['percent'] = ($pass_max > 0) ? $pass_reached / $pass_max : 0;
1845 $found['pass']['num_workedthrough'] = count($arr_results);
1846 $found['pass']['num_questions_total'] = count($unordered);
1847
1848 $found['test']['total_max_points'] = $test_result->getMaxPoints();
1849 $found['test']['total_reached_points'] = $test_result->getReachedPoints();
1850 $found['test']['result_pass'] = $attempt;
1851 $found['test']['result_tstamp'] = $test_result->getTimestamp();
1852 $found['test']['passed'] = $test_result->isPassed();
1853
1854 return $found;
1855 }
1856
1863 public function evalTotalPersons(): int
1864 {
1865 $result = $this->db->queryF(
1866 'SELECT COUNT(active_id) total FROM tst_active WHERE test_fi = %s',
1867 ['integer'],
1868 [$this->getTestId()]
1869 );
1870 $row = $this->db->fetchAssoc($result);
1871 return $row['total'];
1872 }
1873
1881 {
1882 $result = $this->db->queryF(
1883 "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",
1884 ['integer','integer'],
1885 [$this->getTestId(), $user_id]
1886 );
1887 $time = 0;
1888 while ($row = $this->db->fetchAssoc($result)) {
1889 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["started"], $matches);
1890 $epoch_1 = mktime(
1891 (int) $matches[4],
1892 (int) $matches[5],
1893 (int) $matches[6],
1894 (int) $matches[2],
1895 (int) $matches[3],
1896 (int) $matches[1]
1897 );
1898 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["finished"], $matches);
1899 $epoch_2 = mktime(
1900 (int) $matches[4],
1901 (int) $matches[5],
1902 (int) $matches[6],
1903 (int) $matches[2],
1904 (int) $matches[3],
1905 (int) $matches[1]
1906 );
1907 $time += ($epoch_2 - $epoch_1);
1908 }
1909 return $time;
1910 }
1911
1919 {
1920 return $this->_getCompleteWorkingTimeOfParticipants($this->getTestId());
1921 }
1922
1930 public function &_getCompleteWorkingTimeOfParticipants($test_id): array
1931 {
1932 $result = $this->db->queryF(
1933 "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",
1934 ['integer'],
1935 [$test_id]
1936 );
1937 $time = 0;
1938 $times = [];
1939 while ($row = $this->db->fetchAssoc($result)) {
1940 if (!array_key_exists($row["active_fi"], $times)) {
1941 $times[$row["active_fi"]] = 0;
1942 }
1943 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["started"], $matches);
1944 $epoch_1 = mktime(
1945 (int) $matches[4],
1946 (int) $matches[5],
1947 (int) $matches[6],
1948 (int) $matches[2],
1949 (int) $matches[3],
1950 (int) $matches[1]
1951 );
1952 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["finished"], $matches);
1953 $epoch_2 = mktime(
1954 (int) $matches[4],
1955 (int) $matches[5],
1956 (int) $matches[6],
1957 (int) $matches[2],
1958 (int) $matches[3],
1959 (int) $matches[1]
1960 );
1961 $times[$row["active_fi"]] += ($epoch_2 - $epoch_1);
1962 }
1963 return $times;
1964 }
1965
1972 public function getCompleteWorkingTimeOfParticipant($active_id): int
1973 {
1974 $result = $this->db->queryF(
1975 "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",
1976 ['integer','integer'],
1977 [$this->getTestId(), $active_id]
1978 );
1979 $time = 0;
1980 while ($row = $this->db->fetchAssoc($result)) {
1981 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["started"], $matches);
1982 $epoch_1 = mktime(
1983 (int) $matches[4],
1984 (int) $matches[5],
1985 (int) $matches[6],
1986 (int) $matches[2],
1987 (int) $matches[3],
1988 (int) $matches[1]
1989 );
1990 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["finished"], $matches);
1991 $epoch_2 = mktime(
1992 (int) $matches[4],
1993 (int) $matches[5],
1994 (int) $matches[6],
1995 (int) $matches[2],
1996 (int) $matches[3],
1997 (int) $matches[1]
1998 );
1999 $time += ($epoch_2 - $epoch_1);
2000 }
2001 return $time;
2002 }
2003
2007 public function getWorkingTimeOfParticipantForPass(int $active_id, int $pass): int
2008 {
2009 return $this->test_result_repository->fetchWorkingTime($active_id, $pass);
2010 }
2011
2015 public function evalStatistical($active_id): array
2016 {
2017 $pass = ilObjTest::_getResultPass($active_id);
2018 $test_result = &$this->getTestResult($active_id, $pass);
2019 $result = $this->db->queryF(
2020 "SELECT tst_times.* FROM tst_active, tst_times WHERE tst_active.active_id = %s AND tst_active.active_id = tst_times.active_fi",
2021 ['integer'],
2022 [$active_id]
2023 );
2024 $times = [];
2025 $first_visit = 0;
2026 $last_visit = 0;
2027 while ($row = $this->db->fetchObject($result)) {
2028 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row->started, $matches);
2029 $epoch_1 = mktime(
2030 (int) $matches[4],
2031 (int) $matches[5],
2032 (int) $matches[6],
2033 (int) $matches[2],
2034 (int) $matches[3],
2035 (int) $matches[1]
2036 );
2037 if (!$first_visit) {
2038 $first_visit = $epoch_1;
2039 }
2040 if ($epoch_1 < $first_visit) {
2041 $first_visit = $epoch_1;
2042 }
2043 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row->finished, $matches);
2044 $epoch_2 = mktime(
2045 (int) $matches[4],
2046 (int) $matches[5],
2047 (int) $matches[6],
2048 (int) $matches[2],
2049 (int) $matches[3],
2050 (int) $matches[1]
2051 );
2052 if (!$last_visit) {
2053 $last_visit = $epoch_2;
2054 }
2055 if ($epoch_2 > $last_visit) {
2056 $last_visit = $epoch_2;
2057 }
2058 $times[$row->active_fi] += ($epoch_2 - $epoch_1);
2059 }
2060 $max_time = 0;
2061 foreach ($times as $key => $value) {
2062 $max_time += $value;
2063 }
2064 if ((!$test_result["test"]["total_reached_points"]) or (!$test_result["test"]["total_max_points"])) {
2065 $percentage = 0.0;
2066 } else {
2067 $percentage = ($test_result["test"]["total_reached_points"] / $test_result["test"]["total_max_points"]) * 100.0;
2068 if ($percentage < 0) {
2069 $percentage = 0.0;
2070 }
2071 }
2072 $mark_obj = $this->getMarkSchema()->getMatchingMark($percentage);
2073 $first_date = getdate($first_visit);
2074 $last_date = getdate($last_visit);
2075 $qworkedthrough = 0;
2076 foreach ($test_result as $key => $value) {
2077 if (preg_match("/\d+/", $key)) {
2078 $qworkedthrough += $value["workedthrough"];
2079 }
2080 }
2081 if (!$qworkedthrough) {
2082 $atimeofwork = 0;
2083 } else {
2084 $atimeofwork = $max_time / $qworkedthrough;
2085 }
2086
2087 $result_mark = "";
2088 $passed = "";
2089
2090 if ($mark_obj !== null) {
2091 $result_mark = $mark_obj->getShortName();
2092
2093 if ($mark_obj->getPassed()) {
2094 $passed = 1;
2095 } else {
2096 $passed = 0;
2097 }
2098 }
2099 $percent_worked_through = 0;
2100 if (count($this->questions)) {
2101 $percent_worked_through = $qworkedthrough / count($this->questions);
2102 }
2103 $result_array = [
2104 "qworkedthrough" => $qworkedthrough,
2105 "qmax" => count($this->questions),
2106 "pworkedthrough" => $percent_worked_through,
2107 "timeofwork" => $max_time,
2108 "atimeofwork" => $atimeofwork,
2109 "firstvisit" => $first_date,
2110 "lastvisit" => $last_date,
2111 "resultspoints" => $test_result["test"]["total_reached_points"],
2112 "maxpoints" => $test_result["test"]["total_max_points"],
2113 "resultsmarks" => $result_mark,
2114 "passed" => $passed,
2115 "distancemedian" => "0"
2116 ];
2117 foreach ($test_result as $key => $value) {
2118 if (preg_match("/\d+/", $key)) {
2119 $result_array[$key] = $value;
2120 }
2121 }
2122 return $result_array;
2123 }
2124
2132 public function getTotalPointsPassedArray(): array
2133 {
2134 $totalpoints_array = [];
2135 $all_users = $this->evalTotalParticipantsArray();
2136 foreach ($all_users as $active_id => $user_name) {
2137 $test_result = &$this->getTestResult($active_id);
2138 $reached = $test_result["test"]["total_reached_points"];
2139 $total = $test_result["test"]["total_max_points"];
2140 $percentage = $total != 0 ? $reached / $total : 0;
2141 $mark = $this->getMarkSchema()->getMatchingMark($percentage * 100.0);
2142
2143 if ($mark !== null && $mark->getPassed()) {
2144 array_push($totalpoints_array, $test_result["test"]["total_reached_points"]);
2145 }
2146 }
2147 return $totalpoints_array;
2148 }
2149
2155 public function getParticipants(): array
2156 {
2157 $result = $this->db->queryF(
2158 "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",
2159 ['integer'],
2160 [$this->getTestId()]
2161 );
2162 $persons_array = [];
2163 while ($row = $this->db->fetchAssoc($result)) {
2164 $name = $this->lng->txt("anonymous");
2165 $fullname = $this->lng->txt("anonymous");
2166 $login = "";
2167 if (!$this->getAnonymity()) {
2168 if (strlen($row["firstname"] . $row["lastname"] . $row["title"]) == 0) {
2169 $name = $this->lng->txt("deleted_user");
2170 $fullname = $this->lng->txt("deleted_user");
2171 $login = $this->lng->txt("unknown");
2172 } else {
2173 $login = $row["login"];
2174 if ($row["usr_id"] == ANONYMOUS_USER_ID) {
2175 $name = $this->lng->txt("anonymous");
2176 $fullname = $this->lng->txt("anonymous");
2177 } else {
2178 $name = trim($row["lastname"] . ", " . $row["firstname"] . " " . $row["title"]);
2179 $fullname = trim($row["title"] . " " . $row["firstname"] . " " . $row["lastname"]);
2180 }
2181 }
2182 }
2183 $persons_array[$row["active_id"]] = [
2184 "name" => $name,
2185 "fullname" => $fullname,
2186 "login" => $login
2187 ];
2188 }
2189 return $persons_array;
2190 }
2191
2192 public function evalTotalPersonsArray(string $name_sort_order = 'asc'): array
2193 {
2194 $result = $this->db->queryF(
2195 "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),
2196 ['integer'],
2197 [$this->getTestId()]
2198 );
2199 $persons_array = [];
2200 while ($row = $this->db->fetchAssoc($result)) {
2201 if ($this->getAccessFilteredParticipantList() && !$this->getAccessFilteredParticipantList()->isActiveIdInList($row["active_id"])) {
2202 continue;
2203 }
2204
2205 if ($this->getAnonymity()) {
2206 $persons_array[$row["active_id"]] = $this->lng->txt("anonymous");
2207 } else {
2208 if (strlen($row["firstname"] . $row["lastname"] . $row["title"]) == 0) {
2209 $persons_array[$row["active_id"]] = $this->lng->txt("deleted_user");
2210 } else {
2211 if ($row["user_fi"] == ANONYMOUS_USER_ID) {
2212 $persons_array[$row["active_id"]] = $row["lastname"];
2213 } else {
2214 $persons_array[$row["active_id"]] = trim($row["lastname"] . ", " . $row["firstname"] . " " . $row["title"]);
2215 }
2216 }
2217 }
2218 }
2219 return $persons_array;
2220 }
2221
2222 public function evalTotalParticipantsArray(string $name_sort_order = 'asc'): array
2223 {
2224 $result = $this->db->queryF(
2225 'SELECT tst_active.user_fi, tst_active.active_id, usr_data.login, '
2226 . 'usr_data.firstname, usr_data.lastname, usr_data.title FROM tst_active '
2227 . 'LEFT JOIN usr_data ON tst_active.user_fi = usr_data.usr_id '
2228 . 'WHERE tst_active.test_fi = %s '
2229 . 'ORDER BY usr_data.lastname ' . strtoupper($name_sort_order),
2230 ['integer'],
2231 [$this->getTestId()]
2232 );
2233 $persons_array = [];
2234 while ($row = $this->db->fetchAssoc($result)) {
2235 if ($this->getAnonymity()) {
2236 $persons_array[$row['active_id']] = ['name' => $this->lng->txt("anonymous")];
2237 } else {
2238 if (strlen($row['firstname'] . $row['lastname'] . $row["title"]) == 0) {
2239 $persons_array[$row['active_id']] = ['name' => $this->lng->txt('deleted_user')];
2240 } else {
2241 if ($row['user_fi'] == ANONYMOUS_USER_ID) {
2242 $persons_array[$row['active_id']] = ['name' => $row['lastname']];
2243 } else {
2244 $persons_array[$row['active_id']] = [
2245 'name' => trim($row['lastname'] . ', ' . $row['firstname']
2246 . ' ' . $row['title']),
2247 'login' => $row['login']
2248 ];
2249 }
2250 }
2251 }
2252 }
2253 return $persons_array;
2254 }
2255
2256 public function getQuestionsOfTest(int $active_id): array
2257 {
2258 if ($this->isRandomTest()) {
2259 $this->db->setLimit($this->getQuestionCount(), 0);
2260 $result = $this->db->queryF(
2261 'SELECT tst_test_rnd_qst.sequence, tst_test_rnd_qst.question_fi, '
2262 . 'tst_test_rnd_qst.pass, qpl_questions.points '
2263 . 'FROM tst_test_rnd_qst, qpl_questions '
2264 . 'WHERE tst_test_rnd_qst.question_fi = qpl_questions.question_id '
2265 . 'AND tst_test_rnd_qst.active_fi = %s ORDER BY tst_test_rnd_qst.sequence',
2266 ['integer'],
2267 [$active_id]
2268 );
2269 } else {
2270 $result = $this->db->queryF(
2271 'SELECT tst_test_question.sequence, tst_test_question.question_fi, '
2272 . 'qpl_questions.points '
2273 . 'FROM tst_test_question, tst_active, qpl_questions '
2274 . 'WHERE tst_test_question.question_fi = qpl_questions.question_id '
2275 . 'AND tst_active.active_id = %s AND tst_active.test_fi = tst_test_question.test_fi',
2276 ['integer'],
2277 [$active_id]
2278 );
2279 }
2280 $qtest = [];
2281 if ($result->numRows()) {
2282 while ($row = $this->db->fetchAssoc($result)) {
2283 array_push($qtest, $row);
2284 }
2285 }
2286 return $qtest;
2287 }
2288
2289 public function getQuestionsOfPass(int $active_id, int $pass): array
2290 {
2291 if ($this->isRandomTest()) {
2292 $this->db->setLimit($this->getQuestionCount(), 0);
2293 $result = $this->db->queryF(
2294 'SELECT tst_test_rnd_qst.sequence, tst_test_rnd_qst.question_fi, '
2295 . 'qpl_questions.points '
2296 . 'FROM tst_test_rnd_qst, qpl_questions '
2297 . 'WHERE tst_test_rnd_qst.question_fi = qpl_questions.question_id '
2298 . 'AND tst_test_rnd_qst.active_fi = %s AND tst_test_rnd_qst.pass = %s '
2299 . 'ORDER BY tst_test_rnd_qst.sequence',
2300 ['integer', 'integer'],
2301 [$active_id, $pass]
2302 );
2303 } else {
2304 $result = $this->db->queryF(
2305 'SELECT tst_test_question.sequence, tst_test_question.question_fi, '
2306 . 'qpl_questions.points '
2307 . 'FROM tst_test_question, tst_active, qpl_questions '
2308 . 'WHERE tst_test_question.question_fi = qpl_questions.question_id '
2309 . 'AND tst_active.active_id = %s AND tst_active.test_fi = tst_test_question.test_fi',
2310 ['integer'],
2311 [$active_id]
2312 );
2313 }
2314 $qpass = [];
2315 if ($result->numRows()) {
2316 while ($row = $this->db->fetchAssoc($result)) {
2317 array_push($qpass, $row);
2318 }
2319 }
2320 return $qpass;
2321 }
2322
2324 {
2325 return $this->access_filtered_participant_list;
2326 }
2327
2328 public function setAccessFilteredParticipantList(ilTestParticipantList $access_filtered_participant_list): void
2329 {
2330 $this->access_filtered_participant_list = $access_filtered_participant_list;
2331 }
2332
2334 {
2335 $list = new ilTestParticipantList($this, $this->user, $this->lng, $this->db);
2336 $list->initializeFromDbRows($this->getTestParticipants());
2337
2338 return $list->getAccessFilteredList(
2339 $this->participant_access_filter->getAccessStatisticsUserFilter($this->getRefId())
2340 );
2341 }
2342
2346 public function getAnonOnlyParticipantIds(): array
2347 {
2348 $list = new ilTestParticipantList($this, $this->user, $this->lng, $this->db);
2349 $list->initializeFromDbRows($this->getTestParticipants());
2350 if ($this->getAnonymity()) {
2351 return $list->getAllUserIds();
2352 }
2353 return $list->getAccessFilteredList(
2354 $this->participant_access_filter->getAnonOnlyParticipantsUserFilter($this->getRefId())
2355 )->getAllUserIds();
2356 }
2357
2359 {
2360 return (new ilTestEvaluationFactory($this->db, $this))
2361 ->getEvaluationData();
2362 }
2363
2364 public function getQuestionCountAndPointsForPassOfParticipant(int $active_id, int $pass): array
2365 {
2366 $question_set_type = $this->lookupQuestionSetTypeByActiveId($active_id);
2367
2368 switch ($question_set_type) {
2370 $res = $this->db->queryF(
2371 "
2372 SELECT tst_test_rnd_qst.pass,
2373 COUNT(tst_test_rnd_qst.question_fi) qcount,
2374 SUM(qpl_questions.points) qsum
2375
2376 FROM tst_test_rnd_qst,
2377 qpl_questions
2378
2379 WHERE tst_test_rnd_qst.question_fi = qpl_questions.question_id
2380 AND tst_test_rnd_qst.active_fi = %s
2381 AND pass = %s
2382
2383 GROUP BY tst_test_rnd_qst.active_fi,
2384 tst_test_rnd_qst.pass
2385 ",
2386 ['integer', 'integer'],
2387 [$active_id, $pass]
2388 );
2389 break;
2390
2392 $res = $this->db->queryF(
2393 "
2394 SELECT COUNT(tst_test_question.question_fi) qcount,
2395 SUM(qpl_questions.points) qsum
2396
2397 FROM tst_test_question,
2398 qpl_questions,
2399 tst_active
2400
2401 WHERE tst_test_question.question_fi = qpl_questions.question_id
2402 AND tst_test_question.test_fi = tst_active.test_fi
2403 AND tst_active.active_id = %s
2404
2405 GROUP BY tst_test_question.test_fi
2406 ",
2407 ['integer'],
2408 [$active_id]
2409 );
2410 break;
2411
2412 default:
2413 throw new ilTestException("not supported question set type: $question_set_type");
2414 }
2415
2416 $row = $this->db->fetchAssoc($res);
2417
2418 if (is_array($row)) {
2419 return ["count" => $row["qcount"], "points" => $row["qsum"]];
2420 }
2421
2422 return ["count" => 0, "points" => 0];
2423 }
2424
2425 public function getCompleteEvaluationData($filterby = '', $filtertext = ''): ilTestEvaluationData
2426 {
2427 $data = $this->getUnfilteredEvaluationData();
2428 $data->setFilter($filterby, $filtertext);
2429 return $data;
2430 }
2431
2443 public function buildName(
2444 ?int $user_id,
2445 ?string $firstname,
2446 ?string $lastname
2447 ): string {
2448 if ($user_id === null
2449 || $firstname . $lastname === '') {
2450 return $this->lng->txt('deleted_user');
2451 }
2452
2453 if ($this->getAnonymity()) {
2454 return $this->lng->txt('anonymous');
2455 }
2456
2457 if ($user_id == ANONYMOUS_USER_ID) {
2458 return $lastname;
2459 }
2460
2461 return trim($lastname . ', ' . $firstname);
2462 }
2463
2464 public function evalTotalStartedAverageTime(?array $active_ids_to_filter = null): int
2465 {
2466 $query = "SELECT tst_times.* FROM tst_active, tst_times WHERE tst_active.test_fi = %s AND tst_active.active_id = tst_times.active_fi";
2467
2468 if ($active_ids_to_filter !== null && $active_ids_to_filter !== []) {
2469 $query .= " AND " . $this->db->in('active_id', $active_ids_to_filter, false, 'integer');
2470 }
2471
2472 $result = $this->db->queryF($query, ['integer'], [$this->getTestId()]);
2473 $times = [];
2474 while ($row = $this->db->fetchObject($result)) {
2475 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row->started, $matches);
2476 $epoch_1 = mktime(
2477 (int) $matches[4],
2478 (int) $matches[5],
2479 (int) $matches[6],
2480 (int) $matches[2],
2481 (int) $matches[3],
2482 (int) $matches[1]
2483 );
2484 preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row->finished, $matches);
2485 $epoch_2 = mktime(
2486 (int) $matches[4],
2487 (int) $matches[5],
2488 (int) $matches[6],
2489 (int) $matches[2],
2490 (int) $matches[3],
2491 (int) $matches[1]
2492 );
2493 if (isset($times[$row->active_fi])) {
2494 $times[$row->active_fi] += ($epoch_2 - $epoch_1);
2495 } else {
2496 $times[$row->active_fi] = ($epoch_2 - $epoch_1);
2497 }
2498 }
2499 $max_time = 0;
2500 $counter = 0;
2501 foreach ($times as $value) {
2502 $max_time += $value;
2503 $counter++;
2504 }
2505 if ($counter === 0) {
2506 return 0;
2507 }
2508 return (int) round($max_time / $counter);
2509 }
2510
2518 bool $use_object_id = false,
2519 ?bool $equal_points = false,
2520 bool $could_be_offline = false,
2521 bool $show_path = false,
2522 bool $with_questioncount = false,
2523 string $permission = 'read'
2524 ): array {
2525 return ilObjQuestionPool::_getAvailableQuestionpools(
2526 $use_object_id,
2527 $equal_points ?? false,
2528 $could_be_offline,
2529 $show_path,
2530 $with_questioncount,
2531 $permission
2532 );
2533 }
2534
2541 public function getImagePath(): string
2542 {
2543 return CLIENT_WEB_DIR . "/assessment/" . $this->getId() . "/images/";
2544 }
2545
2552 public function getImagePathWeb()
2553 {
2554 $webdir = ilFileUtils::removeTrailingPathSeparators(CLIENT_WEB_DIR) . "/assessment/" . $this->getId() . "/images/";
2555 return str_replace(
2556 ilFileUtils::removeTrailingPathSeparators(ILIAS_ABSOLUTE_PATH),
2558 $webdir
2559 );
2560 }
2561
2570 public function createQuestionGUI($question_type, $question_id = -1): ?assQuestionGUI
2571 {
2572 if ((!$question_type) and ($question_id > 0)) {
2573 $question_type = $this->getQuestionType($question_id);
2574 }
2575
2576 if (!strlen($question_type)) {
2577 return null;
2578 }
2579
2580 if ($question_id > 0) {
2581 $question_gui = assQuestion::instantiateQuestionGUI($question_id);
2582 } else {
2583 $question_type_gui = $question_type . 'GUI';
2584 $question_gui = new $question_type_gui();
2585 }
2586
2587 return $question_gui;
2588 }
2589
2596 public static function _instanciateQuestion($question_id): ?assQuestion
2597 {
2598 if (strcmp((string) $question_id, "") !== 0) {
2599 return assQuestion::instantiateQuestion((int) $question_id);
2600 }
2601
2602 return null;
2603 }
2604
2609 public function moveQuestions(array $move_questions, int $target_index, int $insert_mode): void
2610 {
2611 $this->questions = array_values($this->questions);
2612 $array_pos = array_search($target_index, $this->questions);
2613 if ($insert_mode == 0) {
2614 $part1 = array_slice($this->questions, 0, $array_pos);
2615 $part2 = array_slice($this->questions, $array_pos);
2616 } elseif ($insert_mode == 1) {
2617 $part1 = array_slice($this->questions, 0, $array_pos + 1);
2618 $part2 = array_slice($this->questions, $array_pos + 1);
2619 }
2620 foreach ($move_questions as $question_id) {
2621 if (!(array_search($question_id, $part1) === false)) {
2622 unset($part1[array_search($question_id, $part1)]);
2623 }
2624 if (!(array_search($question_id, $part2) === false)) {
2625 unset($part2[array_search($question_id, $part2)]);
2626 }
2627 }
2628 $part1 = array_values($part1);
2629 $part2 = array_values($part2);
2630 $new_array = array_values(array_merge($part1, $move_questions, $part2));
2631 $this->questions = [];
2632 $counter = 1;
2633 foreach ($new_array as $question_id) {
2634 $this->questions[$counter] = $question_id;
2635 $counter++;
2636 }
2637 $this->saveQuestionsToDb();
2638
2639 if ($this->logger->isLoggingEnabled()) {
2640 $this->logger->logTestAdministrationInteraction(
2641 $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
2642 $this->getRefId(),
2643 $this->user->getId(),
2644 TestAdministrationInteractionTypes::QUESTION_MOVED,
2645 [
2646 AdditionalInformationGenerator::KEY_QUESTION_ORDER => $this->questions
2647 ]
2648 )
2649 );
2650 }
2651 }
2652
2653
2661 public function startingTimeReached(): bool
2662 {
2663 if ($this->isStartingTimeEnabled() && $this->getStartingTime() != 0) {
2664 $now = time();
2665 if ($now < $this->getStartingTime()) {
2666 return false;
2667 }
2668 }
2669 return true;
2670 }
2671
2679 public function endingTimeReached(): bool
2680 {
2681 if ($this->isEndingTimeEnabled() && $this->getEndingTime() != 0) {
2682 $now = time();
2683 if ($now > $this->getEndingTime()) {
2684 return true;
2685 }
2686 }
2687 return false;
2688 }
2689
2695 public function getAvailableQuestions($arr_filter, $completeonly = 0): array
2696 {
2697 $available_pools = array_keys(ilObjQuestionPool::_getAvailableQuestionpools(true, false, false, false, false));
2698 $available = "";
2699 if (count($available_pools)) {
2700 $available = " AND " . $this->db->in('qpl_questions.obj_fi', $available_pools, false, 'integer');
2701 } else {
2702 return [];
2703 }
2704 if ($completeonly) {
2705 $available .= " AND qpl_questions.complete = " . $this->db->quote("1", 'text');
2706 }
2707
2708 $where = "";
2709 if (is_array($arr_filter)) {
2710 if (array_key_exists('title', $arr_filter) && strlen($arr_filter['title'])) {
2711 $where .= " AND " . $this->db->like('qpl_questions.title', 'text', "%%" . $arr_filter['title'] . "%%");
2712 }
2713 if (array_key_exists('description', $arr_filter) && strlen($arr_filter['description'])) {
2714 $where .= " AND " . $this->db->like('qpl_questions.description', 'text', "%%" . $arr_filter['description'] . "%%");
2715 }
2716 if (array_key_exists('author', $arr_filter) && strlen($arr_filter['author'])) {
2717 $where .= " AND " . $this->db->like('qpl_questions.author', 'text', "%%" . $arr_filter['author'] . "%%");
2718 }
2719 if (array_key_exists('type', $arr_filter) && strlen($arr_filter['type'])) {
2720 $where .= " AND qpl_qst_type.type_tag = " . $this->db->quote($arr_filter['type'], 'text');
2721 }
2722 if (array_key_exists('qpl', $arr_filter) && strlen($arr_filter['qpl'])) {
2723 $where .= " AND " . $this->db->like('object_data.title', 'text', "%%" . $arr_filter['qpl'] . "%%");
2724 }
2725 }
2726
2727 $original_ids = &$this->getExistingQuestions();
2728 $original_clause = " qpl_questions.original_id IS NULL";
2729 if (count($original_ids)) {
2730 $original_clause = " qpl_questions.original_id IS NULL AND " . $this->db->in('qpl_questions.question_id', $original_ids, true, 'integer');
2731 }
2732
2733 $query_result = $this->db->query("
2734 SELECT qpl_questions.*, qpl_questions.tstamp,
2735 qpl_qst_type.type_tag, qpl_qst_type.plugin, qpl_qst_type.plugin_name,
2736 object_data.title parent_title
2737 FROM qpl_questions, qpl_qst_type, object_data
2738 WHERE $original_clause $available
2739 AND object_data.obj_id = qpl_questions.obj_fi
2740 AND qpl_questions.tstamp > 0
2741 AND qpl_questions.question_type_fi = qpl_qst_type.question_type_id
2742 $where
2743 ");
2744 $rows = [];
2745
2746 if ($query_result->numRows()) {
2747 while ($row = $this->db->fetchAssoc($query_result)) {
2749
2750 if (!$row['plugin']) {
2751 $row[ 'ttype' ] = $this->lng->txt($row[ "type_tag" ]);
2752
2753 $rows[] = $row;
2754 continue;
2755 }
2756
2757 $plugin = $this->component_repository->getPluginByName($row['plugin_name']);
2758 if (!$plugin->isActive()) {
2759 continue;
2760 }
2761
2762 $pl = $this->component_factory->getPlugin($plugin->getId());
2763 $row[ 'ttype' ] = $pl->getQuestionTypeTranslation();
2764
2765 $rows[] = $row;
2766 }
2767 }
2768 return $rows;
2769 }
2770
2775 public function fromXML(ilQTIAssessment $assessment, array $mappings): void
2776 {
2777 $this->saveToDb(true);
2778
2779 $main_settings = $this->getMainSettings();
2780 $general_settings = $main_settings->getGeneralSettings();
2781 $introduction_settings = $main_settings->getIntroductionSettings();
2782 $access_settings = $main_settings->getAccessSettings();
2783 $test_behaviour_settings = $main_settings->getTestBehaviourSettings();
2784 $question_behaviour_settings = $main_settings->getQuestionBehaviourSettings();
2785 $participant_functionality_settings = $main_settings->getParticipantFunctionalitySettings();
2786 $finishing_settings = $main_settings->getFinishingSettings();
2787 $additional_settings = $main_settings->getAdditionalSettings();
2788
2789 $introduction_settings = $introduction_settings->withIntroductionEnabled(false);
2790 foreach ($assessment->objectives as $objectives) {
2791 foreach ($objectives->materials as $material) {
2792 $introduction_settings = $this->addIntroductionToSettingsFromImport(
2793 $introduction_settings,
2794 $this->qtiMaterialToArray($material),
2795 $mappings
2796 );
2797 }
2798 }
2799
2800 if ($assessment->getPresentationMaterial()
2801 && $assessment->getPresentationMaterial()->getFlowMat(0)
2802 && $assessment->getPresentationMaterial()->getFlowMat(0)->getMaterial(0)) {
2803 $finishing_settings = $this->addConcludingRemarksToSettingsFromImport(
2804 $finishing_settings,
2805 $this->qtiMaterialToArray(
2806 $assessment->getPresentationMaterial()->getFlowMat(0)->getMaterial(0)
2807 ),
2808 $mappings
2809 );
2810 }
2811
2812 $score_settings = $this->getScoreSettings();
2813 $scoring_settings = $score_settings->getScoringSettings();
2814 $gamification_settings = $score_settings->getGamificationSettings();
2815 $result_summary_settings = $score_settings->getResultSummarySettings();
2816 $result_details_settings = $score_settings->getResultDetailsSettings();
2817
2818 $mark_steps = [];
2819 foreach ($assessment->qtimetadata as $metadata) {
2820 switch ($metadata["label"]) {
2821 case "solution_details":
2822 $result_details_settings = $result_details_settings->withShowPassDetails((bool) $metadata["entry"]);
2823 break;
2824 case "show_solution_list_comparison":
2825 $result_details_settings = $result_details_settings->withShowSolutionListComparison((bool) $metadata["entry"]);
2826 break;
2827 case "print_bs_with_res":
2828 $result_details_settings = $result_details_settings->withShowSolutionListComparison((bool) $metadata["entry"]);
2829 break;
2830 case "author":
2831 $this->saveAuthorToMetadata($metadata["entry"]);
2832 break;
2833 case "nr_of_tries":
2834 $test_behaviour_settings = $test_behaviour_settings->withNumberOfTries((int) $metadata["entry"]);
2835 break;
2836 case 'block_after_passed':
2837 $test_behaviour_settings = $test_behaviour_settings->withBlockAfterPassedEnabled((bool) $metadata['entry']);
2838 break;
2839 case "pass_waiting":
2840 $test_behaviour_settings = $test_behaviour_settings->withPassWaiting($metadata["entry"]);
2841 break;
2842 case "kiosk":
2843 $test_behaviour_settings = $test_behaviour_settings->withKioskMode((int) $metadata["entry"]);
2844 break;
2845 case 'show_introduction':
2846 $introduction_settings = $introduction_settings->withIntroductionEnabled((bool) $metadata['entry']);
2847 break;
2848 case "showfinalstatement":
2849 case 'show_concluding_remarks':
2850 $finishing_settings = $finishing_settings->withConcludingRemarksEnabled((bool) $metadata["entry"]);
2851 break;
2852 case 'exam_conditions':
2853 $introduction_settings = $introduction_settings->withExamConditionsCheckboxEnabled($metadata['entry'] === '1');
2854 break;
2855 case "highscore_enabled":
2856 $gamification_settings = $gamification_settings->withHighscoreEnabled((bool) $metadata["entry"]);
2857 break;
2858 case "highscore_anon":
2859 $gamification_settings = $gamification_settings->withHighscoreAnon((bool) $metadata["entry"]);
2860 break;
2861 case "highscore_achieved_ts":
2862 $gamification_settings = $gamification_settings->withHighscoreAchievedTS((bool) $metadata["entry"]);
2863 break;
2864 case "highscore_score":
2865 $gamification_settings = $gamification_settings->withHighscoreScore((bool) $metadata["entry"]);
2866 break;
2867 case "highscore_percentage":
2868 $gamification_settings = $gamification_settings->withHighscorePercentage((bool) $metadata["entry"]);
2869 break;
2870 case "highscore_wtime":
2871 $gamification_settings = $gamification_settings->withHighscoreWTime((bool) $metadata["entry"]);
2872 break;
2873 case "highscore_own_table":
2874 $gamification_settings = $gamification_settings->withHighscoreOwnTable((bool) $metadata["entry"]);
2875 break;
2876 case "highscore_top_table":
2877 $gamification_settings = $gamification_settings->withHighscoreTopTable((bool) $metadata["entry"]);
2878 break;
2879 case "highscore_top_num":
2880 $gamification_settings = $gamification_settings->withHighscoreTopNum((int) $metadata["entry"]);
2881 break;
2882 case "use_previous_answers":
2883 $participant_functionality_settings = $participant_functionality_settings->withUsePreviousAnswerAllowed((bool) $metadata["entry"]);
2884 break;
2885 case 'question_list_enabled':
2886 $participant_functionality_settings = $participant_functionality_settings->withQuestionListEnabled((bool) $metadata['entry']);
2887 // no break
2888 case "title_output":
2889 $question_behaviour_settings = $question_behaviour_settings->withQuestionTitleOutputMode((int) $metadata["entry"]);
2890 break;
2891 case "question_set_type":
2892 if ($metadata['entry'] === self::QUESTION_SET_TYPE_RANDOM) {
2893 $this->questions = [];
2894 }
2895 $general_settings = $general_settings->withQuestionSetType($metadata["entry"]);
2896 break;
2897 case "anonymity":
2898 $general_settings = $general_settings->withAnonymity((bool) $metadata["entry"]);
2899 break;
2900 case "results_presentation":
2901 $result_details_settings = $result_details_settings->withResultsPresentation((int) $metadata["entry"]);
2902 break;
2903 case "reset_processing_time":
2904 $test_behaviour_settings = $test_behaviour_settings->withResetProcessingTime($metadata["entry"] === '1');
2905 break;
2906 case "answer_feedback_points":
2907 $question_behaviour_settings = $question_behaviour_settings->withInstantFeedbackPointsEnabled((bool) $metadata["entry"]);
2908 break;
2909 case "answer_feedback":
2910 $question_behaviour_settings = $question_behaviour_settings->withInstantFeedbackGenericEnabled((bool) $metadata["entry"]);
2911 break;
2912 case 'instant_feedback_specific':
2913 $question_behaviour_settings = $question_behaviour_settings->withInstantFeedbackSpecificEnabled((bool) $metadata['entry']);
2914 break;
2915 case "instant_verification":
2916 $question_behaviour_settings = $question_behaviour_settings->withInstantFeedbackSolutionEnabled((bool) $metadata["entry"]);
2917 break;
2918 case "force_instant_feedback":
2919 $question_behaviour_settings = $question_behaviour_settings->withForceInstantFeedbackOnNextQuestion((bool) $metadata["entry"]);
2920 break;
2921 case "follow_qst_answer_fixation":
2922 $question_behaviour_settings = $question_behaviour_settings->withLockAnswerOnNextQuestionEnabled((bool) $metadata["entry"]);
2923 break;
2924 case "instant_feedback_answer_fixation":
2925 $question_behaviour_settings = $question_behaviour_settings->withLockAnswerOnInstantFeedbackEnabled((bool) $metadata["entry"]);
2926 break;
2927 case "show_cancel":
2928 case "suspend_test_allowed":
2929 $participant_functionality_settings = $participant_functionality_settings->withSuspendTestAllowed((bool) $metadata["entry"]);
2930 break;
2931 case "sequence_settings":
2932 $participant_functionality_settings = $participant_functionality_settings->withPostponedQuestionsMoveToEnd((bool) $metadata["entry"]);
2933 break;
2934 case "show_marker":
2935 $participant_functionality_settings = $participant_functionality_settings->withQuestionMarkingEnabled((bool) $metadata["entry"]);
2936 break;
2937 case "fixed_participants":
2938 $access_settings = $access_settings->withFixedParticipants((bool) $metadata["entry"]);
2939 break;
2940 case "score_reporting":
2941 if ($metadata['entry'] !== null) {
2942 $result_summary_settings = $result_summary_settings->withScoreReporting(
2943 ScoreReportingTypes::tryFrom((int) $metadata['entry']) ?? ScoreReportingTypes::SCORE_REPORTING_DISABLED
2944 );
2945 }
2946 break;
2947 case "shuffle_questions":
2948 $question_behaviour_settings = $question_behaviour_settings->withShuffleQuestions((bool) $metadata["entry"]);
2949 break;
2950 case "count_system":
2951 $scoring_settings = $scoring_settings->withCountSystem((int) $metadata["entry"]);
2952 break;
2953 case "exportsettings":
2954 $result_details_settings = $result_details_settings->withExportSettings((int) $metadata["entry"]);
2955 break;
2956 case "score_cutting":
2957 $scoring_settings = $scoring_settings->withScoreCutting((int) $metadata["entry"]);
2958 break;
2959 case "password":
2960 $access_settings = $access_settings->withPasswordEnabled(
2961 $metadata["entry"] !== null && $metadata["entry"] !== ''
2962 )->withPassword($metadata["entry"]);
2963 break;
2964 case 'ip_range_from':
2965 if ($metadata['entry'] !== '') {
2966 $access_settings = $access_settings->withIpRangeFrom($metadata['entry']);
2967 }
2968 break;
2969 case 'ip_range_to':
2970 if ($metadata['entry'] !== '') {
2971 $access_settings = $access_settings->withIpRangeTo($metadata['entry']);
2972 }
2973 break;
2974 case "pass_scoring":
2975 $scoring_settings = $scoring_settings->withPassScoring((int) $metadata["entry"]);
2976 break;
2977 case 'pass_deletion_allowed':
2978 $result_summary_settings = $result_summary_settings->withPassDeletionAllowed((bool) $metadata["entry"]);
2979 break;
2980 case "usr_pass_overview_mode":
2981 $participant_functionality_settings = $participant_functionality_settings->withUsrPassOverviewMode((int) $metadata["entry"]);
2982 break;
2983 case "question_list":
2984 $participant_functionality_settings = $participant_functionality_settings->withQuestionListEnabled((bool) $metadata["entry"]);
2985 break;
2986
2987 case "reporting_date":
2988 $reporting_date = $this->buildDateTimeImmutableFromPeriod($metadata['entry']);
2989 if ($reporting_date !== null) {
2990 $result_summary_settings = $result_summary_settings->withReportingDate($reporting_date);
2991 }
2992 break;
2993 case 'enable_processing_time':
2994 $test_behaviour_settings = $test_behaviour_settings->withProcessingTimeEnabled((bool) $metadata['entry']);
2995 break;
2996 case "processing_time":
2997 $test_behaviour_settings = $test_behaviour_settings->withProcessingTime($metadata['entry']);
2998 break;
2999 case "starting_time":
3000 $starting_time = $this->buildDateTimeImmutableFromPeriod($metadata['entry']);
3001 if ($starting_time !== null) {
3002 $access_settings = $access_settings->withStartTime($starting_time)
3003 ->withStartTimeEnabled(true);
3004 }
3005 break;
3006 case "ending_time":
3007 $ending_time = $this->buildDateTimeImmutableFromPeriod($metadata['entry']);
3008 if ($ending_time !== null) {
3009 $access_settings = $access_settings->withEndTime($ending_time)
3010 ->withStartTimeEnabled(true);
3011 }
3012 break;
3013 case "enable_examview":
3014 $finishing_settings = $finishing_settings->withShowAnswerOverview((bool) $metadata["entry"]);
3015 break;
3016 case 'redirection_mode':
3017 $finishing_settings = $finishing_settings->withRedirectionMode(
3018 RedirectionModes::tryFrom((int) ($metadata['entry'] ?? 0)) ?? RedirectionModes::NONE
3019 );
3020 break;
3021 case 'redirection_url':
3022 $finishing_settings = $finishing_settings->withRedirectionUrl($metadata['entry']);
3023 break;
3024 case 'examid_in_test_pass':
3025 $test_behaviour_settings = $test_behaviour_settings->withExamIdInTestAttemptEnabled((bool) $metadata['entry']);
3026 break;
3027 case 'examid_in_test_res':
3028 $result_details_settings = $result_details_settings->withShowExamIdInTestResults((bool) $metadata["entry"]);
3029 break;
3030 case 'skill_service':
3031 $additional_settings = $additional_settings->withSkillsServiceEnabled((bool) $metadata['entry']);
3032 break;
3033 case 'show_grading_status':
3034 $result_summary_settings = $result_summary_settings->withShowGradingStatusEnabled((bool) $metadata["entry"]);
3035 break;
3036 case 'show_grading_mark':
3037 $result_summary_settings = $result_summary_settings->withShowGradingMarkEnabled((bool) $metadata["entry"]);
3038 break;
3039 case 'autosave':
3040 $question_behaviour_settings = $question_behaviour_settings->withAutosaveEnabled((bool) $metadata['entry']);
3041 break;
3042 case 'autosave_ival':
3043 $question_behaviour_settings = $question_behaviour_settings->withAutosaveInterval((int) $metadata['entry']);
3044 break;
3045 case 'show_summary':
3046 $participant_functionality_settings = $participant_functionality_settings->withQuestionListEnabled(($metadata['entry'] & 1) > 0)
3047 ->withUsrPassOverviewMode((int) $metadata['entry']);
3048
3049 // no break
3050 case 'hide_info_tab':
3051 $additional_settings = $additional_settings->withHideInfoTab($metadata['entry'] === '1');
3052 }
3053 if (preg_match("/mark_step_\d+/", $metadata["label"])) {
3054 $xmlmark = $metadata["entry"];
3055 preg_match("/<short>(.*?)<\/short>/", $xmlmark, $matches);
3056 $mark_short = $matches[1];
3057 preg_match("/<official>(.*?)<\/official>/", $xmlmark, $matches);
3058 $mark_official = $matches[1];
3059 preg_match("/<percentage>(.*?)<\/percentage>/", $xmlmark, $matches);
3060 $mark_percentage = (float) $matches[1];
3061 preg_match("/<passed>(.*?)<\/passed>/", $xmlmark, $matches);
3062 $mark_passed = (bool) $matches[1];
3063 $mark_steps[] = new Mark($mark_short, $mark_official, $mark_percentage, $mark_passed);
3064 }
3065 }
3066 $this->mark_schema = $this->getMarkSchema()->withMarkSteps($mark_steps);
3067 $this->saveToDb();
3068 $this->getObjectProperties()->storePropertyTitleAndDescription(
3069 $this->getObjectProperties()->getPropertyTitleAndDescription()
3070 ->withTitle($assessment->getTitle())
3071 ->withDescription($assessment->getComment())
3072 );
3073 $this->addToNewsOnOnline(false, $this->getObjectProperties()->getPropertyIsOnline()->getIsOnline());
3074 $main_settings = $main_settings
3075 ->withGeneralSettings($general_settings)
3076 ->withIntroductionSettings($introduction_settings)
3077 ->withAccessSettings($access_settings)
3078 ->withParticipantFunctionalitySettings($participant_functionality_settings)
3079 ->withTestBehaviourSettings($test_behaviour_settings)
3080 ->withQuestionBehaviourSettings($question_behaviour_settings)
3081 ->withFinishingSettings($finishing_settings)
3082 ->withAdditionalSettings($additional_settings);
3083 $this->getMainSettingsRepository()->store($main_settings);
3084 $this->main_settings = $main_settings;
3085
3086 $score_settings = $score_settings
3087 ->withGamificationSettings($gamification_settings)
3088 ->withScoringSettings($scoring_settings)
3089 ->withResultDetailsSettings($result_details_settings)
3090 ->withResultSummarySettings($result_summary_settings);
3091 $this->getScoreSettingsRepository()->store($score_settings);
3092 $this->score_settings = $score_settings;
3093 $this->loadFromDb();
3094 }
3095
3097 SettingsIntroduction $settings,
3098 array $material,
3099 array $mappings
3101 if (!str_starts_with($material['text'], '<PageObject>')) {
3102 return $settings;
3103 }
3104
3105 $text = $this->replaceFilesInPageImports(
3106 $this->replaceMobsInPageImports(
3107 $material['text'],
3108 $mappings['components/ILIAS/MediaObjects']['mob'] ?? []
3109 )
3110 );
3111
3112 $page_object = new ilTestPage();
3113 $page_object->setParentId($this->getId());
3114 $page_object->setXMLContent($text);
3115 $new_page_id = $page_object->createPageWithNextId();
3116 return $settings->withIntroductionPageId($new_page_id);
3117 }
3118
3120 SettingsFinishing $settings,
3121 array $material,
3122 array $mappings
3124 if (!str_starts_with($material['text'], '<PageObject>')) {
3125 return $settings;
3126 }
3127
3128 $text = $this->replaceFilesInPageImports(
3129 $this->replaceMobsInPageImports(
3130 $material['text'],
3131 $mappings['components/ILIAS/MediaObjects']['mob'] ?? []
3132 )
3133 );
3134
3135 $page_object = new ilTestPage();
3136 $page_object->setParentId($this->getId());
3137 $page_object->setXMLContent($text);
3138 $new_page_id = $page_object->createPageWithNextId();
3139 return $settings->withConcludingRemarksPageId($new_page_id);
3140 }
3141
3142 private function replaceMobsInPageImports(string $text, array $mappings): string
3143 {
3144 preg_match_all('/il_(\d+)_mob_(\d+)/', $text, $matches);
3145 foreach ($matches[0] as $index => $match) {
3146 if (empty($mappings[$matches[2][$index]])) {
3147 continue;
3148 }
3149 $text = str_replace($match, "il__mob_{$mappings[$matches[2][$index]]}", $text);
3150 ilObjMediaObject::_saveUsage((int) $mappings[$matches[2][$index]], 'tst', $this->getId());
3151 }
3152 return $text;
3153 }
3154
3155 private function replaceFilesInPageImports(string $text, array $mappings): string
3156 {
3157 preg_match_all('/il_(\d+)_file_(\d+)/', $text, $matches);
3158 foreach ($matches[0] as $index => $match) {
3159 if (empty($mappings[$matches[2][$index]])) {
3160 continue;
3161 }
3162 $text = str_replace($match, "il__file_{$mappings[$matches[2][$index]]}", $text);
3163 }
3164 return $text;
3165 }
3166
3172 public function toXML(): string
3173 {
3174 $main_settings = $this->getMainSettings();
3175 $a_xml_writer = new ilXmlWriter();
3176 // set xml header
3177 $a_xml_writer->xmlHeader();
3178 $a_xml_writer->xmlSetDtdDef("<!DOCTYPE questestinterop SYSTEM \"ims_qtiasiv1p2p1.dtd\">");
3179 $a_xml_writer->xmlStartTag("questestinterop");
3180
3181 $attrs = [
3182 "ident" => "il_" . IL_INST_ID . "_tst_" . $this->getTestId(),
3183 "title" => $this->getTitle()
3184 ];
3185 $a_xml_writer->xmlStartTag("assessment", $attrs);
3186 $a_xml_writer->xmlElement("qticomment", null, $this->getDescription());
3187
3188 if ($main_settings->getTestBehaviourSettings()->getProcessingTimeEnabled()) {
3189 $a_xml_writer->xmlElement(
3190 "duration",
3191 null,
3192 $this->getProcessingTimeForXML()
3193 );
3194 }
3195
3196 $a_xml_writer->xmlStartTag("qtimetadata");
3197 $a_xml_writer->xmlStartTag("qtimetadatafield");
3198 $a_xml_writer->xmlElement("fieldlabel", null, "ILIAS_VERSION");
3199 $a_xml_writer->xmlElement("fieldentry", null, ILIAS_VERSION);
3200 $a_xml_writer->xmlEndTag("qtimetadatafield");
3201
3202 $a_xml_writer->xmlStartTag("qtimetadatafield");
3203 $a_xml_writer->xmlElement("fieldlabel", null, "anonymity");
3204 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getGeneralSettings()->getAnonymity() ? 1 : 0);
3205 $a_xml_writer->xmlEndTag("qtimetadatafield");
3206
3207 $a_xml_writer->xmlStartTag("qtimetadatafield");
3208 $a_xml_writer->xmlElement("fieldlabel", null, "question_set_type");
3209 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getGeneralSettings()->getQuestionSetType());
3210 $a_xml_writer->xmlEndTag("qtimetadatafield");
3211
3212 $a_xml_writer->xmlStartTag("qtimetadatafield");
3213 $a_xml_writer->xmlElement("fieldlabel", null, "sequence_settings");
3214 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getParticipantFunctionalitySettings()->getPostponedQuestionsMoveToEnd() ? 1 : 0);
3215 $a_xml_writer->xmlEndTag("qtimetadatafield");
3216
3217 $a_xml_writer->xmlStartTag("qtimetadatafield");
3218 $a_xml_writer->xmlElement("fieldlabel", null, "author");
3219 $a_xml_writer->xmlElement("fieldentry", null, $this->getAuthor());
3220 $a_xml_writer->xmlEndTag("qtimetadatafield");
3221
3222 $a_xml_writer->xmlStartTag("qtimetadatafield");
3223 $a_xml_writer->xmlElement("fieldlabel", null, "reset_processing_time");
3224 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getTestBehaviourSettings()->getResetProcessingTime() ? 1 : 0);
3225 $a_xml_writer->xmlEndTag("qtimetadatafield");
3226
3227 $a_xml_writer->xmlStartTag("qtimetadatafield");
3228 $a_xml_writer->xmlElement("fieldlabel", null, "count_system");
3229 $a_xml_writer->xmlElement("fieldentry", null, $this->getCountSystem());
3230 $a_xml_writer->xmlEndTag("qtimetadatafield");
3231
3232 $a_xml_writer->xmlStartTag("qtimetadatafield");
3233 $a_xml_writer->xmlElement("fieldlabel", null, "score_cutting");
3234 $a_xml_writer->xmlElement("fieldentry", null, $this->getScoreCutting());
3235 $a_xml_writer->xmlEndTag("qtimetadatafield");
3236
3237 $a_xml_writer->xmlStartTag("qtimetadatafield");
3238 $a_xml_writer->xmlElement("fieldlabel", null, "password");
3239 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getAccessSettings()->getPassword() ?? '');
3240 $a_xml_writer->xmlEndTag("qtimetadatafield");
3241
3242 $a_xml_writer->xmlStartTag("qtimetadatafield");
3243 $a_xml_writer->xmlElement("fieldlabel", null, "ip_range_from");
3244 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getAccessSettings()->getIpRangeFrom());
3245 $a_xml_writer->xmlEndTag("qtimetadatafield");
3246
3247 $a_xml_writer->xmlStartTag("qtimetadatafield");
3248 $a_xml_writer->xmlElement("fieldlabel", null, "ip_range_to");
3249 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getAccessSettings()->getIpRangeTo());
3250 $a_xml_writer->xmlEndTag("qtimetadatafield");
3251
3252 $a_xml_writer->xmlStartTag("qtimetadatafield");
3253 $a_xml_writer->xmlElement("fieldlabel", null, "pass_scoring");
3254 $a_xml_writer->xmlElement("fieldentry", null, $this->getPassScoring());
3255 $a_xml_writer->xmlEndTag("qtimetadatafield");
3256
3257 $a_xml_writer->xmlStartTag('qtimetadatafield');
3258 $a_xml_writer->xmlElement('fieldlabel', null, 'pass_deletion_allowed');
3259 $a_xml_writer->xmlElement('fieldentry', null, $this->isPassDeletionAllowed() ? 1 : 0);
3260 $a_xml_writer->xmlEndTag('qtimetadatafield');
3261
3262 if ($this->getScoreSettings()->getResultSummarySettings()->getReportingDate() !== null) {
3263 $a_xml_writer->xmlStartTag("qtimetadatafield");
3264 $a_xml_writer->xmlElement("fieldlabel", null, "reporting_date");
3265 $a_xml_writer->xmlElement(
3266 "fieldentry",
3267 null,
3268 $this->buildIso8601PeriodForExportCompatibility(
3269 $this->getScoreSettings()->getResultSummarySettings()->getReportingDate(),
3270 ),
3271 );
3272 $a_xml_writer->xmlEndTag("qtimetadatafield");
3273 }
3274
3275 $a_xml_writer->xmlStartTag("qtimetadatafield");
3276 $a_xml_writer->xmlElement("fieldlabel", null, "nr_of_tries");
3277 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getTestBehaviourSettings()->getNumberOfTries());
3278 $a_xml_writer->xmlEndTag("qtimetadatafield");
3279
3280 $a_xml_writer->xmlStartTag('qtimetadatafield');
3281 $a_xml_writer->xmlElement('fieldlabel', null, 'block_after_passed');
3282 $a_xml_writer->xmlElement('fieldentry', null, $main_settings->getTestBehaviourSettings()->getBlockAfterPassedEnabled() ? 1 : 0);
3283 $a_xml_writer->xmlEndTag('qtimetadatafield');
3284
3285 $a_xml_writer->xmlStartTag("qtimetadatafield");
3286 $a_xml_writer->xmlElement("fieldlabel", null, "pass_waiting");
3287 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getTestBehaviourSettings()->getPassWaiting());
3288 $a_xml_writer->xmlEndTag("qtimetadatafield");
3289
3290 $a_xml_writer->xmlStartTag("qtimetadatafield");
3291 $a_xml_writer->xmlElement("fieldlabel", null, "kiosk");
3292 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getTestBehaviourSettings()->getKioskMode());
3293 $a_xml_writer->xmlEndTag("qtimetadatafield");
3294
3295 $a_xml_writer->xmlStartTag('qtimetadatafield');
3296 $a_xml_writer->xmlElement("fieldlabel", null, "redirection_mode");
3297 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getFinishingSettings()->getRedirectionMode()->value);
3298 $a_xml_writer->xmlEndTag("qtimetadatafield");
3299
3300 $a_xml_writer->xmlStartTag('qtimetadatafield');
3301 $a_xml_writer->xmlElement("fieldlabel", null, "redirection_url");
3302 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getFinishingSettings()->getRedirectionUrl());
3303 $a_xml_writer->xmlEndTag("qtimetadatafield");
3304
3305 $a_xml_writer->xmlStartTag("qtimetadatafield");
3306 $a_xml_writer->xmlElement("fieldlabel", null, "use_previous_answers");
3307 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getParticipantFunctionalitySettings()->getUsePreviousAnswerAllowed() ? 1 : 0);
3308 $a_xml_writer->xmlEndTag("qtimetadatafield");
3309
3310 $a_xml_writer->xmlStartTag('qtimetadatafield');
3311 $a_xml_writer->xmlElement('fieldlabel', null, 'question_list_enabled');
3312 $a_xml_writer->xmlElement('fieldentry', null, $main_settings->getParticipantFunctionalitySettings()->getQuestionListEnabled() ? 1 : 0);
3313 $a_xml_writer->xmlEndTag('qtimetadatafield');
3314
3315 $a_xml_writer->xmlStartTag("qtimetadatafield");
3316 $a_xml_writer->xmlElement("fieldlabel", null, "title_output");
3317 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getQuestionTitleOutputMode());
3318 $a_xml_writer->xmlEndTag("qtimetadatafield");
3319
3320 $a_xml_writer->xmlStartTag("qtimetadatafield");
3321 $a_xml_writer->xmlElement("fieldlabel", null, "results_presentation");
3322 $a_xml_writer->xmlElement("fieldentry", null, $this->getScoreSettings()->getResultDetailsSettings()->getResultsPresentation());
3323 $a_xml_writer->xmlEndTag("qtimetadatafield");
3324
3325 $a_xml_writer->xmlStartTag("qtimetadatafield");
3326 $a_xml_writer->xmlElement("fieldlabel", null, "examid_in_test_pass");
3327 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getTestBehaviourSettings()->getExamIdInTestAttemptEnabled() ? 1 : 0);
3328 $a_xml_writer->xmlEndTag("qtimetadatafield");
3329
3330 $a_xml_writer->xmlStartTag("qtimetadatafield");
3331 $a_xml_writer->xmlElement("fieldlabel", null, "examid_in_test_res");
3332 $a_xml_writer->xmlElement("fieldentry", null, $this->getScoreSettings()->getResultDetailsSettings()->getShowExamIdInTestResults() ? 1 : 0);
3333 $a_xml_writer->xmlEndTag("qtimetadatafield");
3334
3335 $a_xml_writer->xmlStartTag("qtimetadatafield");
3336 $a_xml_writer->xmlElement("fieldlabel", null, "usr_pass_overview_mode");
3337 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getParticipantFunctionalitySettings()->getUsrPassOverviewMode());
3338 $a_xml_writer->xmlEndTag("qtimetadatafield");
3339
3340 $a_xml_writer->xmlStartTag("qtimetadatafield");
3341 $a_xml_writer->xmlElement("fieldlabel", null, "score_reporting");
3342 $a_xml_writer->xmlElement("fieldentry", null, $this->getScoreSettings()->getResultSummarySettings()->getScoreReporting()->value);
3343 $a_xml_writer->xmlEndTag("qtimetadatafield");
3344
3345 $a_xml_writer->xmlStartTag("qtimetadatafield");
3346 $a_xml_writer->xmlElement("fieldlabel", null, "show_solution_list_comparison");
3347 $a_xml_writer->xmlElement("fieldentry", null, $this->score_settings->getResultDetailsSettings()->getShowSolutionListComparison() ? 1 : 0);
3348 $a_xml_writer->xmlEndTag("qtimetadatafield");
3349
3350 $a_xml_writer->xmlStartTag("qtimetadatafield");
3351 $a_xml_writer->xmlElement("fieldlabel", null, "instant_verification");
3352 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackSolutionEnabled() ? 1 : 0);
3353 $a_xml_writer->xmlEndTag("qtimetadatafield");
3354
3355 $a_xml_writer->xmlStartTag("qtimetadatafield");
3356 $a_xml_writer->xmlElement("fieldlabel", null, "answer_feedback");
3357 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackGenericEnabled() ? 1 : 0);
3358 $a_xml_writer->xmlEndTag("qtimetadatafield");
3359
3360 $a_xml_writer->xmlStartTag("qtimetadatafield");
3361 $a_xml_writer->xmlElement("fieldlabel", null, "instant_feedback_specific");
3362 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackSpecificEnabled() ? 1 : 0);
3363 $a_xml_writer->xmlEndTag("qtimetadatafield");
3364
3365 $a_xml_writer->xmlStartTag("qtimetadatafield");
3366 $a_xml_writer->xmlElement("fieldlabel", null, "answer_feedback_points");
3367 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackPointsEnabled() ? 1 : 0);
3368 $a_xml_writer->xmlEndTag("qtimetadatafield");
3369
3370 $a_xml_writer->xmlStartTag("qtimetadatafield");
3371 $a_xml_writer->xmlElement("fieldlabel", null, "follow_qst_answer_fixation");
3372 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getLockAnswerOnNextQuestionEnabled() ? 1 : 0);
3373 $a_xml_writer->xmlEndTag("qtimetadatafield");
3374
3375 $a_xml_writer->xmlStartTag("qtimetadatafield");
3376 $a_xml_writer->xmlElement("fieldlabel", null, "instant_feedback_answer_fixation");
3377 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getLockAnswerOnInstantFeedbackEnabled() ? 1 : 0);
3378 $a_xml_writer->xmlEndTag("qtimetadatafield");
3379
3380 $a_xml_writer->xmlStartTag("qtimetadatafield");
3381 $a_xml_writer->xmlElement("fieldlabel", null, "force_instant_feedback");
3382 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getForceInstantFeedbackOnNextQuestion() ? 1 : 0);
3383 $a_xml_writer->xmlEndTag("qtimetadatafield");
3384
3385 $highscore_metadata = [
3386 'highscore_enabled' => $this->getHighscoreEnabled(),
3387 'highscore_anon' => $this->getHighscoreAnon(),
3388 'highscore_achieved_ts' => $this->getHighscoreAchievedTS(),
3389 'highscore_score' => $this->getHighscoreScore(),
3390 'highscore_percentage' => $this->getHighscorePercentage(),
3391 'highscore_wtime' => $this->getHighscoreWTime(),
3392 'highscore_own_table' => $this->getHighscoreOwnTable(),
3393 'highscore_top_table' => $this->getHighscoreTopTable(),
3394 ];
3395 foreach ($highscore_metadata as $label => $value) {
3396 $a_xml_writer->xmlStartTag("qtimetadatafield");
3397 $a_xml_writer->xmlElement("fieldlabel", null, $label);
3398 $a_xml_writer->xmlElement("fieldentry", null, $value ? 1 : 0);
3399 $a_xml_writer->xmlEndTag("qtimetadatafield");
3400 }
3401 $a_xml_writer->xmlStartTag("qtimetadatafield");
3402 $a_xml_writer->xmlElement("fieldlabel", null, "highscore_top_num");
3403 $a_xml_writer->xmlElement("fieldentry", null, $this->getHighscoreTopNum());
3404 $a_xml_writer->xmlEndTag("qtimetadatafield");
3405
3406 $a_xml_writer->xmlStartTag("qtimetadatafield");
3407 $a_xml_writer->xmlElement("fieldlabel", null, "suspend_test_allowed");
3408 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getParticipantFunctionalitySettings()->getSuspendTestAllowed() ? 1 : 0);
3409 $a_xml_writer->xmlEndTag("qtimetadatafield");
3410
3411 $a_xml_writer->xmlStartTag("qtimetadatafield");
3412 $a_xml_writer->xmlElement("fieldlabel", null, "show_marker");
3413 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getParticipantFunctionalitySettings()->getQuestionMarkingEnabled() ? 1 : 0);
3414 $a_xml_writer->xmlEndTag("qtimetadatafield");
3415
3416 $a_xml_writer->xmlStartTag("qtimetadatafield");
3417 $a_xml_writer->xmlElement("fieldlabel", null, "fixed_participants");
3418 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getAccessSettings()->getFixedParticipants() ? 1 : 0);
3419 $a_xml_writer->xmlEndTag("qtimetadatafield");
3420
3421 $a_xml_writer->xmlStartTag("qtimetadatafield");
3422 $a_xml_writer->xmlElement("fieldlabel", null, "show_introduction");
3423 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getIntroductionSettings()->getIntroductionEnabled() ? 1 : 0);
3424 $a_xml_writer->xmlEndTag("qtimetadatafield");
3425
3426 $a_xml_writer->xmlStartTag("qtimetadatafield");
3427 $a_xml_writer->xmlElement("fieldlabel", null, 'exam_conditions');
3428 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getIntroductionSettings()->getExamConditionsCheckboxEnabled() ? 1 : 0);
3429 $a_xml_writer->xmlEndTag("qtimetadatafield");
3430
3431 $a_xml_writer->xmlStartTag("qtimetadatafield");
3432 $a_xml_writer->xmlElement("fieldlabel", null, "show_concluding_remarks");
3433 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getFinishingSettings()->getConcludingRemarksEnabled() ? 1 : 0);
3434 $a_xml_writer->xmlEndTag("qtimetadatafield");
3435
3436 $a_xml_writer->xmlStartTag("qtimetadatafield");
3437 $a_xml_writer->xmlElement("fieldlabel", null, "exportsettings");
3438 $a_xml_writer->xmlElement("fieldentry", null, $this->getExportSettings());
3439 $a_xml_writer->xmlEndTag("qtimetadatafield");
3440
3441 $a_xml_writer->xmlStartTag("qtimetadatafield");
3442 $a_xml_writer->xmlElement("fieldlabel", null, "shuffle_questions");
3443 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getShuffleQuestions() ? 1 : 0);
3444 $a_xml_writer->xmlEndTag("qtimetadatafield");
3445
3446 $a_xml_writer->xmlStartTag("qtimetadatafield");
3447 $a_xml_writer->xmlElement("fieldlabel", null, "processing_time");
3448 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getTestBehaviourSettings()->getProcessingTime());
3449 $a_xml_writer->xmlEndTag("qtimetadatafield");
3450
3451 $a_xml_writer->xmlStartTag("qtimetadatafield");
3452 $a_xml_writer->xmlElement("fieldlabel", null, "enable_examview");
3453 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getFinishingSettings()->getShowAnswerOverview() ? 1 : 0);
3454 $a_xml_writer->xmlEndTag("qtimetadatafield");
3455
3456 $a_xml_writer->xmlStartTag("qtimetadatafield");
3457 $a_xml_writer->xmlElement("fieldlabel", null, "skill_service");
3458 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getAdditionalSettings()->getSkillsServiceEnabled() ? 1 : 0);
3459 $a_xml_writer->xmlEndTag("qtimetadatafield");
3460
3461 if ($this->getInstantFeedbackSolution() == 1) {
3462 $attrs = [
3463 "solutionswitch" => "Yes"
3464 ];
3465 } else {
3466 $attrs = null;
3467 }
3468 $a_xml_writer->xmlElement("assessmentcontrol", $attrs, null);
3469
3470 $a_xml_writer->xmlStartTag("qtimetadatafield");
3471 $a_xml_writer->xmlElement("fieldlabel", null, "show_grading_status");
3472 $a_xml_writer->xmlElement("fieldentry", null, $this->isShowGradingStatusEnabled() ? 1 : 0);
3473 $a_xml_writer->xmlEndTag("qtimetadatafield");
3474
3475 $a_xml_writer->xmlStartTag("qtimetadatafield");
3476 $a_xml_writer->xmlElement("fieldlabel", null, "show_grading_mark");
3477 $a_xml_writer->xmlElement("fieldentry", null, $this->isShowGradingMarkEnabled() ? 1 : 0);
3478 $a_xml_writer->xmlEndTag("qtimetadatafield");
3479
3480 $a_xml_writer->xmlStartTag('qtimetadatafield');
3481 $a_xml_writer->xmlElement('fieldlabel', null, 'hide_info_tab');
3482 $a_xml_writer->xmlElement('fieldentry', null, $this->getMainSettings()->getAdditionalSettings()->getHideInfoTab() ? 1 : 0);
3483 $a_xml_writer->xmlEndTag("qtimetadatafield");
3484
3485 if ($this->getStartingTime() > 0) {
3486 $a_xml_writer->xmlStartTag("qtimetadatafield");
3487 $a_xml_writer->xmlElement("fieldlabel", null, "starting_time");
3488 $a_xml_writer->xmlElement(
3489 "fieldentry",
3490 null,
3491 $this->buildIso8601PeriodForExportCompatibility(
3492 (new DateTimeImmutable())->setTimestamp($this->getStartingTime()),
3493 ),
3494 );
3495 $a_xml_writer->xmlEndTag("qtimetadatafield");
3496 }
3497
3498 if ($this->getEndingTime() > 0) {
3499 $a_xml_writer->xmlStartTag("qtimetadatafield");
3500 $a_xml_writer->xmlElement("fieldlabel", null, "ending_time");
3501 $a_xml_writer->xmlElement(
3502 "fieldentry",
3503 null,
3504 $this->buildIso8601PeriodForExportCompatibility(
3505 (new DateTimeImmutable())->setTimestamp($this->getEndingTime()),
3506 ),
3507 );
3508 $a_xml_writer->xmlEndTag("qtimetadatafield");
3509 }
3510
3511 $a_xml_writer->xmlStartTag("qtimetadatafield");
3512 $a_xml_writer->xmlElement("fieldlabel", null, "autosave");
3513 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getAutosaveEnabled() ? 1 : 0);
3514 $a_xml_writer->xmlEndTag("qtimetadatafield");
3515
3516 $a_xml_writer->xmlStartTag("qtimetadatafield");
3517 $a_xml_writer->xmlElement("fieldlabel", null, "autosave_ival");
3518 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getAutosaveInterval());
3519 $a_xml_writer->xmlEndTag("qtimetadatafield");
3520
3521 $a_xml_writer->xmlStartTag("qtimetadatafield");
3522 $a_xml_writer->xmlElement("fieldlabel", null, "instant_feedback_specific");
3523 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackSpecificEnabled() ? 1 : 0);
3524 $a_xml_writer->xmlEndTag("qtimetadatafield");
3525
3526 $a_xml_writer->xmlStartTag("qtimetadatafield");
3527 $a_xml_writer->xmlElement("fieldlabel", null, "instant_feedback_answer_fixation");
3528 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getLockAnswerOnInstantFeedbackEnabled() ? 1 : 0);
3529 $a_xml_writer->xmlEndTag("qtimetadatafield");
3530
3531 $a_xml_writer->xmlStartTag("qtimetadatafield");
3532 $a_xml_writer->xmlElement("fieldlabel", null, "enable_processing_time");
3533 $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getTestBehaviourSettings()->getProcessingTimeEnabled() ? 1 : 0);
3534 $a_xml_writer->xmlEndTag("qtimetadatafield");
3535
3536 foreach ($this->getMarkSchema()->getMarkSteps() as $index => $mark) {
3537 $a_xml_writer->xmlStartTag("qtimetadatafield");
3538 $a_xml_writer->xmlElement("fieldlabel", null, "mark_step_$index");
3539 $a_xml_writer->xmlElement("fieldentry", null, sprintf(
3540 "<short>%s</short><official>%s</official><percentage>%.2f</percentage><passed>%d</passed>",
3541 $mark->getShortName(),
3542 $mark->getOfficialName(),
3543 $mark->getMinimumLevel(),
3544 $mark->getPassed()
3545 ));
3546 $a_xml_writer->xmlEndTag("qtimetadatafield");
3547 }
3548 $a_xml_writer->xmlEndTag("qtimetadata");
3549
3550 $page_id = $main_settings->getIntroductionSettings()->getIntroductionPageId();
3551 $introduction = $page_id !== null
3552 ? (new ilTestPage($page_id))->getXMLContent()
3553 : ilRTE::_replaceMediaObjectImageSrc($this->getIntroduction(), 0);
3554
3555 $a_xml_writer->xmlStartTag("objectives");
3556 $this->addQTIMaterial($a_xml_writer, $page_id, $introduction);
3557 $a_xml_writer->xmlEndTag("objectives");
3558
3559 if ($this->getInstantFeedbackSolution() == 1) {
3560 $attrs = [
3561 "solutionswitch" => "Yes"
3562 ];
3563 } else {
3564 $attrs = null;
3565 }
3566 $a_xml_writer->xmlElement("assessmentcontrol", $attrs, null);
3567
3568 if (strlen($this->getFinalStatement())) {
3569 $page_id = $main_settings->getFinishingSettings()->getConcludingRemarksPageId();
3570 $concluding_remarks = $page_id !== null
3571 ? (new ilTestPage($page_id))->getXMLContent()
3572 : ilRTE::_replaceMediaObjectImageSrc($this->getFinalStatement());
3573 // add qti presentation_material
3574 $a_xml_writer->xmlStartTag("presentation_material");
3575 $a_xml_writer->xmlStartTag("flow_mat");
3576 $this->addQTIMaterial($a_xml_writer, $page_id, $concluding_remarks);
3577 $a_xml_writer->xmlEndTag("flow_mat");
3578 $a_xml_writer->xmlEndTag("presentation_material");
3579 }
3580
3581 $attrs = [
3582 "ident" => "1"
3583 ];
3584 $a_xml_writer->xmlElement("section", $attrs, null);
3585 $a_xml_writer->xmlEndTag("assessment");
3586 $a_xml_writer->xmlEndTag("questestinterop");
3587
3588 $xml = $a_xml_writer->xmlDumpMem(false);
3589 return $xml;
3590 }
3591
3592 protected function buildIso8601PeriodForExportCompatibility(DateTimeImmutable $date_time): string
3593 {
3594 return $date_time->setTimezone(new DateTimeZone('UTC'))->format('\PY\Yn\Mj\D\TG\Hi\Ms\S');
3595 }
3596
3597 protected function buildDateTimeImmutableFromPeriod(?string $period): ?DateTimeImmutable
3598 {
3599 if ($period === null) {
3600 return null;
3601 }
3602 if (preg_match("/P(\d+)Y(\d+)M(\d+)DT(\d+)H(\d+)M(\d+)S/", $period, $matches)) {
3603 return new DateTimeImmutable(
3604 sprintf(
3605 "%02d-%02d-%02d %02d:%02d:%02d",
3606 $matches[1],
3607 $matches[2],
3608 $matches[3],
3609 $matches[4],
3610 $matches[5],
3611 $matches[6]
3612 ),
3613 new \DateTimeZone('UTC')
3614 );
3615 }
3616 return null;
3617 }
3618
3625 public function exportPagesXML(&$a_xml_writer, $a_inst, $a_target_dir, &$expLog): void
3626 {
3627 $this->mob_ids = [];
3628
3629 // PageObjects
3630 $expLog->write(date("[y-m-d H:i:s] ") . "Start Export Page Objects");
3631 $this->bench->start("ContentObjectExport", "exportPageObjects");
3632 $this->exportXMLPageObjects($a_xml_writer, $a_inst, $expLog);
3633 $this->bench->stop("ContentObjectExport", "exportPageObjects");
3634 $expLog->write(date("[y-m-d H:i:s] ") . "Finished Export Page Objects");
3635
3636 // MediaObjects
3637 $expLog->write(date("[y-m-d H:i:s] ") . "Start Export Media Objects");
3638 $this->bench->start("ContentObjectExport", "exportMediaObjects");
3639 $this->exportXMLMediaObjects($a_xml_writer, $a_inst, $a_target_dir, $expLog);
3640 $this->bench->stop("ContentObjectExport", "exportMediaObjects");
3641 $expLog->write(date("[y-m-d H:i:s] ") . "Finished Export Media Objects");
3642
3643 // FileItems
3644 $expLog->write(date("[y-m-d H:i:s] ") . "Start Export File Items");
3645 $this->bench->start("ContentObjectExport", "exportFileItems");
3646 $this->exportFileItems($a_target_dir, $expLog);
3647 $this->bench->stop("ContentObjectExport", "exportFileItems");
3648 $expLog->write(date("[y-m-d H:i:s] ") . "Finished Export File Items");
3649 }
3650
3656 public function modifyExportIdentifier($a_tag, $a_param, $a_value)
3657 {
3658 if ($a_tag == "Identifier" && $a_param == "Entry") {
3659 $a_value = ilUtil::insertInstIntoID($a_value);
3660 }
3661
3662 return $a_value;
3663 }
3664
3665
3672 public function exportXMLPageObjects(&$a_xml_writer, $inst, &$expLog)
3673 {
3674 foreach ($this->questions as $question_id) {
3675 $this->bench->start("ContentObjectExport", "exportPageObject");
3676 $expLog->write(date("[y-m-d H:i:s] ") . "Page Object " . $question_id);
3677
3678 $attrs = [];
3679 $a_xml_writer->xmlStartTag("PageObject", $attrs);
3680
3681
3682 // export xml to writer object
3683 $this->bench->start("ContentObjectExport", "exportPageObject_XML");
3684 $page_object = new ilAssQuestionPage($question_id);
3685 $page_object->buildDom();
3686 $page_object->insertInstIntoIDs((string) $inst);
3687 $mob_ids = $page_object->collectMediaObjects(false);
3688 $file_ids = ilPCFileList::collectFileItems($page_object, $page_object->getDomDoc());
3689 $xml = $page_object->getXMLFromDom(false, false, false, "", true);
3690 $xml = str_replace("&", "&amp;", $xml);
3691 $a_xml_writer->appendXML($xml);
3692 $page_object->freeDom();
3693 unset($page_object);
3694
3695 $this->bench->stop("ContentObjectExport", "exportPageObject_XML");
3696
3697 // collect media objects
3698 $this->bench->start("ContentObjectExport", "exportPageObject_CollectMedia");
3699 //$mob_ids = $page_obj->getMediaObjectIDs();
3700 foreach ($mob_ids as $mob_id) {
3701 $this->mob_ids[$mob_id] = $mob_id;
3702 }
3703 $this->bench->stop("ContentObjectExport", "exportPageObject_CollectMedia");
3704
3705 // collect all file items
3706 $this->bench->start("ContentObjectExport", "exportPageObject_CollectFileItems");
3707 //$file_ids = $page_obj->getFileItemIds();
3708 foreach ($file_ids as $file_id) {
3709 $this->file_ids[$file_id] = $file_id;
3710 }
3711 $this->bench->stop("ContentObjectExport", "exportPageObject_CollectFileItems");
3712
3713 $a_xml_writer->xmlEndTag("PageObject");
3714 //unset($page_obj);
3715
3716 $this->bench->stop("ContentObjectExport", "exportPageObject");
3717 }
3718 }
3719
3723 public function exportXMLMediaObjects(&$a_xml_writer, $a_inst, $a_target_dir, &$expLog)
3724 {
3725 foreach ($this->mob_ids as $mob_id) {
3726 $expLog->write(date("[y-m-d H:i:s] ") . "Media Object " . $mob_id);
3727 if (ilObjMediaObject::_exists((int) $mob_id)) {
3728 $target_dir = $a_target_dir . DIRECTORY_SEPARATOR . 'objects'
3729 . DIRECTORY_SEPARATOR . 'il_' . IL_INST_ID . '_mob_' . $mob_id;
3730 ilFileUtils::createDirectory($target_dir);
3731 $media_obj = new ilObjMediaObject((int) $mob_id);
3732 $media_obj->exportXML($a_xml_writer, (int) $a_inst);
3733 foreach ($media_obj->getMediaItems() as $item) {
3734 $stream = $item->getLocationStream();
3735 file_put_contents($target_dir . DIRECTORY_SEPARATOR . $item->getLocation(), $stream);
3736 $stream->close();
3737 }
3738 unset($media_obj);
3739 }
3740 }
3741 }
3742
3747 public function exportFileItems($target_dir, &$expLog)
3748 {
3749 foreach ($this->file_ids as $file_id) {
3750 $expLog->write(date("[y-m-d H:i:s] ") . "File Item " . $file_id);
3751 $file_dir = $target_dir . '/objects/il_' . IL_INST_ID . '_file_' . $file_id;
3752 ilFileUtils::makeDir($file_dir);
3753 $file_obj = new ilObjFile((int) $file_id, false);
3754 $source_file = $file_obj->getFile($file_obj->getVersion());
3755 if (!is_file($source_file)) {
3756 $source_file = $file_obj->getFile();
3757 }
3758 if (is_file($source_file)) {
3759 copy($source_file, $file_dir . '/' . $file_obj->getFileName());
3760 }
3761 unset($file_obj);
3762 }
3763 }
3764
3769 public function getImportMapping(): array
3770 {
3771 return [];
3772 }
3773
3776 public function onMarkSchemaSaved(): void
3777 {
3778 $this->saveCompleteStatus($this->question_set_config_factory->getQuestionSetConfig());
3779
3780 if ($this->participantDataExist()) {
3781 $this->recalculateScores(true);
3782 }
3783 }
3784
3788 public function marksEditable(): bool
3789 {
3790 $total = $this->evalTotalPersons();
3791 $results_summary_settings = $this->getScoreSettings()->getResultSummarySettings();
3792 if ($total === 0
3793 || $results_summary_settings->getScoreReporting()->isReportingEnabled() === false) {
3794 return true;
3795 }
3796
3797 if ($results_summary_settings->getScoreReporting() === ScoreReportingTypes::SCORE_REPORTING_DATE) {
3798 return $results_summary_settings->getReportingDate()
3799 >= new DateTimeImmutable('now', new DateTimeZone('UTC'));
3800 }
3801
3802 return false;
3803 }
3804
3814 public function saveAuthorToMetadata($author = "")
3815 {
3816 $path_to_lifecycle = $this->lo_metadata->paths()->custom()->withNextStep('lifeCycle')->get();
3817 $path_to_authors = $this->lo_metadata->paths()->authors();
3818
3819 $reader = $this->lo_metadata->read($this->getId(), 0, $this->getType(), $path_to_lifecycle);
3820 if (!is_null($reader->allData($path_to_lifecycle)->current())) {
3821 return;
3822 }
3823
3824 if ($author === '') {
3825 $author = $this->user->getFullname();
3826 }
3827 $this->lo_metadata->manipulate($this->getId(), 0, $this->getType())
3828 ->prepareCreateOrUpdate($path_to_authors, $author)
3829 ->execute();
3830 }
3831
3835 protected function doCreateMetaData(): void
3836 {
3837 $this->saveAuthorToMetadata();
3838 }
3839
3847 public function getAuthor(): string
3848 {
3849 $path_to_authors = $this->lo_metadata->paths()->authors();
3850 $author_data = $this->lo_metadata->read($this->getId(), 0, $this->getType(), $path_to_authors)
3851 ->allData($path_to_authors);
3852
3853 return $this->lo_metadata->dataHelper()->makePresentableAsList(', ', ...$author_data);
3854 }
3855
3863 public static function _lookupAuthor($obj_id): string
3864 {
3865 global $DIC;
3866
3867 $lo_metadata = $DIC->learningObjectMetadata();
3868
3869 $path_to_authors = $lo_metadata->paths()->authors();
3870 $author_data = $lo_metadata->read($obj_id, 0, "tst", $path_to_authors)
3871 ->allData($path_to_authors);
3872
3873 return $lo_metadata->dataHelper()->makePresentableAsList(',', ...$author_data);
3874 }
3875
3882 public static function _getAvailableTests($use_object_id = false): array
3883 {
3884 global $DIC;
3885 $ilUser = $DIC['ilUser'];
3886
3887 $result_array = [];
3888 $tests = array_slice(
3889 array_reverse(
3890 ilUtil::_getObjectsByOperations("tst", "write", $ilUser->getId(), PHP_INT_MAX)
3891 ),
3892 0,
3893 10000
3894 );
3895
3896 if (count($tests)) {
3897 $titles = ilObject::_prepareCloneSelection($tests, "tst");
3898 foreach ($tests as $ref_id) {
3899 if ($use_object_id) {
3901 $result_array[$obj_id] = $titles[$ref_id];
3902 } else {
3903 $result_array[$ref_id] = $titles[$ref_id];
3904 }
3905 }
3906 }
3907 return $result_array;
3908 }
3909
3918 public function cloneObject(int $target_id, int $copy_id = 0, bool $omit_tree = false): ?ilObject
3919 {
3920 $this->loadFromDb();
3921
3922 $new_obj = parent::cloneObject($target_id, $copy_id, $omit_tree);
3923 $new_obj->setTmpCopyWizardCopyId($copy_id);
3924 $this->cloneMetaData($new_obj);
3925
3926 $new_obj->saveToDb();
3927 $new_obj->addToNewsOnOnline(false, $new_obj->getObjectProperties()->getPropertyIsOnline()->getIsOnline());
3928
3929 $new_main_settings = $this->getMainSettings()
3930 ->withIntroductionSettings(
3931 $this->getMainSettings()->getIntroductionSettings()->withIntroductionPageId(
3932 $this->cloneIntroduction()
3933 )
3934 )->withFinishingSettings(
3935 $this->getMainSettings()->getFinishingSettings()->withConcludingRemarksPageId(
3936 $this->cloneConcludingRemarks()
3937 )
3938 );
3939
3940 $new_main_settings = $this->getMainSettingsRepository()->store($new_main_settings, $new_obj->getTestId());
3941 $this->getScoreSettingsRepository()->store(
3942 $this->getScoreSettings()->withId($new_main_settings->getId())
3943 );
3944 $this->marks_repository->storeMarkSchema(
3945 $this->getMarkSchema()->withTestId($new_obj->getTestId())
3946 );
3947
3948 $new_obj->setTemplate($this->getTemplate());
3949
3950 // clone certificate
3951 $pathFactory = new ilCertificatePathFactory();
3952 $templateRepository = new ilCertificateTemplateDatabaseRepository($this->db);
3953
3954 $cloneAction = new ilCertificateCloneAction(
3955 $this->db,
3956 $pathFactory,
3957 $templateRepository,
3959 );
3960
3961 $cloneAction->cloneCertificate($this, $new_obj);
3962
3963 $this->question_set_config_factory->getQuestionSetConfig()->cloneQuestionSetRelatedData($new_obj);
3964 $new_obj->saveQuestionsToDb();
3965
3966 $skillLevelThresholdList = new ilTestSkillLevelThresholdList($this->db);
3967 $skillLevelThresholdList->setTestId($this->getTestId());
3968 $skillLevelThresholdList->loadFromDb();
3969 $skillLevelThresholdList->cloneListForTest($new_obj->getTestId());
3970
3971 $obj_settings = new ilLPObjSettings($this->getId());
3972 $obj_settings->cloneSettings($new_obj->getId());
3973
3974 if ($new_obj->getTestLogger()->isLoggingEnabled()) {
3975 $new_obj->getTestLogger()->logTestAdministrationInteraction(
3976 $new_obj->getTestLogger()->getInteractionFactory()->buildTestAdministrationInteraction(
3977 $new_obj->getRefId(),
3978 $this->user->getId(),
3979 TestAdministrationInteractionTypes::NEW_TEST_CREATED,
3980 []
3981 )
3982 );
3983 }
3984
3985 return $new_obj;
3986 }
3987
3988 public function getQuestionCount(): int
3989 {
3990 $num = 0;
3991
3992 if ($this->isRandomTest()) {
3993 $questionSetConfig = new ilTestRandomQuestionSetConfig(
3994 $this->tree,
3995 $this->db,
3996 $this->lng,
3997 $this->logger,
3998 $this->component_repository,
3999 $this,
4000 $this->questionrepository
4001 );
4002
4003 $questionSetConfig->loadFromDb();
4004
4005 if ($questionSetConfig->isQuestionAmountConfigurationModePerPool()) {
4006 $sourcePoolDefinitionList = new ilTestRandomQuestionSetSourcePoolDefinitionList(
4007 $this->db,
4008 $this,
4010 );
4011
4012 $sourcePoolDefinitionList->loadDefinitions();
4013
4014 if (is_int($sourcePoolDefinitionList->getQuestionAmount())) {
4015 $num = $sourcePoolDefinitionList->getQuestionAmount();
4016 }
4017 } elseif (is_int($questionSetConfig->getQuestionAmountPerTest())) {
4018 $num = $questionSetConfig->getQuestionAmountPerTest();
4019 }
4020 } else {
4021 $this->loadQuestions();
4022 $num = count($this->questions);
4023 }
4024
4025 return $num;
4026 }
4027
4029 {
4030 if ($this->isRandomTest()) {
4031 return $this->getQuestionCount();
4032 }
4033 return count($this->questions);
4034 }
4035
4043 public static function _getObjectIDFromTestID($test_id)
4044 {
4045 global $DIC;
4046 $ilDB = $DIC['ilDB'];
4047 $object_id = false;
4048 $result = $ilDB->queryF(
4049 "SELECT obj_fi FROM tst_tests WHERE test_id = %s",
4050 ['integer'],
4051 [$test_id]
4052 );
4053 if ($result->numRows()) {
4054 $row = $ilDB->fetchAssoc($result);
4055 $object_id = $row["obj_fi"];
4056 }
4057 return $object_id;
4058 }
4059
4067 public static function _getObjectIDFromActiveID($active_id)
4068 {
4069 global $DIC;
4070 $ilDB = $DIC['ilDB'];
4071 $object_id = false;
4072 $result = $ilDB->queryF(
4073 "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",
4074 ['integer'],
4075 [$active_id]
4076 );
4077 if ($result->numRows()) {
4078 $row = $ilDB->fetchAssoc($result);
4079 $object_id = $row["obj_fi"];
4080 }
4081 return $object_id;
4082 }
4083
4091 public static function _getTestIDFromObjectID($object_id)
4092 {
4093 global $DIC;
4094 $ilDB = $DIC['ilDB'];
4095 $test_id = false;
4096 $result = $ilDB->queryF(
4097 "SELECT test_id FROM tst_tests WHERE obj_fi = %s",
4098 ['integer'],
4099 [$object_id]
4100 );
4101 if ($result->numRows()) {
4102 $row = $ilDB->fetchAssoc($result);
4103 $test_id = $row["test_id"];
4104 }
4105 return $test_id;
4106 }
4107
4116 public function getTextAnswer($active_id, $question_id, $pass = null): string
4117 {
4118 if (($active_id) && ($question_id)) {
4119 if ($pass === null) {
4120 $pass = assQuestion::_getSolutionMaxPass($question_id, $active_id);
4121 }
4122 if ($pass === null) {
4123 return '';
4124 }
4125 $query = $this->db->queryF(
4126 "SELECT value1 FROM tst_solutions WHERE active_fi = %s AND question_fi = %s AND pass = %s",
4127 ['integer', 'integer', 'integer'],
4128 [$active_id, $question_id, $pass]
4129 );
4130 $result = $this->db->fetchAll($query);
4131 if (count($result) == 1) {
4132 return $result[0]["value1"];
4133 }
4134 }
4135 return '';
4136 }
4137
4145 public function getQuestiontext($question_id): string
4146 {
4147 $res = "";
4148 if ($question_id) {
4149 $result = $this->db->queryF(
4150 "SELECT question_text FROM qpl_questions WHERE question_id = %s",
4151 ['integer'],
4152 [$question_id]
4153 );
4154 if ($result->numRows() == 1) {
4155 $row = $this->db->fetchAssoc($result);
4156 $res = $row["question_text"];
4157 }
4158 }
4159 return $res;
4160 }
4161
4163 {
4164 $participant_list = new ilTestParticipantList($this, $this->user, $this->lng, $this->db);
4165 $participant_list->initializeFromDbRows($this->getTestParticipants());
4166
4167 return $participant_list;
4168 }
4169
4176 public function &getInvitedUsers(int $user_id = 0, $order = "login, lastname, firstname"): array
4177 {
4178 $result_array = [];
4179
4180 if ($this->getAnonymity()) {
4181 if ($user_id !== 0) {
4182 $result = $this->db->queryF(
4183 "SELECT tst_active.active_id, tst_active.tries, usr_id, %s login, %s lastname, %s firstname, " .
4184 "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 " .
4185 "LEFT JOIN tst_active ON tst_active.user_fi = tst_invited_user.user_fi AND tst_active.test_fi = tst_invited_user.test_fi " .
4186 "WHERE tst_invited_user.test_fi = %s and tst_invited_user.user_fi=usr_data.usr_id AND usr_data.usr_id=%s " .
4187 "ORDER BY $order",
4188 ['text', 'text', 'text', 'integer', 'integer'],
4189 ['', $this->lng->txt('anonymous'), '', $this->getTestId(), $user_id]
4190 );
4191 } else {
4192 $result = $this->db->queryF(
4193 "SELECT tst_active.active_id, tst_active.tries, usr_id, %s login, %s lastname, %s firstname, " .
4194 "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 " .
4195 "LEFT JOIN tst_active ON tst_active.user_fi = tst_invited_user.user_fi AND tst_active.test_fi = tst_invited_user.test_fi " .
4196 "WHERE tst_invited_user.test_fi = %s and tst_invited_user.user_fi=usr_data.usr_id " .
4197 "ORDER BY $order",
4198 ['text', 'text', 'text', 'integer'],
4199 ['', $this->lng->txt('anonymous'), '', $this->getTestId()]
4200 );
4201 }
4202 } else {
4203 if ($user_id !== 0) {
4204 $result = $this->db->queryF(
4205 "SELECT tst_active.active_id, tst_active.tries, usr_id, login, lastname, firstname, " .
4206 "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 " .
4207 "LEFT JOIN tst_active ON tst_active.user_fi = tst_invited_user.user_fi AND tst_active.test_fi = tst_invited_user.test_fi " .
4208 "WHERE tst_invited_user.test_fi = %s and tst_invited_user.user_fi=usr_data.usr_id AND usr_data.usr_id=%s " .
4209 "ORDER BY $order",
4210 ['integer', 'integer'],
4211 [$this->getTestId(), $user_id]
4212 );
4213 } else {
4214 $result = $this->db->queryF(
4215 "SELECT tst_active.active_id, tst_active.tries, usr_id, login, lastname, firstname, " .
4216 "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 " .
4217 "LEFT JOIN tst_active ON tst_active.user_fi = tst_invited_user.user_fi AND tst_active.test_fi = tst_invited_user.test_fi " .
4218 "WHERE tst_invited_user.test_fi = %s and tst_invited_user.user_fi=usr_data.usr_id " .
4219 "ORDER BY $order",
4220 ['integer'],
4221 [$this->getTestId()]
4222 );
4223 }
4224 }
4225 $result_array = [];
4226 while ($row = $this->db->fetchAssoc($result)) {
4227 $result_array[$row['usr_id']] = $row;
4228 }
4229 return $result_array;
4230 }
4231
4232 public function getTestParticipants(): array
4233 {
4234 if ($this->getMainSettings()->getGeneralSettings()->getAnonymity()) {
4235 $query = "
4236 SELECT tst_active.active_id,
4237 tst_active.tries,
4238 tst_active.user_fi usr_id,
4239 %s login,
4240 %s lastname,
4241 %s firstname,
4242 tst_active.submitted test_finished,
4243 usr_data.matriculation,
4244 usr_data.active,
4245 tst_active.lastindex,
4246 COALESCE(tst_active.last_finished_pass, -1) <> tst_active.last_started_pass unfinished_passes
4247 FROM tst_active
4248 LEFT JOIN usr_data
4249 ON tst_active.user_fi = usr_data.usr_id
4250 WHERE tst_active.test_fi = %s
4251 ORDER BY usr_data.lastname
4252 ";
4253 $result = $this->db->queryF(
4254 $query,
4255 ['text', 'text', 'text', 'integer'],
4256 ['', $this->lng->txt("anonymous"), "", $this->getTestId()]
4257 );
4258 } else {
4259 $query = "
4260 SELECT tst_active.active_id,
4261 tst_active.tries,
4262 tst_active.user_fi usr_id,
4263 usr_data.login,
4264 usr_data.lastname,
4265 usr_data.firstname,
4266 tst_active.submitted test_finished,
4267 usr_data.matriculation,
4268 usr_data.active,
4269 tst_active.lastindex,
4270 COALESCE(tst_active.last_finished_pass, -1) <> tst_active.last_started_pass unfinished_passes
4271 FROM tst_active
4272 LEFT JOIN usr_data
4273 ON tst_active.user_fi = usr_data.usr_id
4274 WHERE tst_active.test_fi = %s
4275 ORDER BY usr_data.lastname
4276 ";
4277 $result = $this->db->queryF(
4278 $query,
4279 ['integer'],
4280 [$this->getTestId()]
4281 );
4282 }
4283 $data = [];
4284 while ($row = $this->db->fetchAssoc($result)) {
4285 $data[$row['active_id']] = $row;
4286 }
4287 foreach ($data as $index => $participant) {
4288 if (strlen(trim($participant["firstname"] . $participant["lastname"])) == 0) {
4289 $data[$index]["lastname"] = $this->lng->txt("deleted_user");
4290 }
4291 }
4292 return $data;
4293 }
4294
4295 public function getTestParticipantsForManualScoring($filter = null): array
4296 {
4297 if (!$this->getGlobalSettings()->isManualScoringEnabled()) {
4298 return [];
4299 }
4300
4301 $filtered_participants = [];
4302 foreach ($this->getTestParticipants() as $active_id => $participant) {
4303 if ($participant['tries'] > 0) {
4304 switch ($filter) {
4305 case 4:
4306 if ($this->test_man_scoring_done_helper->isDone((int) $active_id)) {
4307 $filtered_participants[$active_id] = $participant;
4308 }
4309 break;
4310 case 5:
4311 if (!$this->test_man_scoring_done_helper->isDone((int) $active_id)) {
4312 $filtered_participants[$active_id] = $participant;
4313 }
4314 break;
4315 default:
4316 $filtered_participants[$active_id] = $participant;
4317 }
4318 }
4319 }
4320 return $filtered_participants;
4321 }
4322
4329 public function getUserData($ids): array
4330 {
4331 if (!is_array($ids) || count($ids) == 0) {
4332 return [];
4333 }
4334
4335 if ($this->getAnonymity()) {
4336 $result = $this->db->queryF(
4337 "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",
4338 ['text', 'text', 'text'],
4339 ["", $this->lng->txt("anonymous"), ""]
4340 );
4341 } else {
4342 $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");
4343 }
4344
4345 $result_array = [];
4346 while ($row = $this->db->fetchAssoc($result)) {
4347 $result_array[$row["usr_id"]] = $row;
4348 }
4349 return $result_array;
4350 }
4351
4352 public function getGroupData($ids): array
4353 {
4354 if (!is_array($ids) || count($ids) == 0) {
4355 return [];
4356 }
4357 $result = [];
4358 foreach ($ids as $ref_id) {
4360 $result[$ref_id] = ["ref_id" => $ref_id, "title" => ilObject::_lookupTitle($obj_id), "description" => ilObject::_lookupDescription($obj_id)];
4361 }
4362 return $result;
4363 }
4364
4365 public function getRoleData($ids): array
4366 {
4367 if (!is_array($ids) || count($ids) == 0) {
4368 return [];
4369 }
4370 $result = [];
4371 foreach ($ids as $obj_id) {
4372 $result[$obj_id] = ["obj_id" => $obj_id, "title" => ilObject::_lookupTitle($obj_id), "description" => ilObject::_lookupDescription($obj_id)];
4373 }
4374 return $result;
4375 }
4376
4383 public function inviteUser($user_id, $client_ip = "")
4384 {
4385 $this->db->manipulateF(
4386 "DELETE FROM tst_invited_user WHERE test_fi = %s AND user_fi = %s",
4387 ['integer', 'integer'],
4388 [$this->getTestId(), $user_id]
4389 );
4390 $this->db->manipulateF(
4391 "INSERT INTO tst_invited_user (test_fi, user_fi, ip_range_from, ip_range_to, tstamp) VALUES (%s, %s, %s, %s, %s)",
4392 ['integer', 'integer', 'text', 'text', 'integer'],
4393 [$this->getTestId(), $user_id, (strlen($client_ip)) ? $client_ip : null, (strlen($client_ip)) ? $client_ip : null,time()]
4394 );
4395 }
4396
4402 public static function _getSolvedQuestions($active_id, $question_fi = null): array
4403 {
4404 global $DIC;
4405 $ilDB = $DIC['ilDB'];
4406 if (is_numeric($question_fi)) {
4407 $result = $ilDB->queryF(
4408 "SELECT question_fi, solved FROM tst_qst_solved WHERE active_fi = %s AND question_fi=%s",
4409 ['integer', 'integer'],
4410 [$active_id, $question_fi]
4411 );
4412 } else {
4413 $result = $ilDB->queryF(
4414 "SELECT question_fi, solved FROM tst_qst_solved WHERE active_fi = %s",
4415 ['integer'],
4416 [$active_id]
4417 );
4418 }
4419 $result_array = [];
4420 while ($row = $ilDB->fetchAssoc($result)) {
4421 $result_array[$row["question_fi"]] = $row;
4422 }
4423 return $result_array;
4424 }
4425
4426
4430 public function setQuestionSetSolved($value, $question_id, $user_id)
4431 {
4432 $active_id = $this->getActiveIdOfUser($user_id);
4433 $this->db->manipulateF(
4434 "DELETE FROM tst_qst_solved WHERE active_fi = %s AND question_fi = %s",
4435 ['integer', 'integer'],
4436 [$active_id, $question_id]
4437 );
4438 $this->db->manipulateF(
4439 "INSERT INTO tst_qst_solved (solved, question_fi, active_fi) VALUES (%s, %s, %s)",
4440 ['integer', 'integer', 'integer'],
4441 [$value, $question_id, $active_id]
4442 );
4443 }
4444
4448 public function isTestFinished($active_id): bool
4449 {
4450 $result = $this->db->queryF(
4451 "SELECT submitted FROM tst_active WHERE active_id=%s AND submitted=%s",
4452 ['integer', 'integer'],
4453 [$active_id, 1]
4454 );
4455 return $result->numRows() == 1;
4456 }
4457
4461 public function isActiveTestSubmitted($user_id = null): bool
4462 {
4463 if (!is_numeric($user_id)) {
4464 $user_id = $this->user->getId();
4465 }
4466
4467 $result = $this->db->queryF(
4468 "SELECT submitted FROM tst_active WHERE test_fi=%s AND user_fi=%s AND submitted=%s",
4469 ['integer', 'integer', 'integer'],
4470 [$this->getTestId(), $user_id, 1]
4471 );
4472 return $result->numRows() == 1;
4473 }
4474
4478 public function hasNrOfTriesRestriction(): bool
4479 {
4480 return $this->getNrOfTries() != 0;
4481 }
4482
4483
4489 public function isNrOfTriesReached($tries): bool
4490 {
4491 return $tries >= $this->getNrOfTries();
4492 }
4493
4494
4502 public function getAllTestResults($participants): array
4503 {
4504 $results = [];
4505 $row = [
4506 "user_id" => $this->lng->txt("user_id"),
4507 "matriculation" => $this->lng->txt("matriculation"),
4508 "lastname" => $this->lng->txt("lastname"),
4509 "firstname" => $this->lng->txt("firstname"),
4510 "login" => $this->lng->txt("login"),
4511 "reached_points" => $this->lng->txt("tst_reached_points"),
4512 "max_points" => $this->lng->txt("tst_maximum_points"),
4513 "percent_value" => $this->lng->txt("tst_percent_solved"),
4514 "mark" => $this->lng->txt("tst_mark"),
4515 "passed" => $this->lng->txt("tst_mark_passed"),
4516 ];
4517 $results[] = $row;
4518 if (count($participants)) {
4519 foreach ($participants as $active_id => $user_rec) {
4520 $mark = '';
4521 $row = [];
4522 $reached_points = 0;
4523 $max_points = 0;
4524 $pass = ilObjTest::_getResultPass($active_id);
4525 // abort if no valid pass can be found
4526 if (!is_int($pass)) {
4527 continue;
4528 }
4529 foreach ($this->questions as $value) {
4530 $question = ilObjTest::_instanciateQuestion($value);
4531 if (is_object($question)) {
4532 $max_points += $question->getMaximumPoints();
4533 $reached_points += $question->getReachedPoints($active_id, $pass);
4534 }
4535 }
4536 if ($max_points > 0) {
4537 $percentvalue = $reached_points / $max_points;
4538 if ($percentvalue < 0) {
4539 $percentvalue = 0.0;
4540 }
4541 } else {
4542 $percentvalue = 0;
4543 }
4544 $mark_obj = $this->getMarkSchema()->getMatchingMark($percentvalue * 100);
4545 $passed = "";
4546 if ($mark_obj !== null) {
4547 $mark = $mark_obj->getOfficialName();
4548 }
4549 if ($this->getAnonymity()) {
4550 $user_rec['firstname'] = "";
4551 $user_rec['lastname'] = $this->lng->txt("anonymous");
4552 }
4553 $results[] = [
4554 "user_id" => $user_rec['usr_id'],
4555 "matriculation" => $user_rec['matriculation'],
4556 "lastname" => $user_rec['lastname'],
4557 "firstname" => $user_rec['firstname'],
4558 "login" => $user_rec['login'],
4559 "reached_points" => $reached_points,
4560 "max_points" => $max_points,
4561 "percent_value" => $percentvalue,
4562 "mark" => $mark,
4563 "passed" => $user_rec['passed'] ? '1' : '0',
4564 ];
4565 }
4566 }
4567 return $results;
4568 }
4569
4578 public static function _getPass($active_id): int
4579 {
4580 global $DIC;
4581 $ilDB = $DIC['ilDB'];
4582 $result = $ilDB->queryF(
4583 "SELECT tries FROM tst_active WHERE active_id = %s",
4584 ['integer'],
4585 [$active_id]
4586 );
4587 if ($result->numRows()) {
4588 $row = $ilDB->fetchAssoc($result);
4589 return $row["tries"];
4590 } else {
4591 return 0;
4592 }
4593 }
4594
4604 public static function _getMaxPass($active_id): ?int
4605 {
4606 global $DIC;
4607 $ilDB = $DIC['ilDB'];
4608 $result = $ilDB->queryF(
4609 "SELECT MAX(pass) maxpass FROM tst_pass_result WHERE active_fi = %s",
4610 ['integer'],
4611 [$active_id]
4612 );
4613
4614 if ($result->numRows()) {
4615 $row = $ilDB->fetchAssoc($result);
4616 return $row["maxpass"];
4617 }
4618
4619 return null;
4620 }
4621
4627 public static function _getBestPass($active_id): ?int
4628 {
4629 global $DIC;
4630 $ilDB = $DIC['ilDB'];
4631
4632 $result = $ilDB->queryF(
4633 "SELECT * FROM tst_pass_result WHERE active_fi = %s",
4634 ['integer'],
4635 [$active_id]
4636 );
4637
4638 if (!$result->numRows()) {
4639 return null;
4640 }
4641
4642 $bestrow = null;
4643 $bestfactor = 0.0;
4644 while ($row = $ilDB->fetchAssoc($result)) {
4645 if ($row["maxpoints"] > 0.0) {
4646 $factor = (float) ($row["points"] / $row["maxpoints"]);
4647 } else {
4648 $factor = 0.0;
4649 }
4650 if ($factor === 0.0 && $bestfactor === 0.0
4651 || $factor > $bestfactor) {
4652 $bestrow = $row;
4653 $bestfactor = $factor;
4654 }
4655 }
4656
4657 if (is_array($bestrow)) {
4658 return $bestrow["pass"];
4659 }
4660
4661 return null;
4662 }
4663
4672 public static function _getResultPass($active_id): ?int
4673 {
4674 $counted_pass = null;
4675 if (ilObjTest::_getPassScoring($active_id) == self::SCORE_BEST_PASS) {
4676 $counted_pass = ilObjTest::_getBestPass($active_id);
4677 } else {
4678 $counted_pass = ilObjTest::_getMaxPass($active_id);
4679 }
4680 return $counted_pass;
4681 }
4682
4692 public function getAnsweredQuestionCount($active_id, $pass = null): int
4693 {
4694 if ($this->isRandomTest()) {
4695 $this->loadQuestions($active_id, $pass);
4696 }
4697 $workedthrough = 0;
4698 foreach ($this->questions as $value) {
4699 if ($this->questionrepository->lookupResultRecordExist($active_id, $value, $pass)) {
4700 $workedthrough += 1;
4701 }
4702 }
4703 return $workedthrough;
4704 }
4705
4712 public static function lookupPassResultsUpdateTimestamp($active_id, $pass): int
4713 {
4714 global $DIC;
4715 $ilDB = $DIC['ilDB'];
4716
4717 if (is_null($pass)) {
4718 $pass = 0;
4719 }
4720
4721 $query = "
4722 SELECT tst_pass_result.tstamp pass_res_tstamp,
4723 tst_test_result.tstamp quest_res_tstamp
4724
4725 FROM tst_pass_result
4726
4727 LEFT JOIN tst_test_result
4728 ON tst_test_result.active_fi = tst_pass_result.active_fi
4729 AND tst_test_result.pass = tst_pass_result.pass
4730
4731 WHERE tst_pass_result.active_fi = %s
4732 AND tst_pass_result.pass = %s
4733
4734 ORDER BY tst_test_result.tstamp DESC
4735 ";
4736
4737 $result = $ilDB->queryF(
4738 $query,
4739 ['integer', 'integer'],
4740 [$active_id, $pass]
4741 );
4742
4743 while ($row = $ilDB->fetchAssoc($result)) {
4744 if ($row['quest_res_tstamp']) {
4745 return $row['quest_res_tstamp'];
4746 }
4747
4748 return $row['pass_res_tstamp'];
4749 }
4750
4751 return 0;
4752 }
4753
4762 public function isExecutable($test_session, $user_id, $allow_pass_increase = false): array
4763 {
4764 $result = [
4765 "executable" => true,
4766 "errormessage" => ""
4767 ];
4768
4769 if (!$this->getObjectProperties()->getPropertyIsOnline()->getIsOnline()) {
4770 $result["executable"] = false;
4771 $result["errormessage"] = $this->lng->txt('autosave_failed') . ': ' . $this->lng->txt('offline');
4772 return $result;
4773 }
4774
4775 if (!$this->startingTimeReached()) {
4776 $result["executable"] = false;
4777 $result["errormessage"] = sprintf($this->lng->txt("detail_starting_time_not_reached"), ilDatePresentation::formatDate(new ilDateTime($this->getStartingTime(), IL_CAL_UNIX)));
4778 return $result;
4779 }
4780 if ($this->endingTimeReached()) {
4781 $result["executable"] = false;
4782 $result["errormessage"] = sprintf($this->lng->txt("detail_ending_time_reached"), ilDatePresentation::formatDate(new ilDateTime($this->getEndingTime(), IL_CAL_UNIX)));
4783 return $result;
4784 }
4785
4786 $active_id = $this->getActiveIdOfUser($user_id);
4787
4788 if ($this->getEnableProcessingTime()
4789 && $active_id > 0
4790 && ($starting_time = $this->getStartingTimeOfUser($active_id)) !== false
4791 && $this->isMaxProcessingTimeReached($starting_time, $active_id)) {
4792 $result["executable"] = false;
4793 $result["errormessage"] = $this->lng->txt("detail_max_processing_time_reached");
4794 return $result;
4795 }
4796
4797 $testPassesSelector = new ilTestPassesSelector($this->db, $this);
4798 $testPassesSelector->setActiveId($active_id);
4799 $testPassesSelector->setLastFinishedPass($test_session->getLastFinishedPass());
4800
4801 if ($this->hasNrOfTriesRestriction() && ($active_id > 0)) {
4802 $closedPasses = $testPassesSelector->getClosedPasses();
4803
4804 if (count($closedPasses) >= $this->getNrOfTries()) {
4805 $result["executable"] = false;
4806 $result["errormessage"] = $this->lng->txt("maximum_nr_of_tries_reached");
4807 return $result;
4808 }
4809
4810 if ($this->isBlockPassesAfterPassedEnabled() && !$testPassesSelector->openPassExists()) {
4811 if ($this->test_result_repository->isPassed($user_id, $this->getId())) {
4812 $result['executable'] = false;
4813 $result['errormessage'] = $this->lng->txt("tst_addit_passes_blocked_after_passed_msg");
4814 return $result;
4815 }
4816 }
4817 }
4818
4819 $next_pass_allowed_timestamp = 0;
4820 if (!$this->isNextPassAllowed($testPassesSelector, $next_pass_allowed_timestamp)) {
4821 $date = ilDatePresentation::formatDate(new ilDateTime($next_pass_allowed_timestamp, IL_CAL_UNIX));
4822
4823 $result['executable'] = false;
4824 $result['errormessage'] = sprintf($this->lng->txt('wait_for_next_pass_hint_msg'), $date);
4825 return $result;
4826 }
4827 return $result;
4828 }
4829
4830 public function isNextPassAllowed(ilTestPassesSelector $testPassesSelector, int &$next_pass_allowed_timestamp): bool
4831 {
4832 $waiting_between_passes = $this->getMainSettings()->getTestBehaviourSettings()->getPassWaiting();
4833 $last_finished_pass_timestamp = $testPassesSelector->getLastFinishedPassTimestamp();
4834
4835 if (
4836 $this->getMainSettings()->getTestBehaviourSettings()->getPassWaitingEnabled()
4837 && ($waiting_between_passes !== '')
4838 && ($testPassesSelector->getLastFinishedPass() !== null)
4839 && ($last_finished_pass_timestamp !== null)
4840 ) {
4841 $time_values = explode(':', $waiting_between_passes);
4842 $next_pass_allowed_timestamp = strtotime('+ ' . $time_values[0] . ' Days + ' . $time_values[1] . ' Hours' . $time_values[2] . ' Minutes', $last_finished_pass_timestamp);
4843 return (time() > $next_pass_allowed_timestamp);
4844 }
4845
4846 return true;
4847 }
4848
4849 public function canShowTestResults(ilTestSession $test_session): bool
4850 {
4851 $pass_selector = new ilTestPassesSelector($this->db, $this);
4852
4853 $pass_selector->setActiveId($test_session->getActiveId());
4854 $pass_selector->setLastFinishedPass($test_session->getLastFinishedPass());
4855
4856 return $pass_selector->hasReportablePasses();
4857 }
4858
4859 public function hasAnyTestResult(ilTestSession $test_session): bool
4860 {
4861 $pass_selector = new ilTestPassesSelector($this->db, $this);
4862
4863 $pass_selector->setActiveId($test_session->getActiveId());
4864 $pass_selector->setLastFinishedPass($test_session->getLastFinishedPass());
4865
4866 return $pass_selector->hasExistingPasses();
4867 }
4868
4876 public function getStartingTimeOfUser($active_id, $pass = null)
4877 {
4878 if ($active_id < 1) {
4879 return false;
4880 }
4881 if ($pass === null) {
4882 $pass = ($this->getResetProcessingTime()) ? self::_getPass($active_id) : 0;
4883 }
4884 $result = $this->db->queryF(
4885 "SELECT tst_times.started FROM tst_times WHERE tst_times.active_fi = %s AND tst_times.pass = %s ORDER BY tst_times.started",
4886 ['integer', 'integer'],
4887 [$active_id, $pass]
4888 );
4889 if ($result->numRows()) {
4890 $row = $this->db->fetchAssoc($result);
4891 if (preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["started"], $matches)) {
4892 return mktime(
4893 (int) $matches[4],
4894 (int) $matches[5],
4895 (int) $matches[6],
4896 (int) $matches[2],
4897 (int) $matches[3],
4898 (int) $matches[1]
4899 );
4900 } else {
4901 return time();
4902 }
4903 } else {
4904 return time();
4905 }
4906 }
4907
4914 public function isMaxProcessingTimeReached(int $starting_time, int $active_id): bool
4915 {
4916 if (!$this->getEnableProcessingTime()) {
4917 return false;
4918 }
4919
4920 $processing_time = $this->getProcessingTimeInSeconds($active_id);
4921 $now = time();
4922 if ($now > ($starting_time + $processing_time)) {
4923 return true;
4924 }
4925
4926 return false;
4927 }
4928
4929 public function getTestQuestions(): array
4930 {
4931 $tags_trafo = $this->refinery->string()->stripTags();
4932
4933 $query = "
4934 SELECT questions.*,
4935 questtypes.type_tag,
4936 tstquest.sequence,
4937 origquest.obj_fi orig_obj_fi
4938
4939 FROM qpl_questions questions
4940
4941 INNER JOIN qpl_qst_type questtypes
4942 ON questtypes.question_type_id = questions.question_type_fi
4943
4944 INNER JOIN tst_test_question tstquest
4945 ON tstquest.question_fi = questions.question_id
4946
4947 LEFT JOIN qpl_questions origquest
4948 ON origquest.question_id = questions.original_id
4949
4950 WHERE tstquest.test_fi = %s
4951
4952 ORDER BY tstquest.sequence
4953 ";
4954
4955 $query_result = $this->db->queryF(
4956 $query,
4957 ['integer'],
4958 [$this->getTestId()]
4959 );
4960
4961 $questions = [];
4962
4963 while ($row = $this->db->fetchAssoc($query_result)) {
4964 $row['title'] = $tags_trafo->transform($row['title']);
4965 $row['description'] = $tags_trafo->transform($row['description'] !== '' && $row['description'] !== null ? $row['description'] : '&nbsp;');
4966 $row['author'] = $tags_trafo->transform($row['author']);
4967
4968 $questions[] = $row;
4969 }
4970
4971 return $questions;
4972 }
4973
4974 public function isTestQuestion(int $question_id): bool
4975 {
4976 foreach ($this->getTestQuestions() as $questionData) {
4977 if ($questionData['question_id'] != $question_id) {
4978 continue;
4979 }
4980
4981 return true;
4982 }
4983
4984 return false;
4985 }
4986
4987 public function checkQuestionParent(int $question_id): bool
4988 {
4989 $row = $this->db->fetchAssoc($this->db->queryF(
4990 "SELECT COUNT(question_id) cnt FROM qpl_questions WHERE question_id = %s AND obj_fi = %s",
4991 ['integer', 'integer'],
4992 [$question_id, $this->getId()]
4993 ));
4994
4995 return (bool) $row['cnt'];
4996 }
4997
4998 public function getFixedQuestionSetTotalPoints(): float
4999 {
5000 $points = 0;
5001
5002 foreach ($this->getTestQuestions() as $question_data) {
5003 $points += $question_data['points'];
5004 }
5005
5006 return $points;
5007 }
5008
5012 public function getPotentialRandomTestQuestions(): array
5013 {
5014 $query = "
5015 SELECT questions.*,
5016 questtypes.type_tag,
5017 origquest.obj_fi orig_obj_fi
5018
5019 FROM qpl_questions questions
5020
5021 INNER JOIN qpl_qst_type questtypes
5022 ON questtypes.question_type_id = questions.question_type_fi
5023
5024 INNER JOIN tst_rnd_cpy tstquest
5025 ON tstquest.qst_fi = questions.question_id
5026
5027 LEFT JOIN qpl_questions origquest
5028 ON origquest.question_id = questions.original_id
5029
5030 WHERE tstquest.tst_fi = %s
5031 ";
5032
5033 $query_result = $this->db->queryF(
5034 $query,
5035 ['integer'],
5036 [$this->getTestId()]
5037 );
5038
5039 return $this->db->fetchAll($query_result);
5040 }
5041
5042 public function getShuffleQuestions(): bool
5043 {
5044 return $this->getMainSettings()->getQuestionBehaviourSettings()->getShuffleQuestions();
5045 }
5046
5059 {
5060 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getUsrPassOverviewMode();
5061 }
5062
5063 public function getListOfQuestions(): bool
5064 {
5065 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getQuestionListEnabled();
5066 }
5067
5068 public function getUsrPassOverviewEnabled(): bool
5069 {
5070 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getUsrPassOverviewEnabled();
5071 }
5072
5073 public function getListOfQuestionsStart(): bool
5074 {
5075 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getShownQuestionListAtBeginning();
5076 }
5077
5078 public function getListOfQuestionsEnd(): bool
5079 {
5080 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getShownQuestionListAtEnd();
5081 }
5082
5083 public function getListOfQuestionsDescription(): bool
5084 {
5085 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getShowDescriptionInQuestionList();
5086 }
5087
5091 public function getShowPassDetails(): bool
5092 {
5093 return $this->getScoreSettings()->getResultDetailsSettings()->getShowPassDetails();
5094 }
5095
5099 public function getShowSolutionPrintview(): bool
5100 {
5101 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionPrintview();
5102 }
5106 public function canShowSolutionPrintview($user_id = null): bool
5107 {
5108 return $this->getShowSolutionPrintview();
5109 }
5110
5114 public function getShowSolutionFeedback(): bool
5115 {
5116 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionFeedback();
5117 }
5118
5122 public function getShowSolutionAnswersOnly(): bool
5123 {
5124 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionAnswersOnly();
5125 }
5126
5130 public function getShowSolutionSignature(): bool
5131 {
5132 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionSignature();
5133 }
5134
5138 public function getShowSolutionSuggested(): bool
5139 {
5140 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionSuggested();
5141 }
5142
5147 public function getShowSolutionListComparison(): bool
5148 {
5149 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionListComparison();
5150 }
5151
5152 public function getShowSolutionListOwnAnswers(): bool
5153 {
5154 return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionListOwnAnswers();
5155 }
5156
5160 public static function _getUserIdFromActiveId(int $active_id): int
5161 {
5162 global $DIC;
5163 $ilDB = $DIC['ilDB'];
5164 $result = $ilDB->queryF(
5165 "SELECT user_fi FROM tst_active WHERE active_id = %s",
5166 ['integer'],
5167 [$active_id]
5168 );
5169 if ($result->numRows()) {
5170 $row = $ilDB->fetchAssoc($result);
5171 return $row["user_fi"];
5172 } else {
5173 return -1;
5174 }
5175 }
5176
5177 public function _getLastAccess(int $active_id): string
5178 {
5179 $result = $this->db->queryF(
5180 "SELECT finished FROM tst_times WHERE active_fi = %s ORDER BY finished DESC",
5181 ['integer'],
5182 [$active_id]
5183 );
5184 if ($result->numRows()) {
5185 $row = $this->db->fetchAssoc($result);
5186 return $row["finished"];
5187 }
5188 return "";
5189 }
5190
5191 public static function lookupLastTestPassAccess(int $active_id, int $pass_index): ?int
5192 {
5194 global $DIC;
5195 $ilDB = $DIC['ilDB'];
5196
5197 $query = "
5198 SELECT MAX(tst_times.tstamp) as last_pass_access
5199 FROM tst_times
5200 WHERE active_fi = %s
5201 AND pass = %s
5202 ";
5203
5204 $res = $ilDB->queryF(
5205 $query,
5206 ['integer', 'integer'],
5207 [$active_id, $pass_index]
5208 );
5209
5210 while ($row = $ilDB->fetchAssoc($res)) {
5211 return $row['last_pass_access'];
5212 }
5213
5214 return null;
5215 }
5216
5224 public function isHTML($a_text): bool
5225 {
5226 if (preg_match("/<[^>]*?>/", $a_text)) {
5227 return true;
5228 } else {
5229 return false;
5230 }
5231 }
5232
5239 public function qtiMaterialToArray($a_material): array
5240 {
5241 $result = '';
5242 $mobs = [];
5243 for ($i = 0; $i < $a_material->getMaterialCount(); $i++) {
5244 $material = $a_material->getMaterial($i);
5245 if ($material['type'] === 'mattext') {
5246 $result .= $material['material']->getContent();
5247 }
5248 if ($material['type'] === 'matimage') {
5249 $matimage = $material['material'];
5250 if (preg_match('/(il_([0-9]+)_mob_([0-9]+))/', $matimage->getLabel(), $matches)) {
5251 $mobs[] = [
5252 'mob' => $matimage->getLabel(),
5253 'uri' => $matimage->getUri()
5254 ];
5255 }
5256 }
5257 }
5258
5259 $decoded_result = base64_decode($result);
5260 if (str_starts_with($decoded_result, '<PageObject>')) {
5261 $result = $decoded_result;
5262 }
5263
5264 return [
5265 'text' => $result,
5266 'mobs' => $mobs
5267 ];
5268 }
5269
5270 public function addQTIMaterial(ilXmlWriter &$xml_writer, ?int $page_id, string $material = ''): void
5271 {
5272 $xml_writer->xmlStartTag('material');
5273 $attrs = [
5274 'texttype' => 'text/plain'
5275 ];
5276 $file_ids = [];
5277 $mobs = [];
5278 if ($page_id !== null) {
5279 $attrs['texttype'] = 'text/xml';
5280 $mobs = ilObjMediaObject::_getMobsOfObject('tst:pg', $page_id);
5281 $page_object = new ilTestPage($page_id);
5282 $page_object->buildDom();
5283 $page_object->insertInstIntoIDs((string) IL_INST_ID);
5284 $material = base64_encode($page_object->getXMLFromDom());
5285 $file_ids = ilPCFileList::collectFileItems($page_object, $page_object->getDomDoc());
5286 foreach ($file_ids as $file_id) {
5287 $this->file_ids[] = (int) $file_id;
5288 };
5289 $mob_string = 'il_' . IL_INST_ID . '_mob_';
5290 } elseif ($this->isHTML($material)) {
5291 $attrs['texttype'] = 'text/xhtml';
5292 $mobs = ilObjMediaObject::_getMobsOfObject('tst:html', $this->getId());
5293 $mob_string = 'mm_';
5294 }
5295
5296 $xml_writer->xmlElement('mattext', $attrs, $material);
5297 foreach ($mobs as $mob) {
5298 $mob_id_string = (string) $mob;
5299 $moblabel = 'il_' . IL_INST_ID . '_mob_' . $mob_id_string;
5300 if (strpos($material, $mob_string . $mob_id_string) !== false) {
5301 if (ilObjMediaObject::_exists($mob)) {
5302 $mob_obj = new ilObjMediaObject($mob);
5303 $imgattrs = [
5304 'label' => $moblabel,
5305 'uri' => 'objects/' . 'il_' . IL_INST_ID . '_mob_' . $mob_id_string . '/' . $mob_obj->getTitle()
5306 ];
5307 }
5308 $xml_writer->xmlElement('matimage', $imgattrs, null);
5309 }
5310 }
5311 $xml_writer->xmlEndTag('material');
5312 }
5313
5320 public function prepareTextareaOutput($txt_output, $prepare_for_latex_output = false, $omitNl2BrWhenTextArea = false)
5321 {
5322 if ($txt_output == null) {
5323 $txt_output = '';
5324 }
5326 $txt_output,
5327 $prepare_for_latex_output,
5328 $omitNl2BrWhenTextArea
5329 );
5330 }
5331
5332 public function getAnonymity(): bool
5333 {
5334 return $this->getMainSettings()->getGeneralSettings()->getAnonymity();
5335 }
5336
5337 public function getShowCancel(): bool
5338 {
5339 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getSuspendTestAllowed();
5340 }
5341
5342 public function getShowMarker(): bool
5343 {
5344 return $this->getMainSettings()->getParticipantFunctionalitySettings()->getQuestionMarkingEnabled();
5345 }
5346
5347 public function getFixedParticipants(): bool
5348 {
5349 return $this->getMainSettings()->getAccessSettings()->getFixedParticipants();
5350 }
5351
5352 public function lookupQuestionSetTypeByActiveId(int $active_id): string
5353 {
5354 return $this->main_settings_repository->getFor(
5355 self::_getTestIDFromObjectID(self::_getObjectIDFromActiveID($active_id)),
5356 )->getGeneralSettings()->getQuestionSetType();
5357 }
5358
5369 public function userLookupFullName($user_id, $overwrite_anonymity = false, $sorted_order = false, $suffix = ""): string
5370 {
5371 if ($this->getAnonymity() && !$overwrite_anonymity) {
5372 return $this->lng->txt("anonymous") . $suffix;
5373 } else {
5375 if (strlen($uname["firstname"] . $uname["lastname"]) == 0) {
5376 $uname["firstname"] = $this->lng->txt("deleted_user");
5377 }
5378 if ($sorted_order) {
5379 return trim($uname["lastname"] . ", " . $uname["firstname"]) . $suffix;
5380 } else {
5381 return trim($uname["firstname"] . " " . $uname["lastname"]) . $suffix;
5382 }
5383 }
5384 }
5385
5387 DateTimeImmutable|int|null $date_time
5388 ): ?DateTimeImmutable {
5389 if ($date_time === null || $date_time instanceof DateTimeImmutable) {
5390 return $date_time;
5391 }
5392
5393 return DateTimeImmutable::createFromFormat('U', (string) $date_time);
5394 }
5395
5403 public function processPrintoutput2FO($print_output): string
5404 {
5405 if (extension_loaded("tidy")) {
5406 $config = [
5407 "indent" => false,
5408 "output-xml" => true,
5409 "numeric-entities" => true
5410 ];
5411 $tidy = new tidy();
5412 $tidy->parseString($print_output, $config, 'utf8');
5413 $tidy->cleanRepair();
5414 $print_output = tidy_get_output($tidy);
5415 $print_output = preg_replace("/^.*?(<html)/", "\\1", $print_output);
5416 } else {
5417 $print_output = str_replace("&nbsp;", "&#160;", $print_output);
5418 $print_output = str_replace("&otimes;", "X", $print_output);
5419 }
5420 $xsl = file_get_contents("./components/ILIAS/Test/xml/question2fo.xsl");
5421
5422 // additional font support
5423
5424 $xsl = str_replace(
5425 'font-family="Helvetica, unifont"',
5426 'font-family="' . $this->settings->get('rpc_pdf_font', 'Helvetica, unifont') . '"',
5427 $xsl
5428 );
5429
5430 $args = [ '/_xml' => $print_output, '/_xsl' => $xsl ];
5431 $xh = xslt_create();
5432 $params = [];
5433 $output = xslt_process($xh, "arg:/_xml", "arg:/_xsl", null, $args, $params);
5434 xslt_error($xh);
5435 xslt_free($xh);
5436 return $output;
5437 }
5438
5445 public function deliverPDFfromHTML($content, $title = null)
5446 {
5447 $content = preg_replace("/href=\".*?\"/", "", $content);
5448 $printbody = new ilTemplate("tpl.il_as_tst_print_body.html", true, true, "components/ILIAS/Test");
5449 $printbody->setVariable("TITLE", ilLegacyFormElementsUtil::prepareFormOutput($this->getTitle()));
5450 $printbody->setVariable("ADM_CONTENT", $content);
5451 $printbody->setCurrentBlock("css_file");
5452 $printbody->setVariable("CSS_FILE", ilUtil::getStyleSheetLocation("filesystem", "delos.css"));
5453 $printbody->parseCurrentBlock();
5454 $printoutput = $printbody->get();
5455 $html = str_replace("href=\"./", "href=\"" . ILIAS_HTTP_PATH . "/", $printoutput);
5456 $html = preg_replace("/<div id=\"dontprint\">.*?<\\/div>/ims", "", $html);
5457 if (extension_loaded("tidy")) {
5458 $config = [
5459 "indent" => false,
5460 "output-xml" => true,
5461 "numeric-entities" => true
5462 ];
5463 $tidy = new tidy();
5464 $tidy->parseString($html, $config, 'utf8');
5465 $tidy->cleanRepair();
5466 $html = tidy_get_output($tidy);
5467 $html = preg_replace("/^.*?(<html)/", "\\1", $html);
5468 } else {
5469 $html = str_replace("&nbsp;", "&#160;", $html);
5470 $html = str_replace("&otimes;", "X", $html);
5471 }
5472 $html = preg_replace("/src=\".\\//ims", "src=\"" . ILIAS_HTTP_PATH . "/", $html);
5473 $this->deliverPDFfromFO($this->processPrintoutput2FO($html), $title);
5474 }
5475
5481 public function deliverPDFfromFO($fo, $title = null): bool
5482 {
5483 $fo_file = ilFileUtils::ilTempnam() . ".fo";
5484 $fp = fopen($fo_file, "w");
5485 fwrite($fp, $fo);
5486 fclose($fp);
5487
5488 try {
5489 $pdf_base64 = ilRpcClientFactory::factory('RPCTransformationHandler')->ilFO2PDF($fo);
5490 $filename = (strlen($title)) ? $title : $this->getTitle();
5493 $pdf_base64->scalar,
5495 "application/pdf"
5496 );
5497 return true;
5498 } catch (Exception $e) {
5499 $this->logger->info(__METHOD__ . ': ' . $e->getMessage());
5500 return false;
5501 }
5502 }
5503
5513 public static function getManualFeedback(int $active_id, int $question_id, ?int $pass): string
5514 {
5515 if ($pass === null) {
5516 return '';
5517 }
5518 $feedback = '';
5519 $row = self::getSingleManualFeedback((int) $active_id, (int) $question_id, (int) $pass);
5520
5521 if ($row !== [] && ($row['finalized_evaluation'] || \ilTestService::isManScoringDone((int) $active_id))) {
5522 $feedback = $row['feedback'] ?? '';
5523 }
5524
5525 return $feedback;
5526 }
5527
5528 public static function getSingleManualFeedback(int $active_id, int $question_id, int $pass): array
5529 {
5530 global $DIC;
5531 $ilDB = $DIC['ilDB'];
5532 $row = [];
5533 $result = $ilDB->queryF(
5534 "SELECT * FROM tst_manual_fb WHERE active_fi = %s AND question_fi = %s AND pass = %s",
5535 ['integer', 'integer', 'integer'],
5536 [$active_id, $question_id, $pass]
5537 );
5538
5539 if ($ilDB->numRows($result) === 1) {
5540 $row = $ilDB->fetchAssoc($result);
5541 $row['feedback'] = ilRTE::_replaceMediaObjectImageSrc($row['feedback'] ?? '', 1);
5542 } elseif ($ilDB->numRows($result) > 1) {
5543 $DIC->logger()->root()->warning(
5544 "WARNING: Multiple feedback entries on tst_manual_fb for " .
5545 "active_fi = $active_id , question_fi = $question_id and pass = $pass"
5546 );
5547 }
5548
5549 return $row;
5550 }
5551
5559 public function getCompleteManualFeedback(int $question_id): array
5560 {
5561 global $DIC;
5562 $ilDB = $DIC['ilDB'];
5563
5564 $feedback = [];
5565 $result = $ilDB->queryF(
5566 "SELECT * FROM tst_manual_fb WHERE question_fi = %s",
5567 ['integer'],
5568 [$question_id]
5569 );
5570
5571 while ($row = $ilDB->fetchAssoc($result)) {
5572 $active = $row['active_fi'];
5573 $pass = $row['pass'];
5574 $question = $row['question_fi'];
5575
5576 $row['feedback'] = ilRTE::_replaceMediaObjectImageSrc($row['feedback'] ?? '', 1);
5577
5578 $feedback[$active][$pass][$question] = $row;
5579 }
5580
5581 return $feedback;
5582 }
5583
5584 public function saveManualFeedback(
5585 int $active_id,
5586 int $question_id,
5587 int $pass,
5588 ?string $feedback,
5589 bool $finalized = false
5590 ): void {
5591 $feedback_old = self::getSingleManualFeedback($active_id, $question_id, $pass);
5592 $this->db->manipulateF(
5593 'DELETE FROM tst_manual_fb WHERE active_fi = %s AND question_fi = %s AND pass = %s',
5594 ['integer', 'integer', 'integer'],
5595 [$active_id, $question_id, $pass]
5596 );
5597
5598 $this->insertManualFeedback($active_id, $question_id, $pass, $feedback, $finalized, $feedback_old);
5599
5600 }
5601
5602 private function insertManualFeedback(
5603 int $active_id,
5604 int $question_id,
5605 int $pass,
5606 ?string $feedback,
5607 bool $finalized,
5608 array $feedback_old
5609 ): void {
5610 $next_id = $this->db->nextId('tst_manual_fb');
5611 $user = $this->user->getId();
5612 $finalized_time = time();
5613
5614 $update_default = [
5615 'manual_feedback_id' => [ 'integer', $next_id],
5616 'active_fi' => [ 'integer', $active_id],
5617 'question_fi' => [ 'integer', $question_id],
5618 'pass' => [ 'integer', $pass],
5619 'feedback' => [ 'clob', $feedback ? ilRTE::_replaceMediaObjectImageSrc($feedback, 0) : null],
5620 'tstamp' => [ 'integer', time()]
5621 ];
5622
5623 if ($feedback_old !== [] && (int) $feedback_old['finalized_evaluation'] === 1) {
5624 $user = $feedback_old['finalized_by_usr_id'];
5625 $finalized_time = $feedback_old['finalized_tstamp'];
5626 }
5627
5628 if ($finalized === false) {
5629 $update_default['finalized_evaluation'] = ['integer', 0];
5630 $update_default['finalized_by_usr_id'] = ['integer', 0];
5631 $update_default['finalized_tstamp'] = ['integer', 0];
5632 } elseif ($finalized === true) {
5633 $update_default['finalized_evaluation'] = ['integer', 1];
5634 $update_default['finalized_by_usr_id'] = ['integer', $user];
5635 $update_default['finalized_tstamp'] = ['integer', $finalized_time];
5636 }
5637
5638 $this->db->insert('tst_manual_fb', $update_default);
5639
5640 if ($this->logger->isLoggingEnabled()) {
5641 $this->logger->logScoringInteraction(
5642 $this->logger->getInteractionFactory()->buildScoringInteraction(
5643 $this->getRefId(),
5644 $question_id,
5645 $this->user->getId(),
5646 self::_getUserIdFromActiveId($active_id),
5647 TestScoringInteractionTypes::QUESTION_GRADED,
5648 [
5649 AdditionalInformationGenerator::KEY_EVAL_FINALIZED => $this->logger
5650 ->getAdditionalInformationGenerator()->getTrueFalseTagForBool($finalized),
5651 AdditionalInformationGenerator::KEY_FEEDBACK => $feedback ? ilRTE::_replaceMediaObjectImageSrc($feedback, 0) : ''
5652 ]
5653 )
5654 );
5655 }
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.
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()
exportFileItems($target_dir, &$expLog)
export files of file itmes
getQuestionsOfTest(int $active_id)
getImagePath()
Returns the image path for web accessable images of a test The image path is under the CLIENT_WEB_DIR...
getShowSolutionSignature()
Returns if the signature field should be shown in the test results.
getHighscoreAnon()
Gets if the highscores should be anonymized per setting.
exportXMLMediaObjects(&$a_xml_writer, $a_inst, $a_target_dir, &$expLog)
export media objects to xml (see ilias_co.dtd)
getPassScoring()
Gets the pass scoring type.
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 $a_package, int $a_timeout=0)
Creates an ilRpcClient instance to our ilServer.
static get(string $a_var)
ILIAS Setting Class.
special template class to simplify handling of ITX/PEAR
Base Exception for all Exceptions relating to Modules/Test.
@ilCtrl_Calls ilTestPageGUI: ilPageEditorGUI, ilEditClipboardGUI, ilMDEditorGUI @ilCtrl_Calls ilTestP...
@ilCtrl_Calls ilTestEditPageGUI: ilPageEditorGUI, ilEditClipboardGUI, ilMDEditorGUI @ilCtrl_Calls ilT...
Test sequence handler.
static isManScoringDone(int $active_id)
Test session handler.
static getStyleSheetLocation(string $mode="output", string $a_css_name="")
get full style sheet file name (path inclusive) of current user
static insertInstIntoID(string $a_value)
inserts installation id into ILIAS id
static _getObjectsByOperations( $a_obj_type, string $a_operation, int $a_usr_id=0, int $limit=0)
Get all objects of a specific type and check access This function is not recursive,...
static deliverData(string $a_data, string $a_filename, string $mime="application/octet-stream")
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
xmlElement(string $tag, $attrs=null, $data=null, $encode=true, $escape=true)
Writes a basic element (no children, just textual content)
xmlEndTag(string $tag)
Writes an endtag.
xmlStartTag(string $tag, ?array $attrs=null, bool $empty=false, bool $encode=true, bool $escape=true)
Writes a starttag.
const CLIENT_WEB_DIR
Definition: constants.php:47
const IL_INST_ID
Definition: constants.php:40
const ANONYMOUS_USER_ID
Definition: constants.php:27
return['delivery_method'=> 'php',]
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
const ILIAS_VERSION
The filesystem interface provides the public interface for the Filesystem service API consumer.
Definition: Filesystem.php:37
Readable part of repository interface to ilComponentDataDB.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Interface for html sanitizing functionality.
$ref_id
Definition: ltiauth.php:66
if(! $DIC->user() ->getId()||!ilLTIConsumerAccess::hasCustomProviderCreationAccess()) $params
Definition: ltiregstart.php:31
$res
Definition: ltiservices.php:69
__construct(Container $dic, ilPlugin $plugin)
@inheritDoc
Class ilObjForumAdministration.
global $lng
Definition: privfeed.php:31
global $ilSetting
Definition: privfeed.php:31
$results
if(!file_exists('../ilias.ini.php'))
global $DIC
Definition: shib_login.php:26
$counter
$objectives
getGeneralSettings()
$text
Definition: xapiexit.php:21