ILIAS  release_10 Revision v10.1-43-ga1241a92c2f
class.ilObjTest.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
28 use ILIAS\Test\ExportImport\Types as ExportImportTypes;
36 use ILIAS\Test\Settings\GlobalSettings\Repository as GlobalSettingsRepository;
52 
63 class 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  public const REDIRECT_NONE = 0;
75  public const REDIRECT_ALWAYS = 1;
76  public const REDIRECT_KIOSK = 2;
77 
78  private ?bool $activation_limited = null;
79  private array $mob_ids;
80  private array $file_ids = [];
81  private bool $online;
85  private ?MarkSchema $mark_schema = null;
86  public int $test_id = -1;
87  public int $invitation = self::INVITATION_OFF;
88  public string $author;
89 
93  public $metadata;
94  public array $questions = [];
95 
100 
104  public $test_sequence = false;
105 
106  private int $template_id = 0;
107 
108  protected bool $print_best_solution_with_result = true;
109 
110  protected bool $activation_visibility = false;
111  protected ?int $activation_starting_time = null;
112  protected ?int $activation_ending_time = null;
113 
119  private $participantDataExist = null;
120 
121  private ?int $tmpCopyWizardCopyId = null;
122 
125  protected Refinery $refinery;
126  protected ilSetting $settings;
127  protected ilBenchmark $bench;
129 
130  protected GlobalSettingsRepository $global_settings_repo;
131  protected ?MainSettings $main_settings = null;
133  protected ?ScoreSettings $score_settings = null;
135 
136  protected TestLogger $logger;
139 
141 
145 
148 
150 
157  public function __construct(int $id = 0, bool $a_call_by_reference = true)
158  {
159  $this->type = "tst";
160 
162  global $DIC;
163  $this->ctrl = $DIC['ilCtrl'];
164  $this->refinery = $DIC['refinery'];
165  $this->settings = $DIC['ilSetting'];
166  $this->bench = $DIC['ilBench'];
167  $this->component_repository = $DIC['component.repository'];
168  $this->component_factory = $DIC['component.factory'];
169  $this->filesystem_web = $DIC->filesystem()->web();
170  $this->lo_metadata = $DIC->learningObjectMetadata();
171 
172  $local_dic = $this->getLocalDIC();
173  $this->participant_access_filter = $local_dic['participant.access_filter.factory'];
174  $this->test_man_scoring_done_helper = $local_dic['scoring.manual.done_helper'];
175  $this->logger = $local_dic['logging.logger'];
176  $this->log_viewer = $local_dic['logging.viewer'];
177  $this->global_settings_repo = $local_dic['settings.global.repository'];
178  $this->marks_repository = $local_dic['marks.repository'];
179  $this->questionrepository = $local_dic['question.general_properties.repository'];
180  $this->testrequest = $local_dic['request_data_collector'];
181  $this->participant_repository = $local_dic['participant.repository'];
182  $this->export_factory = $local_dic['exportimport.factory'];
183 
184  parent::__construct($id, $a_call_by_reference);
185 
186  $this->lng->loadLanguageModule("assessment");
187  $this->score_settings = null;
188 
189  $this->question_set_config_factory = new ilTestQuestionSetConfigFactory(
190  $this->tree,
191  $this->db,
192  $this->lng,
193  $this->logger,
194  $this->component_repository,
195  $this,
196  $this->questionrepository
197  );
198  }
199 
200  public function getLocalDIC(): TestDIC
201  {
202  return TestDIC::dic();
203  }
204 
205  public function getTestLogger(): TestLogger
206  {
207  return $this->logger;
208  }
209 
210  public function getTestLogViewer(): TestLogViewer
211  {
212  return $this->log_viewer;
213  }
214 
216  {
217  return $this->question_set_config_factory->getQuestionSetConfig();
218  }
219 
223  public function getTitleFilenameCompliant(): string
224  {
225  return ilFileUtils::getASCIIFilename($this->getTitle());
226  }
227 
228  public function getTmpCopyWizardCopyId(): ?int
229  {
231  }
232 
233  public function setTmpCopyWizardCopyId(int $tmpCopyWizardCopyId): void
234  {
235  $this->tmpCopyWizardCopyId = $tmpCopyWizardCopyId;
236  }
237 
238  public function create(): int
239  {
240  $id = parent::create();
241  $this->createMetaData();
242  return $id;
243  }
244 
245  public function update(): bool
246  {
247  if (!parent::update()) {
248  return false;
249  }
250 
251  // put here object specific stuff
252  $this->updateMetaData();
253  return true;
254  }
255 
256  public function read(): void
257  {
258  parent::read();
259  $this->main_settings = null;
260  $this->score_settings = null;
261  $this->mark_schema = null;
262  $this->loadFromDb();
263  }
264 
265  public function delete(): bool
266  {
267  // always call parent delete function first!!
268  if (!parent::delete()) {
269  return false;
270  }
271 
272  // delet meta data
273  $this->deleteMetaData();
274 
275  //put here your module specific stuff
276  $this->deleteTest();
277 
278  $qsaImportFails = new ilAssQuestionSkillAssignmentImportFails($this->getId());
279  $qsaImportFails->deleteRegisteredImportFails();
280  $sltImportFails = new ilTestSkillLevelThresholdImportFails($this->getId());
281  $sltImportFails->deleteRegisteredImportFails();
282 
283  if ($this->logger->isLoggingEnabled()) {
284  $this->logger->logTestAdministrationInteraction(
285  $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
286  $this->getRefId(),
287  $this->user->getId(),
289  [
290  AdditionalInformationGenerator::KEY_TEST_TITLE => $test_title = $this->title
291  ]
292  )
293  );
294  }
295 
296  return true;
297  }
298 
299  public function deleteTest(): void
300  {
301  $participantData = new ilTestParticipantData($this->db, $this->lng);
302  $participantData->load($this->getTestId());
303  $this->removeTestResults($participantData);
304 
305  $this->db->manipulateF(
306  "DELETE FROM tst_mark WHERE test_fi = %s",
307  ['integer'],
308  [$this->getTestId()]
309  );
310 
311  $this->db->manipulateF(
312  "DELETE FROM tst_tests WHERE test_id = %s",
313  ['integer'],
314  [$this->getTestId()]
315  );
316 
317  $tst_data_dir = ilFileUtils::getDataDir() . "/tst_data";
318  $directory = $tst_data_dir . "/tst_" . $this->getId();
319  if (is_dir($directory)) {
320  ilFileUtils::delDir($directory);
321  }
322  $mobs = ilObjMediaObject::_getMobsOfObject("tst:html", $this->getId());
323  // remaining usages are not in text anymore -> delete them
324  // and media objects (note: delete method of ilObjMediaObject
325  // checks whether object is used in another context; if yes,
326  // the object is not deleted!)
327  foreach ($mobs as $mob) {
328  ilObjMediaObject::_removeUsage($mob, "tst:html", $this->getId());
329  if (ilObjMediaObject::_exists($mob)) {
330  $mob_obj = new ilObjMediaObject($mob);
331  $mob_obj->delete();
332  }
333  }
334  }
335 
341  public function createExportDirectory(): void
342  {
343  $tst_data_dir = ilFileUtils::getDataDir() . "/tst_data";
344  ilFileUtils::makeDir($tst_data_dir);
345  if (!is_writable($tst_data_dir)) {
346  $this->ilias->raiseError("Test Data Directory (" . $tst_data_dir
347  . ") not writeable.", $this->ilias->error_obj->MESSAGE);
348  }
349 
350  // create learning module directory (data_dir/lm_data/lm_<id>)
351  $tst_dir = $tst_data_dir . "/tst_" . $this->getId();
352  ilFileUtils::makeDir($tst_dir);
353  if (!@is_dir($tst_dir)) {
354  $this->ilias->raiseError("Creation of Test Directory failed.", $this->ilias->error_obj->MESSAGE);
355  }
356  // create Export subdirectory (data_dir/lm_data/lm_<id>/Export)
357  $export_dir = $tst_dir . "/export";
358  ilFileUtils::makeDir($export_dir);
359  if (!@is_dir($export_dir)) {
360  $this->ilias->raiseError("Creation of Export Directory failed.", $this->ilias->error_obj->MESSAGE);
361  }
362  }
363 
364  public function getExportDirectory(): string
365  {
366  $export_dir = ilFileUtils::getDataDir() . "/tst_data" . "/tst_" . $this->getId() . "/export";
367  return $export_dir;
368  }
369 
370  public function getExportFiles(string $dir = ''): array
371  {
372  // quit if import dir not available
373  if (!@is_dir($dir) || !is_writable($dir)) {
374  return [];
375  }
376 
377  $files = [];
378  foreach (new DirectoryIterator($dir) as $file) {
382  if ($file->isDir()) {
383  continue;
384  }
385 
386  $files[] = $file->getBasename();
387  }
388 
389  sort($files);
390 
391  return $files;
392  }
393 
399  public static function _createImportDirectory(): string
400  {
401  global $DIC;
402  $ilias = $DIC['ilias'];
403  $tst_data_dir = ilFileUtils::getDataDir() . "/tst_data";
404  ilFileUtils::makeDir($tst_data_dir);
405 
406  if (!is_writable($tst_data_dir)) {
407  $ilias->raiseError("Test Data Directory (" . $tst_data_dir
408  . ") not writeable.", $ilias->error_obj->FATAL);
409  }
410 
411  // create test directory (data_dir/tst_data/tst_import)
412  $tst_dir = $tst_data_dir . "/tst_import";
413  ilFileUtils::makeDir($tst_dir);
414  if (!@is_dir($tst_dir)) {
415  $ilias->raiseError("Creation of test import directory failed.", $ilias->error_obj->FATAL);
416  }
417 
418  // assert that this is empty and does not contain old data
419  ilFileUtils::delDir($tst_dir, true);
420 
421  return $tst_dir;
422  }
423 
424  final public function isComplete(ilTestQuestionSetConfig $test_question_set_config): bool
425  {
426  if ($this->getMarkSchema() === null
427  || $this->getMarkSchema()->getMarkSteps() === []) {
428  return false;
429  }
430 
431  if (!$test_question_set_config->isQuestionSetConfigured()) {
432  return false;
433  }
434 
435  return true;
436  }
437 
438  public function saveCompleteStatus(ilTestQuestionSetConfig $test_question_set_config): void
439  {
440  $complete = 0;
441  if ($this->isComplete($test_question_set_config)) {
442  $complete = 1;
443  }
444  if ($this->getTestId() > 0) {
445  $this->db->manipulateF(
446  'UPDATE tst_tests SET complete = %s WHERE test_id = %s',
447  ['text', 'integer'],
448  [$complete, $this->test_id]
449  );
450  }
451  }
452 
453  public function saveToDb(bool $properties_only = false): void
454  {
455  if ($this->test_id === -1) {
456  // Create new dataset
457  $next_id = $this->db->nextId('tst_tests');
458 
459  $this->db->insert(
460  'tst_tests',
461  [
462  'test_id' => ['integer', $next_id],
463  'obj_fi' => ['integer', $this->getId()],
464  'created' => ['integer', time()],
465  'tstamp' => ['integer', time()],
466  'template_id' => ['integer', $this->getTemplate()]
467  ]
468  );
469 
470  $this->test_id = $next_id;
471  } else {
472  if ($this->evalTotalPersons() > 0) {
473  // reset the finished status of participants if the nr of test passes did change
474  if ($this->getNrOfTries() > 0) {
475  // set all unfinished tests with nr of passes >= allowed passes finished
476  $aresult = $this->db->queryF(
477  "SELECT active_id FROM tst_active WHERE test_fi = %s AND tries >= %s AND submitted = %s",
478  ['integer', 'integer', 'integer'],
479  [$this->getTestId(), $this->getNrOfTries(), 0]
480  );
481  while ($row = $this->db->fetchAssoc($aresult)) {
482  $this->db->manipulateF(
483  "UPDATE tst_active SET submitted = %s, submittimestamp = %s WHERE active_id = %s",
484  ['integer', 'timestamp', 'integer'],
485  [1, date('Y-m-d H:i:s'), $row["active_id"]]
486  );
487  }
488 
489  // set all finished tests with nr of passes < allowed passes not finished
490  $aresult = $this->db->queryF(
491  "SELECT active_id FROM tst_active WHERE test_fi = %s AND tries < %s AND submitted = %s",
492  ['integer', 'integer', 'integer'],
493  [$this->getTestId(), $this->getNrOfTries() - 1, 1]
494  );
495  while ($row = $this->db->fetchAssoc($aresult)) {
496  $this->db->manipulateF(
497  "UPDATE tst_active SET submitted = %s, submittimestamp = %s WHERE active_id = %s",
498  ['integer', 'timestamp', 'integer'],
499  [0, null, $row["active_id"]]
500  );
501  }
502  } else {
503  // set all finished tests with nr of passes >= allowed passes not finished
504  $aresult = $this->db->queryF(
505  "SELECT active_id FROM tst_active WHERE test_fi = %s AND submitted = %s",
506  ['integer', 'integer'],
507  [$this->getTestId(), 1]
508  );
509  while ($row = $this->db->fetchAssoc($aresult)) {
510  $this->db->manipulateF(
511  "UPDATE tst_active SET submitted = %s, submittimestamp = %s WHERE active_id = %s",
512  ['integer', 'timestamp', 'integer'],
513  [0, null, $row["active_id"]]
514  );
515  }
516  }
517  }
518  }
519 
521  $this->isActivationLimited(),
522  $this->getActivationStartingTime(),
523  $this->getActivationEndingTime(),
524  $this->getActivationVisibility(),
525  );
526 
527  if ($properties_only) {
528  return;
529  }
530 
531  if ($this->getQuestionSetType() == self::QUESTION_SET_TYPE_FIXED) {
532  $this->saveQuestionsToDb();
533  }
534 
535  $this->marks_repository->storeMarkSchema($this->getMarkSchema());
536  }
537 
538  public function saveQuestionsToDb(): void
539  {
540  $this->db->manipulateF(
541  'DELETE FROM tst_test_question WHERE test_fi = %s',
542  ['integer'],
543  [$this->getTestId()]
544  );
545  foreach ($this->questions as $key => $value) {
546  $next_id = $this->db->nextId('tst_test_question');
547  $this->db->insert('tst_test_question', [
548  'test_question_id' => ['integer', $next_id],
549  'test_fi' => ['integer', $this->getTestId()],
550  'question_fi' => ['integer', $value],
551  'sequence' => ['integer', $key],
552  'tstamp' => ['integer', time()]
553  ]);
554  }
555  }
556 
560  public function copyQuestions(array $question_ids): void
561  {
562  $copy_count = 0;
563  $question_titles = $this->getQuestionTitles();
564 
565  foreach ($question_ids as $id) {
566  $question = assQuestion::instantiateQuestionGUI($id);
567  if ($question) {
568  $title = $question->getObject()->getTitle();
569  $i = 2;
570  while (in_array($title . ' (' . $i . ')', $question_titles)) {
571  $i++;
572  }
573 
574  $title .= ' (' . $i . ')';
575 
576  $question_titles[] = $title;
577 
578  $new_id = $question->getObject()->duplicate(false, $title);
579 
580  $clone = assQuestion::instantiateQuestionGUI($new_id);
581  $question = $clone->getObject();
582  $question->setObjId($this->getId());
583  $clone->setObject($question);
584  $clone->getObject()->saveToDb();
585 
586  $this->insertQuestion($new_id, true);
587 
588  $copy_count++;
589  }
590  }
591  }
592 
596  public function getNrOfResultsForPass($active_id, $pass): int
597  {
598  $result = $this->db->queryF(
599  "SELECT test_result_id FROM tst_test_result WHERE active_fi = %s AND pass = %s",
600  ['integer','integer'],
601  [$active_id, $pass]
602  );
603  return $result->numRows();
604  }
605 
606  public function loadFromDb(): void
607  {
608  $result = $this->db->queryF(
609  "SELECT test_id FROM tst_tests WHERE obj_fi = %s",
610  ['integer'],
611  [$this->getId()]
612  );
613  if ($result->numRows() === 1) {
614  $data = $this->db->fetchObject($result);
615  $this->setTestId($data->test_id);
616  $this->loadQuestions();
617  }
618 
619  // moved activation to ilObjectActivation
620  if (isset($this->ref_id)) {
621  $activation = ilObjectActivation::getItem($this->ref_id);
622  switch ($activation["timing_type"]) {
624  $this->setActivationLimited(true);
625  $this->setActivationStartingTime($activation["timing_start"]);
626  $this->setActivationEndingTime($activation["timing_end"]);
627  $this->setActivationVisibility($activation["visible"]);
628  break;
629 
630  default:
631  $this->setActivationLimited(false);
632  break;
633  }
634  }
635  }
636 
641  public function loadQuestions(int $active_id = 0, ?int $pass = null): void
642  {
643  $this->questions = [];
644  if ($this->isRandomTest()) {
645  if ($active_id === 0) {
646  $active_id = $this->getActiveIdOfUser($this->user->getId());
647  }
648  if (is_null($pass)) {
649  $pass = self::_getPass($active_id);
650  }
651  $result = $this->db->queryF(
652  'SELECT tst_test_rnd_qst.* '
653  . 'FROM tst_test_rnd_qst, qpl_questions '
654  . 'WHERE tst_test_rnd_qst.active_fi = %s '
655  . 'AND qpl_questions.question_id = tst_test_rnd_qst.question_fi '
656  . 'AND tst_test_rnd_qst.pass = %s '
657  . 'ORDER BY sequence',
658  ['integer', 'integer'],
659  [$active_id, $pass]
660  );
661  } else {
662  $result = $this->db->queryF(
663  'SELECT tst_test_question.* '
664  . 'FROM tst_test_question, qpl_questions '
665  . 'WHERE tst_test_question.test_fi = %s '
666  . 'AND qpl_questions.question_id = tst_test_question.question_fi '
667  . 'ORDER BY sequence',
668  ['integer'],
669  [$this->test_id]
670  );
671  }
672  $index = 1;
673  if ($this->test_id !== -1) {
674  //Omit loading of questions for non-id'ed test
675  while ($data = $this->db->fetchAssoc($result)) {
676  $this->questions[$index++] = $data["question_fi"];
677  }
678  }
679  }
680 
681  public function getIntroduction(): string
682  {
683  $page_id = $this->getMainSettings()->getIntroductionSettings()->getIntroductionPageId();
684  if ($page_id !== null) {
685  return (new ilTestPageGUI('tst', $page_id))->showPage();
686  }
687 
688  return $this->getMainSettings()->getIntroductionSettings()->getIntroductionText();
689  }
690 
691  private function cloneIntroduction(): ?int
692  {
693  $page_id = $this->getMainSettings()->getIntroductionSettings()->getIntroductionPageId();
694  if ($page_id === null) {
695  return null;
696  }
697  return $this->clonePage($page_id);
698  }
699 
700  public function getFinalStatement(): string
701  {
702  $page_id = $this->getMainSettings()->getFinishingSettings()->getConcludingRemarksPageId();
703  if ($page_id !== null) {
704  return (new ilTestPageGUI('tst', $page_id))->showPage();
705  }
706  return $this->getMainSettings()->getFinishingSettings()->getConcludingRemarksText();
707  }
708 
709  private function cloneConcludingRemarks(): ?int
710  {
711  $page_id = $this->getMainSettings()->getFinishingSettings()->getConcludingRemarksPageId();
712  if ($page_id === null) {
713  return null;
714  }
715  return $this->clonePage($page_id);
716  }
717 
718  private function clonePage(int $source_page_id): int
719  {
720  $page_object = new ilTestPage();
721  $page_object->setParentId($this->getId());
722  $new_page_id = $page_object->createPageWithNextId();
723  (new ilTestPage($source_page_id))->copy($new_page_id);
724  return $new_page_id;
725  }
726 
730  public function getTestId(): int
731  {
732  return $this->test_id;
733  }
734 
735  public function isPostponingEnabled(): bool
736  {
737  return $this->getMainSettings()->getParticipantFunctionalitySettings()->getPostponedQuestionsMoveToEnd();
738  }
739 
740  public function isScoreReportingEnabled(): bool
741  {
742  return $this->getScoreSettings()->getResultSummarySettings()->getScoreReporting()->isReportingEnabled();
743  }
744 
745  public function getAnswerFeedbackPoints(): bool
746  {
747  return $this->getMainSettings()->getQuestionBehaviourSettings()->getInstantFeedbackPointsEnabled();
748  }
749 
750  public function getGenericAnswerFeedback(): bool
751  {
752  return $this->getMainSettings()->getQuestionBehaviourSettings()->getInstantFeedbackGenericEnabled();
753  }
754 
755  public function getInstantFeedbackSolution(): bool
756  {
757  return $this->getMainSettings()->getQuestionBehaviourSettings()->getInstantFeedbackSolutionEnabled();
758  }
759 
760  public function getCountSystem(): int
761  {
762  return $this->getScoreSettings()->getScoringSettings()->getCountSystem();
763  }
764 
765  public static function _getCountSystem($active_id)
766  {
767  global $DIC;
768  $ilDB = $DIC['ilDB'];
769  $result = $ilDB->queryF(
770  "SELECT tst_tests.count_system FROM tst_tests, tst_active WHERE tst_active.active_id = %s AND tst_active.test_fi = tst_tests.test_id",
771  ['integer'],
772  [$active_id]
773  );
774  if ($result->numRows()) {
775  $row = $ilDB->fetchAssoc($result);
776  return $row["count_system"];
777  }
778  return false;
779  }
780 
784  public function getScoreCutting(): int
785  {
786  return $this->getScoreSettings()->getScoringSettings()->getScoreCutting();
787  }
788 
792  public function getPassScoring(): int
793  {
794  return $this->getScoreSettings()->getScoringSettings()->getPassScoring();
795  }
796 
800  public static function _getPassScoring(int $active_id): int
801  {
802  global $DIC;
803  $ilDB = $DIC['ilDB'];
804  $result = $ilDB->queryF(
805  "SELECT tst_tests.pass_scoring FROM tst_tests, tst_active WHERE tst_tests.test_id = tst_active.test_fi AND tst_active.active_id = %s",
806  ['integer'],
807  [$active_id]
808  );
809  if ($result->numRows()) {
810  $row = $ilDB->fetchAssoc($result);
811  return (int) $row["pass_scoring"];
812  }
813  return 0;
814  }
815 
819  public static function _getScoreCutting(int $active_id): bool
820  {
821  global $DIC;
822  $ilDB = $DIC['ilDB'];
823  $result = $ilDB->queryF(
824  "SELECT tst_tests.score_cutting FROM tst_tests, tst_active WHERE tst_active.active_id = %s AND tst_tests.test_id = tst_active.test_fi",
825  ['integer'],
826  [$active_id]
827  );
828  if ($result->numRows()) {
829  $row = $ilDB->fetchAssoc($result);
830  return (bool) $row["score_cutting"];
831  }
832  return false;
833  }
834 
835  public function getMarkSchema(): MarkSchema
836  {
837  if ($this->mark_schema === null) {
838  $this->mark_schema = $this->marks_repository->getMarkSchemaFor($this->getTestId());
839  }
840 
841  return $this->mark_schema;
842  }
843 
844  public function storeMarkSchema(MarkSchema $mark_schema): void
845  {
846  $this->marks_repository->storeMarkSchema($mark_schema);
847  $this->mark_schema = null;
848  }
849 
850  public function getNrOfTries(): int
851  {
852  return $this->getMainSettings()->getTestBehaviourSettings()->getNumberOfTries();
853  }
854 
855  public function isBlockPassesAfterPassedEnabled(): bool
856  {
857  return $this->getMainSettings()->getTestBehaviourSettings()->getBlockAfterPassedEnabled();
858  }
859 
860  public function getKioskMode(): bool
861  {
862  return $this->getMainSettings()->getTestBehaviourSettings()->getKioskModeEnabled();
863  }
864 
865  public function getShowKioskModeTitle(): bool
866  {
867  return $this->getMainSettings()->getTestBehaviourSettings()->getShowTitleInKioskMode();
868  }
869  public function getShowKioskModeParticipant(): bool
870  {
871  return $this->getMainSettings()->getTestBehaviourSettings()->getShowParticipantNameInKioskMode();
872  }
873 
874  public function getUsePreviousAnswers(): bool
875  {
876  return $this->getMainSettings()->getParticipantFunctionalitySettings()->getUsePreviousAnswerAllowed();
877  }
878 
879  public function getTitleOutput(): int
880  {
881  return $this->getMainSettings()->getQuestionBehaviourSettings()->getQuestionTitleOutputMode();
882  }
883 
884  public function isPreviousSolutionReuseEnabled($active_id): bool
885  {
886  $result = $this->db->queryF(
887  "SELECT tst_tests.use_previous_answers FROM tst_tests, tst_active WHERE tst_tests.test_id = tst_active.test_fi AND tst_active.active_id = %s",
888  ["integer"],
889  [$active_id]
890  );
891  if ($result->numRows()) {
892  $row = $this->db->fetchAssoc($result);
893  $test_allows_reuse = $row["use_previous_answers"];
894  }
895 
896  if ($test_allows_reuse === '1') {
897  $res = $this->user->getPref("tst_use_previous_answers");
898  if ($res === '1') {
899  return true;
900  }
901  }
902  return false;
903  }
904 
905  public function getProcessingTime(): ?string
906  {
907  return $this->getMainSettings()->getTestBehaviourSettings()->getProcessingTime();
908  }
909 
910  private function getProcessingTimeForXML(): string
911  {
912  $processing_time = $this->getMainSettings()->getTestBehaviourSettings()->getProcessingTime();
913  if ($processing_time === null
914  || $processing_time === ''
915  || !preg_match('/(\d{2}):(\d{2}):(\d{2})/is', $processing_time, $matches)
916  ) {
917  return '';
918  }
919 
920  return sprintf(
921  "P0Y0M0DT%dH%dM%dS",
922  $matches[1],
923  $matches[2],
924  $matches[3]
925  );
926  }
927 
928  public function getProcessingTimeInSeconds(int $active_id = 0): int
929  {
930  $processing_time = $this->getMainSettings()->getTestBehaviourSettings()->getProcessingTime() ?? '';
931  if (preg_match("/(\d{2}):(\d{2}):(\d{2})/", (string) $processing_time, $matches)) {
932  $extratime = $this->getExtraTime($active_id) * 60;
933  return ($matches[1] * 3600) + ($matches[2] * 60) + $matches[3] + $extratime;
934  } else {
935  return 0;
936  }
937  }
938 
939  public function getEnableProcessingTime(): bool
940  {
941  return $this->getMainSettings()->getTestBehaviourSettings()->getProcessingTimeEnabled();
942  }
943 
944  public function getResetProcessingTime(): bool
945  {
946  return $this->getMainSettings()->getTestBehaviourSettings()->getResetProcessingTime();
947  }
948 
949  public function isStartingTimeEnabled(): bool
950  {
951  return $this->getMainSettings()->getAccessSettings()->getStartTimeEnabled();
952  }
953 
954  public function getStartingTime(): int
955  {
956  $start_time = $this->getMainSettings()->getAccessSettings()->getStartTime();
957  return $start_time !== null ? $start_time->getTimestamp() : 0;
958  }
959 
960  public function isEndingTimeEnabled(): bool
961  {
962  return $this->getMainSettings()->getAccessSettings()->getEndTimeEnabled();
963  }
964 
965  public function getEndingTime(): int
966  {
967  $end_time = $this->getMainSettings()->getAccessSettings()->getEndTime();
968  return $end_time !== null ? $end_time->getTimestamp() : 0;
969  }
970 
971  public function getRedirectionMode(): int
972  {
973  return $this->getMainSettings()->getFinishingSettings()->getRedirectionMode();
974  }
975 
976  public function isRedirectModeKiosk(): bool
977  {
978  return $this->getMainSettings()->getFinishingSettings()->getRedirectionMode() === self::REDIRECT_KIOSK;
979  }
980 
981  public function isRedirectModeNone(): bool
982  {
983  return $this->getMainSettings()->getFinishingSettings()->getRedirectionMode() === self::REDIRECT_NONE;
984  }
985 
986  public function getRedirectionUrl(): string
987  {
988  return $this->getMainSettings()->getFinishingSettings()->getRedirectionUrl() ?? '';
989  }
990 
991  public function isPasswordEnabled(): bool
992  {
993  return $this->getMainSettings()->getAccessSettings()->getPasswordEnabled();
994  }
995 
996  public function getPassword(): ?string
997  {
998  return $this->getMainSettings()->getAccessSettings()->getPassword();
999  }
1000 
1001  public function removeQuestionsWithResults(array $question_ids): void
1002  {
1003  $scoring = new TestScoring(
1004  $this,
1005  $this->user,
1006  $this->db,
1007  $this->lng
1008  );
1009 
1010  array_walk(
1011  $question_ids,
1012  fn(int $v, int $k) => $this->removeQuestionWithResults($v, $scoring)
1013  );
1014  }
1015 
1016  private function removeQuestionWithResults(int $question_id, TestScoring $scoring): void
1017  {
1018  $question = \assQuestion::instantiateQuestion($question_id);
1019 
1020  $participant_data = new ilTestParticipantData($this->db, $this->lng);
1021  $participant_data->load($this->test_id);
1022 
1023  $question->removeAllExistingSolutions();
1024  $scoring->removeAllQuestionResults($question_id);
1025 
1026  $this->removeQuestion($question_id);
1027  if (!$this->isRandomTest()) {
1029  $question_id,
1030  $participant_data->getActiveIds(),
1032  );
1033  }
1034 
1035  $scoring->updatePassAndTestResults($participant_data->getActiveIds());
1036  ilLPStatusWrapper::_refreshStatus($this->getId(), $participant_data->getUserIds());
1037  $question->delete($question_id);
1038 
1039  if ($this->getTestQuestions() === []) {
1042  $object_properties->getPropertyIsOnline()->withOffline()
1043  );
1044  }
1045 
1046  if ($this->logger->isLoggingEnabled()) {
1047  $this->logger->logTestAdministrationInteraction(
1048  $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
1049  $this->getRefId(),
1050  $this->user->getId(),
1051  TestAdministrationInteractionTypes::QUESTION_REMOVED_IN_CORRECTIONS,
1052  [
1053  AdditionalInformationGenerator::KEY_QUESTION_TITLE => $question->getTitleForHTMLOutput(),
1054  AdditionalInformationGenerator::KEY_QUESTION_TEXT => $question->getQuestion(),
1055  AdditionalInformationGenerator::KEY_QUESTION_ID => $question->getId(),
1056  AdditionalInformationGenerator::KEY_QUESTION_TYPE => $question->getQuestionType()
1057  ]
1058  )
1059  );
1060  }
1061  }
1062 
1067  int $question_id,
1068  array $active_ids,
1069  ilTestReindexedSequencePositionMap $reindexed_sequence_position_map
1070  ): void {
1071  $test_sequence_factory = new ilTestSequenceFactory(
1072  $this,
1073  $this->db,
1074  $this->questionrepository
1075  );
1076 
1077  foreach ($active_ids as $active_id) {
1078  $passSelector = new ilTestPassesSelector($this->db, $this);
1079  $passSelector->setActiveId($active_id);
1080 
1081  foreach ($passSelector->getExistingPasses() as $pass) {
1082  $test_sequence = $test_sequence_factory->getSequenceByActiveIdAndPass($active_id, $pass);
1083  $test_sequence->loadFromDb();
1084 
1085  $test_sequence->removeQuestion($question_id, $reindexed_sequence_position_map);
1086  $test_sequence->saveToDb();
1087  }
1088  }
1089  }
1090 
1094  public function removeQuestions(array $question_ids): void
1095  {
1096  foreach ($question_ids as $question_id) {
1097  $this->removeQuestion((int) $question_id);
1098  }
1099 
1101  }
1102 
1103  public function removeQuestion(int $question_id): void
1104  {
1105  try {
1106  $question = self::_instanciateQuestion($question_id);
1107  $question_title = $question->getTitleForHTMLOutput();
1108  $question->delete($question_id);
1109  if ($this->logger->isLoggingEnabled()) {
1110  $this->logger->logTestAdministrationInteraction(
1111  $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
1112  $this->getRefId(),
1113  $this->user->getId(),
1114  TestAdministrationInteractionTypes::QUESTION_REMOVED,
1115  [
1116  AdditionalInformationGenerator::KEY_QUESTION_TITLE => $question_title
1117  ]
1118  )
1119  );
1120  }
1121  } catch (InvalidArgumentException $e) {
1122  $this->logger->error($e->getMessage());
1123  $this->logger->error($e->getTraceAsString());
1124  }
1125  }
1126 
1135  public function removeTestResultsFromSoapLpAdministration(array $user_ids)
1136  {
1137  $this->removeTestResultsByUserIds($user_ids);
1138 
1139  $participantData = new ilTestParticipantData($this->db, $this->lng);
1140  $participantData->setUserIdsFilter($user_ids);
1141  $participantData->load($this->getTestId());
1142 
1143  $this->removeTestActives($participantData->getActiveIds());
1144 
1145 
1146  if ($this->logger->isLoggingEnabled()) {
1147  $this->logger->logTestAdministrationInteraction(
1148  $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
1149  $this->getRefId(),
1150  $this->user->getId(),
1151  TestAdministrationInteractionTypes::PARTICIPANT_DATA_REMOVED,
1152  [
1153  AdditionalInformationGenerator::KEY_USERS => $participantData->getUserIds()
1154  ]
1155  )
1156  );
1157  }
1158  }
1159 
1160  public function removeTestResults(ilTestParticipantData $participant_data): void
1161  {
1162  if ($participant_data->getAnonymousActiveIds() !== []) {
1163  $this->removeTestResultsByActiveIds($participant_data->getAnonymousActiveIds());
1164 
1165  $user_ids = array_map(
1166  static fn($active_id) => $participant_data->getUserIdByActiveId($active_id),
1167  $participant_data->getAnonymousActiveIds(),
1168  );
1169  $this->participant_repository->removeExtraTimeByUserId($this->getTestId(), $user_ids);
1170  }
1171 
1172  if ($participant_data->getUserIds() !== []) {
1173  /* @var ilTestLP $testLP */
1174  $test_lp = ilObjectLP::getInstance($this->getId());
1175  if ($test_lp instanceof ilTestLP) {
1176  $test_lp->setTestObject($this);
1177  $test_lp->resetLPDataForUserIds($participant_data->getUserIds(), false);
1178  }
1179 
1180  $this->participant_repository->removeExtraTimeByUserId($this->getTestId(), $participant_data->getUserIds());
1181  }
1182 
1183  if ($participant_data->getActiveIds() !== []) {
1184  $this->removeTestActives($participant_data->getActiveIds());
1185 
1186  $user_ids = array_map(
1187  static fn($active_id) => $participant_data->getUserIdByActiveId($active_id),
1188  $participant_data->getActiveIds(),
1189  );
1190  $this->participant_repository->removeExtraTimeByUserId($this->getTestId(), $user_ids);
1191  }
1192 
1193  if ($this->logger->isLoggingEnabled()) {
1194  $this->logger->logTestAdministrationInteraction(
1195  $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
1196  $this->getRefId(),
1197  $this->user->getId(),
1198  TestAdministrationInteractionTypes::PARTICIPANT_DATA_REMOVED,
1199  [
1200  AdditionalInformationGenerator::KEY_USERS => $participant_data->getUserIds(),
1201  AdditionalInformationGenerator::KEY_ANON_IDS => $participant_data->getAnonymousActiveIds()
1202  ]
1203  )
1204  );
1205  }
1206  }
1207 
1208  public function removeTestResultsByUserIds(array $user_ids): void
1209  {
1210  $participantData = new ilTestParticipantData($this->db, $this->lng);
1211  $participantData->setUserIdsFilter($user_ids);
1212  $participantData->load($this->getTestId());
1213 
1214  $in_user_ids = $this->db->in('usr_id', $participantData->getUserIds(), false, 'integer');
1215  $this->db->manipulateF(
1216  "DELETE FROM usr_pref WHERE {$in_user_ids} AND keyword = %s",
1217  ['text'],
1218  ['tst_password_' . $this->getTestId()]
1219  );
1220 
1221  if ($participantData->getActiveIds() !== []) {
1222  $this->removeTestResultsByActiveIds($participantData->getActiveIds());
1223  }
1224  }
1225 
1226  private function removeTestResultsByActiveIds(array $active_ids): void
1227  {
1228  $in_active_ids = $this->db->in('active_fi', $active_ids, false, 'integer');
1229 
1230  $this->db->manipulate("DELETE FROM tst_solutions WHERE {$in_active_ids}");
1231  $this->db->manipulate("DELETE FROM tst_qst_solved WHERE {$in_active_ids}");
1232  $this->db->manipulate("DELETE FROM tst_test_result WHERE {$in_active_ids}");
1233  $this->db->manipulate("DELETE FROM tst_pass_result WHERE {$in_active_ids}");
1234  $this->db->manipulate("DELETE FROM tst_result_cache WHERE {$in_active_ids}");
1235  $this->db->manipulate("DELETE FROM tst_sequence WHERE {$in_active_ids}");
1236  $this->db->manipulate("DELETE FROM tst_times WHERE {$in_active_ids}");
1237  $this->db->manipulate(
1239  . ' WHERE ' . $this->db->in('active_id', $active_ids, false, 'integer')
1240  );
1241 
1242  if ($this->isRandomTest()) {
1243  $this->db->manipulate("DELETE FROM tst_test_rnd_qst WHERE {$in_active_ids}");
1244  }
1245 
1246  foreach ($active_ids as $active_id) {
1247  // remove file uploads
1248  if (is_dir(CLIENT_WEB_DIR . "/assessment/tst_" . $this->getTestId() . "/$active_id")) {
1249  ilFileUtils::delDir(CLIENT_WEB_DIR . "/assessment/tst_" . $this->getTestId() . "/$active_id");
1250  }
1251  }
1252 
1254  }
1255 
1259  public function removeTestActives(array $active_ids): void
1260  {
1261  $IN_activeIds = $this->db->in('active_id', $active_ids, false, 'integer');
1262  $this->db->manipulate("DELETE FROM tst_active WHERE $IN_activeIds");
1263  }
1264 
1272  public function questionMoveUp($question_id)
1273  {
1274  // Move a question up in sequence
1275  $result = $this->db->queryF(
1276  "SELECT * FROM tst_test_question WHERE test_fi=%s AND question_fi=%s",
1277  ['integer', 'integer'],
1278  [$this->getTestId(), $question_id]
1279  );
1280  $data = $this->db->fetchObject($result);
1281  if ($data->sequence > 1) {
1282  // OK, it's not the top question, so move it up
1283  $result = $this->db->queryF(
1284  "SELECT * FROM tst_test_question WHERE test_fi=%s AND sequence=%s",
1285  ['integer','integer'],
1286  [$this->getTestId(), $data->sequence - 1]
1287  );
1288  $data_previous = $this->db->fetchObject($result);
1289  // change previous dataset
1290  $this->db->manipulateF(
1291  "UPDATE tst_test_question SET sequence=%s WHERE test_question_id=%s",
1292  ['integer','integer'],
1293  [$data->sequence, $data_previous->test_question_id]
1294  );
1295  // move actual dataset up
1296  $this->db->manipulateF(
1297  "UPDATE tst_test_question SET sequence=%s WHERE test_question_id=%s",
1298  ['integer','integer'],
1299  [$data->sequence - 1, $data->test_question_id]
1300  );
1301  }
1302  $this->loadQuestions();
1303  }
1304 
1312  public function questionMoveDown($question_id)
1313  {
1314  $current_question_result = $this->db->queryF(
1315  "SELECT * FROM tst_test_question WHERE test_fi=%s AND question_fi=%s",
1316  ['integer','integer'],
1317  [$this->getTestId(), $question_id]
1318  );
1319  $current_question_data = $this->db->fetchObject($current_question_result);
1320  $next_question_result = $this->db->queryF(
1321  "SELECT * FROM tst_test_question WHERE test_fi=%s AND sequence=%s",
1322  ['integer','integer'],
1323  [$this->getTestId(), $current_question_data->sequence + 1]
1324  );
1325  if ($this->db->numRows($next_question_result) === 1) {
1326  // OK, it's not the last question, so move it down
1327  $next_question_data = $this->db->fetchObject($next_question_result);
1328  // change next dataset
1329  $this->db->manipulateF(
1330  "UPDATE tst_test_question SET sequence=%s WHERE test_question_id=%s",
1331  ['integer','integer'],
1332  [$current_question_data->sequence, $next_question_data->test_question_id]
1333  );
1334  // move actual dataset down
1335  $this->db->manipulateF(
1336  "UPDATE tst_test_question SET sequence=%s WHERE test_question_id=%s",
1337  ['integer','integer'],
1338  [$current_question_data->sequence + 1, $current_question_data->test_question_id]
1339  );
1340  }
1341  $this->loadQuestions();
1342  }
1343 
1350  public function duplicateQuestionForTest($question_id): int
1351  {
1352  $question = ilObjTest::_instanciateQuestion($question_id);
1353  $duplicate_id = $question->duplicate(true, '', '', -1, $this->getId());
1354  return $duplicate_id;
1355  }
1356 
1357  public function insertQuestion(int $question_id, bool $link_only = false): int
1358  {
1359  if ($link_only) {
1360  $duplicate_id = $question_id;
1361  } else {
1362  $duplicate_id = $this->duplicateQuestionForTest($question_id);
1363  }
1364 
1365  // get maximum sequence index in test
1366  $result = $this->db->queryF(
1367  "SELECT MAX(sequence) seq FROM tst_test_question WHERE test_fi=%s",
1368  ['integer'],
1369  [$this->getTestId()]
1370  );
1371  $sequence = 1;
1372 
1373  if ($result->numRows() == 1) {
1374  $data = $this->db->fetchObject($result);
1375  $sequence = $data->seq + 1;
1376  }
1377 
1378  $next_id = $this->db->nextId('tst_test_question');
1379  $this->db->manipulateF(
1380  "INSERT INTO tst_test_question (test_question_id, test_fi, question_fi, sequence, tstamp) VALUES (%s, %s, %s, %s, %s)",
1381  ['integer', 'integer','integer','integer','integer'],
1382  [$next_id, $this->getTestId(), $duplicate_id, $sequence, time()]
1383  );
1384  // remove test_active entries, because test has changed
1385  $this->db->manipulateF(
1386  "DELETE FROM tst_active WHERE test_fi = %s",
1387  ['integer'],
1388  [$this->getTestId()]
1389  );
1390  $this->loadQuestions();
1391  $this->saveCompleteStatus($this->question_set_config_factory->getQuestionSetConfig());
1392 
1393  if ($this->logger->isLoggingEnabled()) {
1394  $this->logger->logTestAdministrationInteraction(
1395  $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
1396  $this->getRefId(),
1397  $this->user->getId(),
1398  TestAdministrationInteractionTypes::QUESTION_ADDED,
1399  [
1400  AdditionalInformationGenerator::KEY_QUESTION_ID => $question_id
1401  ] + (assQuestion::instantiateQuestion($question_id))
1402  ->toLog($this->logger->getAdditionalInformationGenerator())
1403  )
1404  );
1405  }
1406 
1407  return $duplicate_id;
1408  }
1409 
1410  private function getQuestionTitles(): array
1411  {
1412  $titles = [];
1413  if ($this->getQuestionSetType() === self::QUESTION_SET_TYPE_FIXED) {
1414  $result = $this->db->queryF(
1415  'SELECT qpl_questions.title FROM tst_test_question, qpl_questions '
1416  . 'WHERE tst_test_question.test_fi = %s AND tst_test_question.question_fi = qpl_questions.question_id '
1417  . 'ORDER BY tst_test_question.sequence',
1418  ['integer'],
1419  [$this->getTestId()]
1420  );
1421  while ($row = $this->db->fetchAssoc($result)) {
1422  array_push($titles, $row['title']);
1423  }
1424  }
1425  return $titles;
1426  }
1427 
1435  public function getQuestionTitlesAndIndexes(): array
1436  {
1437  $titles = [];
1438  if ($this->getQuestionSetType() == self::QUESTION_SET_TYPE_FIXED) {
1439  $result = $this->db->queryF(
1440  'SELECT qpl_questions.title, qpl_questions.question_id '
1441  . 'FROM tst_test_question, qpl_questions '
1442  . 'WHERE tst_test_question.test_fi = %s AND tst_test_question.question_fi = qpl_questions.question_id '
1443  . 'ORDER BY tst_test_question.sequence',
1444  ['integer'],
1445  [$this->getTestId()]
1446  );
1447  while ($row = $this->db->fetchAssoc($result)) {
1448  $titles[$row['question_id']] = $row["title"];
1449  }
1450  }
1451  return $titles;
1452  }
1453 
1454  // fau: testNav - add number parameter (to show if title should not be shown)
1464  public function getQuestionTitle($title, $nr = null, $points = null): string
1465  {
1466  switch ($this->getTitleOutput()) {
1467  case '0':
1468  case '1':
1469  return $title;
1470  break;
1471  case '2':
1472  if (isset($nr)) {
1473  return $this->lng->txt("ass_question") . ' ' . $nr;
1474  }
1475  return $this->lng->txt("ass_question");
1476  break;
1477  case 3:
1478  if (isset($nr)) {
1479  $txt = $this->lng->txt("ass_question") . ' ' . $nr;
1480  } else {
1481  $txt = $this->lng->txt("ass_question");
1482  }
1483  if ($points != '') {
1484  $lngv = $this->lng->txt('points');
1485  if ($points == 1) {
1486  $lngv = $this->lng->txt('point');
1487  }
1488  $txt .= ' - ' . $points . ' ' . $lngv;
1489  }
1490  return $txt;
1491  break;
1492 
1493  }
1494  return $this->lng->txt("ass_question");
1495  }
1496  // fau.
1497 
1506  public function getQuestionDataset($question_id): object
1507  {
1508  $result = $this->db->queryF(
1509  "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",
1510  ['integer'],
1511  [$question_id]
1512  );
1513  $row = $this->db->fetchObject($result);
1514  return $row;
1515  }
1516 
1523  public function &getExistingQuestions($pass = null): array
1524  {
1525  $existing_questions = [];
1526  $active_id = $this->getActiveIdOfUser($this->user->getId());
1527  if ($this->isRandomTest()) {
1528  if (is_null($pass)) {
1529  $pass = 0;
1530  }
1531  $result = $this->db->queryF(
1532  "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",
1533  ['integer','integer'],
1534  [$active_id, $pass]
1535  );
1536  } else {
1537  $result = $this->db->queryF(
1538  "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",
1539  ['integer'],
1540  [$this->getTestId()]
1541  );
1542  }
1543  while ($data = $this->db->fetchObject($result)) {
1544  if ($data->original_id === null) {
1545  continue;
1546  }
1547 
1548  array_push($existing_questions, $data->original_id);
1549  }
1550  return $existing_questions;
1551  }
1552 
1560  public function getQuestionType($question_id)
1561  {
1562  if ($question_id < 1) {
1563  return -1;
1564  }
1565  $result = $this->db->queryF(
1566  "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",
1567  ['integer'],
1568  [$question_id]
1569  );
1570  if ($result->numRows() == 1) {
1571  $data = $this->db->fetchObject($result);
1572  return $data->type_tag;
1573  } else {
1574  return "";
1575  }
1576  }
1577 
1584  public function startWorkingTime($active_id, $pass)
1585  {
1586  $next_id = $this->db->nextId('tst_times');
1587  $affectedRows = $this->db->manipulateF(
1588  "INSERT INTO tst_times (times_id, active_fi, started, finished, pass, tstamp) VALUES (%s, %s, %s, %s, %s, %s)",
1589  ['integer', 'integer', 'timestamp', 'timestamp', 'integer', 'integer'],
1590  [$next_id, $active_id, date("Y-m-d H:i:s"), date("Y-m-d H:i:s"), $pass, time()]
1591  );
1592  return $next_id;
1593  }
1594 
1601  public function updateWorkingTime($times_id)
1602  {
1603  $affectedRows = $this->db->manipulateF(
1604  "UPDATE tst_times SET finished = %s, tstamp = %s WHERE times_id = %s",
1605  ['timestamp', 'integer', 'integer'],
1606  [date('Y-m-d H:i:s'), time(), $times_id]
1607  );
1608  }
1609 
1616  public function &getWorkedQuestions($active_id, $pass = null): array
1617  {
1618  if (is_null($pass)) {
1619  $result = $this->db->queryF(
1620  "SELECT question_fi FROM tst_solutions WHERE active_fi = %s AND pass = %s GROUP BY question_fi",
1621  ['integer','integer'],
1622  [$active_id, 0]
1623  );
1624  } else {
1625  $result = $this->db->queryF(
1626  "SELECT question_fi FROM tst_solutions WHERE active_fi = %s AND pass = %s GROUP BY question_fi",
1627  ['integer','integer'],
1628  [$active_id, $pass]
1629  );
1630  }
1631  $result_array = [];
1632  while ($row = $this->db->fetchAssoc($result)) {
1633  array_push($result_array, $row["question_fi"]);
1634  }
1635  return $result_array;
1636  }
1637 
1646  public function isTestFinishedToViewResults($active_id, $currentpass): bool
1647  {
1648  $num = ilObjTest::lookupPassResultsUpdateTimestamp($active_id, $currentpass);
1649  return ((($currentpass > 0) && ($num == 0)) || $this->isTestFinished($active_id)) ? true : false;
1650  }
1651 
1658  public function getAllQuestions($pass = null): array
1659  {
1660  if ($this->isRandomTest()) {
1661  $active_id = $this->getActiveIdOfUser($this->user->getId());
1662  if ($active_id === null) {
1663  return [];
1664  }
1665  $this->loadQuestions($active_id, $pass);
1666  if (count($this->questions) === 0) {
1667  return [];
1668  }
1669  if (is_null($pass)) {
1670  $pass = self::_getPass($active_id);
1671  }
1672  $result = $this->db->queryF(
1673  "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'),
1674  ['integer','integer'],
1675  [$active_id, $pass]
1676  );
1677  } else {
1678  if (count($this->questions) === 0) {
1679  return [];
1680  }
1681  $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'));
1682  }
1683  $result_array = [];
1684  while ($row = $this->db->fetchAssoc($result)) {
1685  $result_array[$row["question_id"]] = $row;
1686  }
1687  return $result_array;
1688  }
1689 
1698  public function getActiveIdOfUser($user_id = "", $anonymous_id = ""): ?int
1699  {
1700  if (!$user_id) {
1701  $user_id = $this->user->getId();
1702  }
1703 
1704  $tst_access_code = ilSession::get('tst_access_code');
1705  if (is_array($tst_access_code) &&
1706  $this->user->getId() === ANONYMOUS_USER_ID &&
1707  isset($tst_access_code[$this->getTestId()]) &&
1708  $tst_access_code[$this->getTestId()] !== '') {
1709  $result = $this->db->queryF(
1710  'SELECT active_id FROM tst_active WHERE user_fi = %s AND test_fi = %s AND anonymous_id = %s',
1711  ['integer', 'integer', 'text'],
1712  [$user_id, $this->test_id, $tst_access_code[$this->getTestId()]]
1713  );
1714  } elseif ((string) $anonymous_id !== '') {
1715  $result = $this->db->queryF(
1716  'SELECT active_id FROM tst_active WHERE user_fi = %s AND test_fi = %s AND anonymous_id = %s',
1717  ['integer', 'integer', 'text'],
1718  [$user_id, $this->test_id, $anonymous_id]
1719  );
1720  } else {
1721  if ((int) $user_id === ANONYMOUS_USER_ID) {
1722  return null;
1723  }
1724  $result = $this->db->queryF(
1725  'SELECT active_id FROM tst_active WHERE user_fi = %s AND test_fi = %s',
1726  ['integer', 'integer'],
1727  [$user_id, $this->test_id]
1728  );
1729  }
1730 
1731  if ($result->numRows()) {
1732  $row = $this->db->fetchAssoc($result);
1733  return (int) $row['active_id'];
1734  }
1735 
1736  return null;
1737  }
1738 
1739  public static function _getActiveIdOfUser($user_id = "", $test_id = "")
1740  {
1741  global $DIC;
1742  $ilDB = $DIC['ilDB'];
1743  $ilUser = $DIC['ilUser'];
1744 
1745  if (!$user_id) {
1746  $user_id = $ilUser->id;
1747  }
1748  if (!$test_id) {
1749  return "";
1750  }
1751  $result = $ilDB->queryF(
1752  "SELECT tst_active.active_id FROM tst_active WHERE user_fi = %s AND test_fi = %s",
1753  ['integer', 'integer'],
1754  [$user_id, $test_id]
1755  );
1756  if ($result->numRows()) {
1757  $row = $ilDB->fetchAssoc($result);
1758  return $row["active_id"];
1759  } else {
1760  return "";
1761  }
1762  }
1763 
1770  public function pcArrayShuffle($array): array
1771  {
1772  $keys = array_keys($array);
1773  shuffle($keys);
1774  $result = [];
1775  foreach ($keys as $key) {
1776  $result[$key] = $array[$key];
1777  }
1778  return $result;
1779  }
1780 
1787  public function getTestResult(
1788  int $active_id,
1789  ?int $pass = null,
1790  bool $ordered_sequence = false,
1791  bool $consider_hidden_questions = true,
1792  bool $consider_optional_questions = true
1793  ): array {
1794  $results = $this->getResultsForActiveId($active_id);
1795 
1796  if ($pass === null) {
1797  $pass = (int) $results['pass'];
1798  }
1799 
1800  $test_sequence_factory = new ilTestSequenceFactory($this, $this->db, $this->questionrepository);
1801  $test_sequence = $test_sequence_factory->getSequenceByActiveIdAndPass($active_id, $pass);
1802 
1803  $test_sequence->setConsiderHiddenQuestionsEnabled($consider_hidden_questions);
1804  $test_sequence->setConsiderOptionalQuestionsEnabled($consider_optional_questions);
1805 
1806  $test_sequence->loadFromDb();
1807  $test_sequence->loadQuestions();
1808 
1809  if ($ordered_sequence) {
1810  $sequence = $test_sequence->getOrderedSequenceQuestions();
1811  } else {
1812  $sequence = $test_sequence->getUserSequenceQuestions();
1813  }
1814 
1815  $arr_results = [];
1816 
1817  $query = "
1818  SELECT
1819  tst_test_result.question_fi,
1820  tst_test_result.points reached,
1821  tst_test_result.hint_count requested_hints,
1822  tst_test_result.hint_points hint_points,
1823  tst_test_result.answered answered,
1824  tst_manual_fb.finalized_evaluation finalized_evaluation
1825 
1826  FROM tst_test_result
1827 
1828  LEFT JOIN tst_solutions
1829  ON tst_solutions.active_fi = tst_test_result.active_fi
1830  AND tst_solutions.question_fi = tst_test_result.question_fi
1831 
1832  LEFT JOIN tst_manual_fb
1833  ON tst_test_result.active_fi = tst_manual_fb.active_fi
1834  AND tst_test_result.question_fi = tst_manual_fb.question_fi
1835 
1836  WHERE tst_test_result.active_fi = %s
1837  AND tst_test_result.pass = %s
1838  ";
1839 
1840  $solutionresult = $this->db->queryF(
1841  $query,
1842  ['integer', 'integer'],
1843  [$active_id, $pass]
1844  );
1845 
1846  while ($row = $this->db->fetchAssoc($solutionresult)) {
1847  $arr_results[ $row['question_fi'] ] = $row;
1848  }
1849 
1850  $num_worked_through = count($arr_results);
1851 
1852  $IN_question_ids = $this->db->in('qpl_questions.question_id', $sequence, false, 'integer');
1853 
1854  $query = "
1855  SELECT qpl_questions.*,
1856  qpl_qst_type.type_tag,
1857  qpl_sol_sug.question_fi has_sug_sol
1858 
1859  FROM qpl_qst_type,
1860  qpl_questions
1861 
1862  LEFT JOIN qpl_sol_sug
1863  ON qpl_sol_sug.question_fi = qpl_questions.question_id
1864 
1865  WHERE qpl_qst_type.question_type_id = qpl_questions.question_type_fi
1866  AND $IN_question_ids
1867  ";
1868 
1869  $result = $this->db->query($query);
1870  $unordered = [];
1871  $key = 1;
1872  while ($row = $this->db->fetchAssoc($result)) {
1873  if (!isset($arr_results[ $row['question_id'] ])) {
1874  $percentvalue = 0.0;
1875  } else {
1876  $percentvalue = (
1877  $row['points'] ? $arr_results[$row['question_id']]['reached'] / $row['points'] : 0
1878  );
1879  }
1880  if ($percentvalue < 0) {
1881  $percentvalue = 0.0;
1882  }
1883 
1884  $data = [
1885  "nr" => "$key",
1886  "title" => ilLegacyFormElementsUtil::prepareFormOutput($row['title']),
1887  "max" => round($row['points'], 2),
1888  "reached" => round($arr_results[$row['question_id']]['reached'] ?? 0, 2),
1889  'requested_hints' => $arr_results[$row['question_id']]['requested_hints'] ?? 0,
1890  'hint_points' => $arr_results[$row['question_id']]['hint_points'] ?? 0,
1891  "percent" => sprintf("%2.2f ", ($percentvalue) * 100) . "%",
1892  "solution" => ($row['has_sug_sol']) ? assQuestion::_getSuggestedSolutionOutput($row['question_id']) : '',
1893  "type" => $row["type_tag"],
1894  "qid" => $row['question_id'],
1895  "original_id" => $row["original_id"],
1896  "workedthrough" => isset($arr_results[$row['question_id']]) ? 1 : 0,
1897  'answered' => $arr_results[$row['question_id']]['answered'] ?? 0,
1898  'finalized_evaluation' => $arr_results[$row['question_id']]['finalized_evaluation'] ?? 0,
1899  ];
1900 
1901  $unordered[ $row['question_id'] ] = $data;
1902  $key++;
1903  }
1904 
1905  $numQuestionsTotal = count($unordered);
1906 
1907  $pass_max = 0;
1908  $pass_reached = 0;
1909  $pass_requested_hints = 0;
1910  $pass_hint_points = 0;
1911 
1912  $found = [];
1913 
1914  foreach ($sequence as $qid) {
1915  // building pass point sums based on prepared data
1916  // for question that exists in users qst sequence
1917  $pass_max += round($unordered[$qid]['max'], 2);
1918  $pass_reached += round($unordered[$qid]['reached'], 2);
1919  $pass_requested_hints += $unordered[$qid]['requested_hints'];
1920  $pass_hint_points += $unordered[$qid]['hint_points'];
1921  $found[] = $unordered[$qid];
1922  }
1923 
1924  $unordered = null;
1925 
1926  if ($this->getScoreCutting() == 1) {
1927  if ($results['reached_points'] < 0) {
1928  $results['reached_points'] = 0;
1929  }
1930 
1931  if ($pass_reached < 0) {
1932  $pass_reached = 0;
1933  }
1934  }
1935 
1936  $found['pass']['total_max_points'] = $pass_max;
1937  $found['pass']['total_reached_points'] = $pass_reached;
1938  $found['pass']['total_requested_hints'] = $pass_requested_hints;
1939  $found['pass']['total_hint_points'] = $pass_hint_points;
1940  $found['pass']['percent'] = ($pass_max > 0) ? $pass_reached / $pass_max : 0;
1941  $found['pass']['num_workedthrough'] = $num_worked_through;
1942  $found['pass']['num_questions_total'] = $numQuestionsTotal;
1943 
1944  $found["test"]["total_max_points"] = $results['max_points'];
1945  $found["test"]["total_reached_points"] = $results['reached_points'];
1946  $found["test"]["total_requested_hints"] = $results['hint_count'];
1947  $found["test"]["total_hint_points"] = $results['hint_points'];
1948  $found["test"]["result_pass"] = $results['pass'];
1949  $found['test']['result_tstamp'] = $results['tstamp'];
1950 
1951  if ((!$found['pass']['total_reached_points']) or (!$found['pass']['total_max_points'])) {
1952  $percentage = 0.0;
1953  } else {
1954  $percentage = ($found['pass']['total_reached_points'] / $found['pass']['total_max_points']) * 100.0;
1955 
1956  if ($percentage < 0) {
1957  $percentage = 0.0;
1958  }
1959  }
1960 
1961  $found["test"]["passed"] = $results['passed'];
1962 
1963  return $found;
1964  }
1965 
1972  public function evalTotalPersons(): int
1973  {
1974  $result = $this->db->queryF(
1975  'SELECT COUNT(active_id) total FROM tst_active WHERE test_fi = %s',
1976  ['integer'],
1977  [$this->getTestId()]
1978  );
1979  $row = $this->db->fetchAssoc($result);
1980  return $row['total'];
1981  }
1982 
1990  {
1991  $result = $this->db->queryF(
1992  "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",
1993  ['integer','integer'],
1994  [$this->getTestId(), $user_id]
1995  );
1996  $time = 0;
1997  while ($row = $this->db->fetchAssoc($result)) {
1998  preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["started"], $matches);
1999  $epoch_1 = mktime(
2000  (int) $matches[4],
2001  (int) $matches[5],
2002  (int) $matches[6],
2003  (int) $matches[2],
2004  (int) $matches[3],
2005  (int) $matches[1]
2006  );
2007  preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["finished"], $matches);
2008  $epoch_2 = mktime(
2009  (int) $matches[4],
2010  (int) $matches[5],
2011  (int) $matches[6],
2012  (int) $matches[2],
2013  (int) $matches[3],
2014  (int) $matches[1]
2015  );
2016  $time += ($epoch_2 - $epoch_1);
2017  }
2018  return $time;
2019  }
2020 
2027  public function &getCompleteWorkingTimeOfParticipants(): array
2028  {
2029  return $this->_getCompleteWorkingTimeOfParticipants($this->getTestId());
2030  }
2031 
2039  public function &_getCompleteWorkingTimeOfParticipants($test_id): array
2040  {
2041  $result = $this->db->queryF(
2042  "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",
2043  ['integer'],
2044  [$test_id]
2045  );
2046  $time = 0;
2047  $times = [];
2048  while ($row = $this->db->fetchAssoc($result)) {
2049  if (!array_key_exists($row["active_fi"], $times)) {
2050  $times[$row["active_fi"]] = 0;
2051  }
2052  preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["started"], $matches);
2053  $epoch_1 = mktime(
2054  (int) $matches[4],
2055  (int) $matches[5],
2056  (int) $matches[6],
2057  (int) $matches[2],
2058  (int) $matches[3],
2059  (int) $matches[1]
2060  );
2061  preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["finished"], $matches);
2062  $epoch_2 = mktime(
2063  (int) $matches[4],
2064  (int) $matches[5],
2065  (int) $matches[6],
2066  (int) $matches[2],
2067  (int) $matches[3],
2068  (int) $matches[1]
2069  );
2070  $times[$row["active_fi"]] += ($epoch_2 - $epoch_1);
2071  }
2072  return $times;
2073  }
2074 
2081  public function getCompleteWorkingTimeOfParticipant($active_id): int
2082  {
2083  $result = $this->db->queryF(
2084  "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",
2085  ['integer','integer'],
2086  [$this->getTestId(), $active_id]
2087  );
2088  $time = 0;
2089  while ($row = $this->db->fetchAssoc($result)) {
2090  preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["started"], $matches);
2091  $epoch_1 = mktime(
2092  (int) $matches[4],
2093  (int) $matches[5],
2094  (int) $matches[6],
2095  (int) $matches[2],
2096  (int) $matches[3],
2097  (int) $matches[1]
2098  );
2099  preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["finished"], $matches);
2100  $epoch_2 = mktime(
2101  (int) $matches[4],
2102  (int) $matches[5],
2103  (int) $matches[6],
2104  (int) $matches[2],
2105  (int) $matches[3],
2106  (int) $matches[1]
2107  );
2108  $time += ($epoch_2 - $epoch_1);
2109  }
2110  return $time;
2111  }
2112 
2113  public function getWorkingTimeOfParticipantForPass(int $active_id, int $pass): int
2114  {
2115  $result = $this->db->queryF(
2116  "SELECT * FROM tst_times WHERE active_fi = %s AND pass = %s ORDER BY started",
2117  ['integer','integer'],
2118  [$active_id, $pass]
2119  );
2120  $time = 0;
2121  while ($row = $this->db->fetchAssoc($result)) {
2122  preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["started"], $matches);
2123  $epoch_1 = mktime(
2124  (int) $matches[4],
2125  (int) $matches[5],
2126  (int) $matches[6],
2127  (int) $matches[2],
2128  (int) $matches[3],
2129  (int) $matches[1]
2130  );
2131  preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["finished"], $matches);
2132  $epoch_2 = mktime(
2133  (int) $matches[4],
2134  (int) $matches[5],
2135  (int) $matches[6],
2136  (int) $matches[2],
2137  (int) $matches[3],
2138  (int) $matches[1]
2139  );
2140  $time += ($epoch_2 - $epoch_1);
2141  }
2142  return $time;
2143  }
2144 
2148  public function evalStatistical($active_id): array
2149  {
2150  $pass = ilObjTest::_getResultPass($active_id);
2151  $test_result = &$this->getTestResult($active_id, $pass);
2152  $result = $this->db->queryF(
2153  "SELECT tst_times.* FROM tst_active, tst_times WHERE tst_active.active_id = %s AND tst_active.active_id = tst_times.active_fi",
2154  ['integer'],
2155  [$active_id]
2156  );
2157  $times = [];
2158  $first_visit = 0;
2159  $last_visit = 0;
2160  while ($row = $this->db->fetchObject($result)) {
2161  preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row->started, $matches);
2162  $epoch_1 = mktime(
2163  (int) $matches[4],
2164  (int) $matches[5],
2165  (int) $matches[6],
2166  (int) $matches[2],
2167  (int) $matches[3],
2168  (int) $matches[1]
2169  );
2170  if (!$first_visit) {
2171  $first_visit = $epoch_1;
2172  }
2173  if ($epoch_1 < $first_visit) {
2174  $first_visit = $epoch_1;
2175  }
2176  preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row->finished, $matches);
2177  $epoch_2 = mktime(
2178  (int) $matches[4],
2179  (int) $matches[5],
2180  (int) $matches[6],
2181  (int) $matches[2],
2182  (int) $matches[3],
2183  (int) $matches[1]
2184  );
2185  if (!$last_visit) {
2186  $last_visit = $epoch_2;
2187  }
2188  if ($epoch_2 > $last_visit) {
2189  $last_visit = $epoch_2;
2190  }
2191  $times[$row->active_fi] += ($epoch_2 - $epoch_1);
2192  }
2193  $max_time = 0;
2194  foreach ($times as $key => $value) {
2195  $max_time += $value;
2196  }
2197  if ((!$test_result["test"]["total_reached_points"]) or (!$test_result["test"]["total_max_points"])) {
2198  $percentage = 0.0;
2199  } else {
2200  $percentage = ($test_result["test"]["total_reached_points"] / $test_result["test"]["total_max_points"]) * 100.0;
2201  if ($percentage < 0) {
2202  $percentage = 0.0;
2203  }
2204  }
2205  $mark_obj = $this->getMarkSchema()->getMatchingMark($percentage);
2206  $first_date = getdate($first_visit);
2207  $last_date = getdate($last_visit);
2208  $qworkedthrough = 0;
2209  foreach ($test_result as $key => $value) {
2210  if (preg_match("/\d+/", $key)) {
2211  $qworkedthrough += $value["workedthrough"];
2212  }
2213  }
2214  if (!$qworkedthrough) {
2215  $atimeofwork = 0;
2216  } else {
2217  $atimeofwork = $max_time / $qworkedthrough;
2218  }
2219 
2220  $result_mark = "";
2221  $passed = "";
2222 
2223  if ($mark_obj !== null) {
2224  $result_mark = $mark_obj->getShortName();
2225 
2226  if ($mark_obj->getPassed()) {
2227  $passed = 1;
2228  } else {
2229  $passed = 0;
2230  }
2231  }
2232  $percent_worked_through = 0;
2233  if (count($this->questions)) {
2234  $percent_worked_through = $qworkedthrough / count($this->questions);
2235  }
2236  $result_array = [
2237  "qworkedthrough" => $qworkedthrough,
2238  "qmax" => count($this->questions),
2239  "pworkedthrough" => $percent_worked_through,
2240  "timeofwork" => $max_time,
2241  "atimeofwork" => $atimeofwork,
2242  "firstvisit" => $first_date,
2243  "lastvisit" => $last_date,
2244  "resultspoints" => $test_result["test"]["total_reached_points"],
2245  "maxpoints" => $test_result["test"]["total_max_points"],
2246  "resultsmarks" => $result_mark,
2247  "passed" => $passed,
2248  "distancemedian" => "0"
2249  ];
2250  foreach ($test_result as $key => $value) {
2251  if (preg_match("/\d+/", $key)) {
2252  $result_array[$key] = $value;
2253  }
2254  }
2255  return $result_array;
2256  }
2257 
2265  public function getTotalPointsPassedArray(): array
2266  {
2267  $totalpoints_array = [];
2268  $all_users = $this->evalTotalParticipantsArray();
2269  foreach ($all_users as $active_id => $user_name) {
2270  $test_result = &$this->getTestResult($active_id);
2271  $reached = $test_result["test"]["total_reached_points"];
2272  $total = $test_result["test"]["total_max_points"];
2273  $percentage = $total != 0 ? $reached / $total : 0;
2274  $mark = $this->getMarkSchema()->getMatchingMark($percentage * 100.0);
2275 
2276  if ($mark !== null && $mark->getPassed()) {
2277  array_push($totalpoints_array, $test_result["test"]["total_reached_points"]);
2278  }
2279  }
2280  return $totalpoints_array;
2281  }
2282 
2288  public function getParticipants(): array
2289  {
2290  $result = $this->db->queryF(
2291  "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",
2292  ['integer'],
2293  [$this->getTestId()]
2294  );
2295  $persons_array = [];
2296  while ($row = $this->db->fetchAssoc($result)) {
2297  $name = $this->lng->txt("anonymous");
2298  $fullname = $this->lng->txt("anonymous");
2299  $login = "";
2300  if (!$this->getAnonymity()) {
2301  if (strlen($row["firstname"] . $row["lastname"] . $row["title"]) == 0) {
2302  $name = $this->lng->txt("deleted_user");
2303  $fullname = $this->lng->txt("deleted_user");
2304  $login = $this->lng->txt("unknown");
2305  } else {
2306  $login = $row["login"];
2307  if ($row["usr_id"] == ANONYMOUS_USER_ID) {
2308  $name = $this->lng->txt("anonymous");
2309  $fullname = $this->lng->txt("anonymous");
2310  } else {
2311  $name = trim($row["lastname"] . ", " . $row["firstname"] . " " . $row["title"]);
2312  $fullname = trim($row["title"] . " " . $row["firstname"] . " " . $row["lastname"]);
2313  }
2314  }
2315  }
2316  $persons_array[$row["active_id"]] = [
2317  "name" => $name,
2318  "fullname" => $fullname,
2319  "login" => $login
2320  ];
2321  }
2322  return $persons_array;
2323  }
2324 
2325  public function evalTotalPersonsArray(string $name_sort_order = 'asc'): array
2326  {
2327  $result = $this->db->queryF(
2328  "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),
2329  ['integer'],
2330  [$this->getTestId()]
2331  );
2332  $persons_array = [];
2333  while ($row = $this->db->fetchAssoc($result)) {
2334  if ($this->getAccessFilteredParticipantList() && !$this->getAccessFilteredParticipantList()->isActiveIdInList($row["active_id"])) {
2335  continue;
2336  }
2337 
2338  if ($this->getAnonymity()) {
2339  $persons_array[$row["active_id"]] = $this->lng->txt("anonymous");
2340  } else {
2341  if (strlen($row["firstname"] . $row["lastname"] . $row["title"]) == 0) {
2342  $persons_array[$row["active_id"]] = $this->lng->txt("deleted_user");
2343  } else {
2344  if ($row["user_fi"] == ANONYMOUS_USER_ID) {
2345  $persons_array[$row["active_id"]] = $row["lastname"];
2346  } else {
2347  $persons_array[$row["active_id"]] = trim($row["lastname"] . ", " . $row["firstname"] . " " . $row["title"]);
2348  }
2349  }
2350  }
2351  }
2352  return $persons_array;
2353  }
2354 
2355  public function evalTotalParticipantsArray(string $name_sort_order = 'asc'): array
2356  {
2357  $result = $this->db->queryF(
2358  'SELECT tst_active.user_fi, tst_active.active_id, usr_data.login, '
2359  . 'usr_data.firstname, usr_data.lastname, usr_data.title FROM tst_active '
2360  . 'LEFT JOIN usr_data ON tst_active.user_fi = usr_data.usr_id '
2361  . 'WHERE tst_active.test_fi = %s '
2362  . 'ORDER BY usr_data.lastname ' . strtoupper($name_sort_order),
2363  ['integer'],
2364  [$this->getTestId()]
2365  );
2366  $persons_array = [];
2367  while ($row = $this->db->fetchAssoc($result)) {
2368  if ($this->getAnonymity()) {
2369  $persons_array[$row['active_id']] = ['name' => $this->lng->txt("anonymous")];
2370  } else {
2371  if (strlen($row['firstname'] . $row['lastname'] . $row["title"]) == 0) {
2372  $persons_array[$row['active_id']] = ['name' => $this->lng->txt('deleted_user')];
2373  } else {
2374  if ($row['user_fi'] == ANONYMOUS_USER_ID) {
2375  $persons_array[$row['active_id']] = ['name' => $row['lastname']];
2376  } else {
2377  $persons_array[$row['active_id']] = [
2378  'name' => trim($row['lastname'] . ', ' . $row['firstname']
2379  . ' ' . $row['title']),
2380  'login' => $row['login']
2381  ];
2382  }
2383  }
2384  }
2385  }
2386  return $persons_array;
2387  }
2388 
2389  public function getQuestionsOfTest(int $active_id): array
2390  {
2391  if ($this->isRandomTest()) {
2392  $this->db->setLimit($this->getQuestionCount(), 0);
2393  $result = $this->db->queryF(
2394  'SELECT tst_test_rnd_qst.sequence, tst_test_rnd_qst.question_fi, '
2395  . 'tst_test_rnd_qst.pass, qpl_questions.points '
2396  . 'FROM tst_test_rnd_qst, qpl_questions '
2397  . 'WHERE tst_test_rnd_qst.question_fi = qpl_questions.question_id '
2398  . 'AND tst_test_rnd_qst.active_fi = %s ORDER BY tst_test_rnd_qst.sequence',
2399  ['integer'],
2400  [$active_id]
2401  );
2402  } else {
2403  $result = $this->db->queryF(
2404  'SELECT tst_test_question.sequence, tst_test_question.question_fi, '
2405  . 'qpl_questions.points '
2406  . 'FROM tst_test_question, tst_active, qpl_questions '
2407  . 'WHERE tst_test_question.question_fi = qpl_questions.question_id '
2408  . 'AND tst_active.active_id = %s AND tst_active.test_fi = tst_test_question.test_fi',
2409  ['integer'],
2410  [$active_id]
2411  );
2412  }
2413  $qtest = [];
2414  if ($result->numRows()) {
2415  while ($row = $this->db->fetchAssoc($result)) {
2416  array_push($qtest, $row);
2417  }
2418  }
2419  return $qtest;
2420  }
2421 
2422  public function getQuestionsOfPass(int $active_id, int $pass): array
2423  {
2424  if ($this->isRandomTest()) {
2425  $this->db->setLimit($this->getQuestionCount(), 0);
2426  $result = $this->db->queryF(
2427  'SELECT tst_test_rnd_qst.sequence, tst_test_rnd_qst.question_fi, '
2428  . 'qpl_questions.points '
2429  . 'FROM tst_test_rnd_qst, qpl_questions '
2430  . 'WHERE tst_test_rnd_qst.question_fi = qpl_questions.question_id '
2431  . 'AND tst_test_rnd_qst.active_fi = %s AND tst_test_rnd_qst.pass = %s '
2432  . 'ORDER BY tst_test_rnd_qst.sequence',
2433  ['integer', 'integer'],
2434  [$active_id, $pass]
2435  );
2436  } else {
2437  $result = $this->db->queryF(
2438  'SELECT tst_test_question.sequence, tst_test_question.question_fi, '
2439  . 'qpl_questions.points '
2440  . 'FROM tst_test_question, tst_active, qpl_questions '
2441  . 'WHERE tst_test_question.question_fi = qpl_questions.question_id '
2442  . 'AND tst_active.active_id = %s AND tst_active.test_fi = tst_test_question.test_fi',
2443  ['integer'],
2444  [$active_id]
2445  );
2446  }
2447  $qpass = [];
2448  if ($result->numRows()) {
2449  while ($row = $this->db->fetchAssoc($result)) {
2450  array_push($qpass, $row);
2451  }
2452  }
2453  return $qpass;
2454  }
2455 
2457  {
2459  }
2460 
2461  public function setAccessFilteredParticipantList(ilTestParticipantList $access_filtered_participant_list): void
2462  {
2463  $this->access_filtered_participant_list = $access_filtered_participant_list;
2464  }
2465 
2467  {
2468  $list = new ilTestParticipantList($this, $this->user, $this->lng, $this->db);
2469  $list->initializeFromDbRows($this->getTestParticipants());
2470 
2471  return $list->getAccessFilteredList(
2472  $this->participant_access_filter->getAccessStatisticsUserFilter($this->getRefId())
2473  );
2474  }
2475 
2477  {
2478  return (new ilTestEvaluationFactory($this->db, $this))
2479  ->getEvaluationData();
2480  }
2481 
2482  public function getQuestionCountAndPointsForPassOfParticipant(int $active_id, int $pass): array
2483  {
2484  $question_set_type = $this->lookupQuestionSetTypeByActiveId($active_id);
2485 
2486  switch ($question_set_type) {
2488  $res = $this->db->queryF(
2489  "
2490  SELECT tst_test_rnd_qst.pass,
2491  COUNT(tst_test_rnd_qst.question_fi) qcount,
2492  SUM(qpl_questions.points) qsum
2493 
2494  FROM tst_test_rnd_qst,
2495  qpl_questions
2496 
2497  WHERE tst_test_rnd_qst.question_fi = qpl_questions.question_id
2498  AND tst_test_rnd_qst.active_fi = %s
2499  AND pass = %s
2500 
2501  GROUP BY tst_test_rnd_qst.active_fi,
2502  tst_test_rnd_qst.pass
2503  ",
2504  ['integer', 'integer'],
2505  [$active_id, $pass]
2506  );
2507  break;
2508 
2510  $res = $this->db->queryF(
2511  "
2512  SELECT COUNT(tst_test_question.question_fi) qcount,
2513  SUM(qpl_questions.points) qsum
2514 
2515  FROM tst_test_question,
2516  qpl_questions,
2517  tst_active
2518 
2519  WHERE tst_test_question.question_fi = qpl_questions.question_id
2520  AND tst_test_question.test_fi = tst_active.test_fi
2521  AND tst_active.active_id = %s
2522 
2523  GROUP BY tst_test_question.test_fi
2524  ",
2525  ['integer'],
2526  [$active_id]
2527  );
2528  break;
2529 
2530  default:
2531  throw new ilTestException("not supported question set type: $question_set_type");
2532  }
2533 
2534  $row = $this->db->fetchAssoc($res);
2535 
2536  if (is_array($row)) {
2537  return ["count" => $row["qcount"], "points" => $row["qsum"]];
2538  }
2539 
2540  return ["count" => 0, "points" => 0];
2541  }
2542 
2543  public function getCompleteEvaluationData($filterby = '', $filtertext = ''): ilTestEvaluationData
2544  {
2546  $data->setFilter($filterby, $filtertext);
2547  return $data;
2548  }
2549 
2561  public function buildName(
2562  ?int $user_id,
2563  ?string $firstname,
2564  ?string $lastname
2565  ): string {
2566  if ($user_id === null
2567  || $firstname . $lastname === '') {
2568  return $this->lng->txt('deleted_user');
2569  }
2570 
2571  if ($this->getAnonymity()) {
2572  return $this->lng->txt('anonymous');
2573  }
2574 
2575  if ($user_id == ANONYMOUS_USER_ID) {
2576  return $lastname;
2577  }
2578 
2579  return trim($lastname . ', ' . $firstname);
2580  }
2581 
2582  public function evalTotalStartedAverageTime(?array $active_ids_to_filter = null): int
2583  {
2584  $query = "SELECT tst_times.* FROM tst_active, tst_times WHERE tst_active.test_fi = %s AND tst_active.active_id = tst_times.active_fi";
2585 
2586  if ($active_ids_to_filter !== null && $active_ids_to_filter !== []) {
2587  $query .= " AND " . $this->db->in('active_id', $active_ids_to_filter, false, 'integer');
2588  }
2589 
2590  $result = $this->db->queryF($query, ['integer'], [$this->getTestId()]);
2591  $times = [];
2592  while ($row = $this->db->fetchObject($result)) {
2593  preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row->started, $matches);
2594  $epoch_1 = mktime(
2595  (int) $matches[4],
2596  (int) $matches[5],
2597  (int) $matches[6],
2598  (int) $matches[2],
2599  (int) $matches[3],
2600  (int) $matches[1]
2601  );
2602  preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row->finished, $matches);
2603  $epoch_2 = mktime(
2604  (int) $matches[4],
2605  (int) $matches[5],
2606  (int) $matches[6],
2607  (int) $matches[2],
2608  (int) $matches[3],
2609  (int) $matches[1]
2610  );
2611  if (isset($times[$row->active_fi])) {
2612  $times[$row->active_fi] += ($epoch_2 - $epoch_1);
2613  } else {
2614  $times[$row->active_fi] = ($epoch_2 - $epoch_1);
2615  }
2616  }
2617  $max_time = 0;
2618  $counter = 0;
2619  foreach ($times as $value) {
2620  $max_time += $value;
2621  $counter++;
2622  }
2623  if ($counter === 0) {
2624  return 0;
2625  }
2626  return (int) round($max_time / $counter);
2627  }
2628 
2635  public function getAvailableQuestionpools(
2636  bool $use_object_id = false,
2637  ?bool $equal_points = false,
2638  bool $could_be_offline = false,
2639  bool $show_path = false,
2640  bool $with_questioncount = false,
2641  string $permission = 'read'
2642  ): array {
2644  $use_object_id,
2645  $equal_points ?? false,
2646  $could_be_offline,
2647  $show_path,
2648  $with_questioncount,
2649  $permission
2650  );
2651  }
2652 
2659  public function getImagePath(): string
2660  {
2661  return CLIENT_WEB_DIR . "/assessment/" . $this->getId() . "/images/";
2662  }
2663 
2670  public function getImagePathWeb()
2671  {
2672  $webdir = ilFileUtils::removeTrailingPathSeparators(CLIENT_WEB_DIR) . "/assessment/" . $this->getId() . "/images/";
2673  return str_replace(
2674  ilFileUtils::removeTrailingPathSeparators(ILIAS_ABSOLUTE_PATH),
2676  $webdir
2677  );
2678  }
2679 
2688  public function createQuestionGUI($question_type, $question_id = -1): ?assQuestionGUI
2689  {
2690  if ((!$question_type) and ($question_id > 0)) {
2691  $question_type = $this->getQuestionType($question_id);
2692  }
2693 
2694  if (!strlen($question_type)) {
2695  return null;
2696  }
2697 
2698  if ($question_id > 0) {
2699  $question_gui = assQuestion::instantiateQuestionGUI($question_id);
2700  } else {
2701  $question_type_gui = $question_type . 'GUI';
2702  $question_gui = new $question_type_gui();
2703  }
2704 
2705  return $question_gui;
2706  }
2707 
2714  public static function _instanciateQuestion($question_id): ?assQuestion
2715  {
2716  if (strcmp((string) $question_id, "") !== 0) {
2717  return assQuestion::instantiateQuestion((int) $question_id);
2718  }
2719 
2720  return null;
2721  }
2722 
2727  public function moveQuestions(array $move_questions, int $target_index, int $insert_mode): void
2728  {
2729  $this->questions = array_values($this->questions);
2730  $array_pos = array_search($target_index, $this->questions);
2731  if ($insert_mode == 0) {
2732  $part1 = array_slice($this->questions, 0, $array_pos);
2733  $part2 = array_slice($this->questions, $array_pos);
2734  } elseif ($insert_mode == 1) {
2735  $part1 = array_slice($this->questions, 0, $array_pos + 1);
2736  $part2 = array_slice($this->questions, $array_pos + 1);
2737  }
2738  foreach ($move_questions as $question_id) {
2739  if (!(array_search($question_id, $part1) === false)) {
2740  unset($part1[array_search($question_id, $part1)]);
2741  }
2742  if (!(array_search($question_id, $part2) === false)) {
2743  unset($part2[array_search($question_id, $part2)]);
2744  }
2745  }
2746  $part1 = array_values($part1);
2747  $part2 = array_values($part2);
2748  $new_array = array_values(array_merge($part1, $move_questions, $part2));
2749  $this->questions = [];
2750  $counter = 1;
2751  foreach ($new_array as $question_id) {
2752  $this->questions[$counter] = $question_id;
2753  $counter++;
2754  }
2755  $this->saveQuestionsToDb();
2756 
2757  if ($this->logger->isLoggingEnabled()) {
2758  $this->logger->logTestAdministrationInteraction(
2759  $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
2760  $this->getRefId(),
2761  $this->user->getId(),
2762  TestAdministrationInteractionTypes::QUESTION_MOVED,
2763  [
2764  AdditionalInformationGenerator::KEY_QUESTION_ORDER => $this->questions
2765  ]
2766  )
2767  );
2768  }
2769  }
2770 
2771 
2779  public function startingTimeReached(): bool
2780  {
2781  if ($this->isStartingTimeEnabled() && $this->getStartingTime() != 0) {
2782  $now = time();
2783  if ($now < $this->getStartingTime()) {
2784  return false;
2785  }
2786  }
2787  return true;
2788  }
2789 
2797  public function endingTimeReached(): bool
2798  {
2799  if ($this->isEndingTimeEnabled() && $this->getEndingTime() != 0) {
2800  $now = time();
2801  if ($now > $this->getEndingTime()) {
2802  return true;
2803  }
2804  }
2805  return false;
2806  }
2807 
2813  public function getAvailableQuestions($arr_filter, $completeonly = 0): array
2814  {
2815  $available_pools = array_keys(ilObjQuestionPool::_getAvailableQuestionpools(true, false, false, false, false));
2816  $available = "";
2817  if (count($available_pools)) {
2818  $available = " AND " . $this->db->in('qpl_questions.obj_fi', $available_pools, false, 'integer');
2819  } else {
2820  return [];
2821  }
2822  if ($completeonly) {
2823  $available .= " AND qpl_questions.complete = " . $this->db->quote("1", 'text');
2824  }
2825 
2826  $where = "";
2827  if (is_array($arr_filter)) {
2828  if (array_key_exists('title', $arr_filter) && strlen($arr_filter['title'])) {
2829  $where .= " AND " . $this->db->like('qpl_questions.title', 'text', "%%" . $arr_filter['title'] . "%%");
2830  }
2831  if (array_key_exists('description', $arr_filter) && strlen($arr_filter['description'])) {
2832  $where .= " AND " . $this->db->like('qpl_questions.description', 'text', "%%" . $arr_filter['description'] . "%%");
2833  }
2834  if (array_key_exists('author', $arr_filter) && strlen($arr_filter['author'])) {
2835  $where .= " AND " . $this->db->like('qpl_questions.author', 'text', "%%" . $arr_filter['author'] . "%%");
2836  }
2837  if (array_key_exists('type', $arr_filter) && strlen($arr_filter['type'])) {
2838  $where .= " AND qpl_qst_type.type_tag = " . $this->db->quote($arr_filter['type'], 'text');
2839  }
2840  if (array_key_exists('qpl', $arr_filter) && strlen($arr_filter['qpl'])) {
2841  $where .= " AND " . $this->db->like('object_data.title', 'text', "%%" . $arr_filter['qpl'] . "%%");
2842  }
2843  }
2844 
2845  $original_ids = &$this->getExistingQuestions();
2846  $original_clause = " qpl_questions.original_id IS NULL";
2847  if (count($original_ids)) {
2848  $original_clause = " qpl_questions.original_id IS NULL AND " . $this->db->in('qpl_questions.question_id', $original_ids, true, 'integer');
2849  }
2850 
2851  $query_result = $this->db->query("
2852  SELECT qpl_questions.*, qpl_questions.tstamp,
2853  qpl_qst_type.type_tag, qpl_qst_type.plugin, qpl_qst_type.plugin_name,
2854  object_data.title parent_title
2855  FROM qpl_questions, qpl_qst_type, object_data
2856  WHERE $original_clause $available
2857  AND object_data.obj_id = qpl_questions.obj_fi
2858  AND qpl_questions.tstamp > 0
2859  AND qpl_questions.question_type_fi = qpl_qst_type.question_type_id
2860  $where
2861  ");
2862  $rows = [];
2863 
2864  if ($query_result->numRows()) {
2865  while ($row = $this->db->fetchAssoc($query_result)) {
2867 
2868  if (!$row['plugin']) {
2869  $row[ 'ttype' ] = $this->lng->txt($row[ "type_tag" ]);
2870 
2871  $rows[] = $row;
2872  continue;
2873  }
2874 
2875  $plugin = $this->component_repository->getPluginByName($row['plugin_name']);
2876  if (!$plugin->isActive()) {
2877  continue;
2878  }
2879 
2880  $pl = $this->component_factory->getPlugin($plugin->getId());
2881  $row[ 'ttype' ] = $pl->getQuestionTypeTranslation();
2882 
2883  $rows[] = $row;
2884  }
2885  }
2886  return $rows;
2887  }
2888 
2893  public function fromXML(ilQTIAssessment $assessment, array $mappings): void
2894  {
2895  if (($importdir = ilSession::get('path_to_container_import_file')) === null) {
2896  $importdir = $this->buildImportDirectoryFromImportFile(ilSession::get('path_to_import_file'));
2897  }
2898  ilSession::clear('path_to_container_import_file');
2899  ilSession::clear('import_mob_xhtml');
2900 
2901  $this->saveToDb(true);
2902 
2903  $main_settings = $this->getMainSettings();
2904  $general_settings = $main_settings->getGeneralSettings();
2905  $introduction_settings = $main_settings->getIntroductionSettings();
2906  $access_settings = $main_settings->getAccessSettings();
2907  $test_behaviour_settings = $main_settings->getTestBehaviourSettings();
2908  $question_behaviour_settings = $main_settings->getQuestionBehaviourSettings();
2909  $participant_functionality_settings = $main_settings->getParticipantFunctionalitySettings();
2910  $finishing_settings = $main_settings->getFinishingSettings();
2911  $additional_settings = $main_settings->getAdditionalSettings();
2912 
2913  $introduction_settings = $introduction_settings->withIntroductionEnabled(false);
2914  foreach ($assessment->objectives as $objectives) {
2915  foreach ($objectives->materials as $material) {
2916  $introduction_settings = $this->addIntroductionToSettingsFromImport(
2917  $introduction_settings,
2918  $this->qtiMaterialToArray($material),
2919  $importdir,
2920  $mappings
2921  );
2922  }
2923  }
2924 
2925  if ($assessment->getPresentationMaterial()
2926  && $assessment->getPresentationMaterial()->getFlowMat(0)
2927  && $assessment->getPresentationMaterial()->getFlowMat(0)->getMaterial(0)) {
2928  $finishing_settings = $this->addConcludingRemarksToSettingsFromImport(
2929  $finishing_settings,
2930  $this->qtiMaterialToArray(
2931  $assessment->getPresentationMaterial()->getFlowMat(0)->getMaterial(0)
2932  ),
2933  $importdir,
2934  $mappings
2935  );
2936  }
2937 
2938  $score_settings = $this->getScoreSettings();
2939  $scoring_settings = $score_settings->getScoringSettings();
2940  $gamification_settings = $score_settings->getGamificationSettings();
2941  $result_summary_settings = $score_settings->getResultSummarySettings();
2942  $result_details_settings = $score_settings->getResultDetailsSettings();
2943 
2944  $mark_steps = [];
2945  foreach ($assessment->qtimetadata as $metadata) {
2946  switch ($metadata["label"]) {
2947  case "solution_details":
2948  $result_details_settings = $result_details_settings->withShowPassDetails((bool) $metadata["entry"]);
2949  break;
2950  case "show_solution_list_comparison":
2951  $result_details_settings = $result_details_settings->withShowSolutionListComparison((bool) $metadata["entry"]);
2952  break;
2953  case "print_bs_with_res":
2954  $result_details_settings = $result_details_settings->withShowSolutionListComparison((bool) $metadata["entry"]);
2955  break;
2956  case "author":
2957  $this->saveAuthorToMetadata($metadata["entry"]);
2958  break;
2959  case "nr_of_tries":
2960  $test_behaviour_settings = $test_behaviour_settings->withNumberOfTries((int) $metadata["entry"]);
2961  break;
2962  case 'block_after_passed':
2963  $test_behaviour_settings = $test_behaviour_settings->withBlockAfterPassedEnabled((bool) $metadata['entry']);
2964  break;
2965  case "pass_waiting":
2966  $test_behaviour_settings = $test_behaviour_settings->withPassWaiting($metadata["entry"]);
2967  break;
2968  case "kiosk":
2969  $test_behaviour_settings = $test_behaviour_settings->withKioskMode((int) $metadata["entry"]);
2970  break;
2971  case 'show_introduction':
2972  $introduction_settings = $introduction_settings->withIntroductionEnabled((bool) $metadata['entry']);
2973  break;
2974  case "showfinalstatement":
2975  case 'show_concluding_remarks':
2976  $finishing_settings = $finishing_settings->withConcludingRemarksEnabled((bool) $metadata["entry"]);
2977  break;
2978  case 'exam_conditions':
2979  $introduction_settings = $introduction_settings->withExamConditionsCheckboxEnabled($metadata['entry'] === '1');
2980  break;
2981  case "highscore_enabled":
2982  $gamification_settings = $gamification_settings->withHighscoreEnabled((bool) $metadata["entry"]);
2983  break;
2984  case "highscore_anon":
2985  $gamification_settings = $gamification_settings->withHighscoreAnon((bool) $metadata["entry"]);
2986  break;
2987  case "highscore_achieved_ts":
2988  $gamification_settings = $gamification_settings->withHighscoreAchievedTS((bool) $metadata["entry"]);
2989  break;
2990  case "highscore_score":
2991  $gamification_settings = $gamification_settings->withHighscoreScore((bool) $metadata["entry"]);
2992  break;
2993  case "highscore_percentage":
2994  $gamification_settings = $gamification_settings->withHighscorePercentage((bool) $metadata["entry"]);
2995  break;
2996  case "highscore_hints":
2997  $gamification_settings = $gamification_settings->withHighscoreHints((bool) $metadata["entry"]);
2998  break;
2999  case "highscore_wtime":
3000  $gamification_settings = $gamification_settings->withHighscoreWTime((bool) $metadata["entry"]);
3001  break;
3002  case "highscore_own_table":
3003  $gamification_settings = $gamification_settings->withHighscoreOwnTable((bool) $metadata["entry"]);
3004  break;
3005  case "highscore_top_table":
3006  $gamification_settings = $gamification_settings->withHighscoreTopTable((bool) $metadata["entry"]);
3007  break;
3008  case "highscore_top_num":
3009  $gamification_settings = $gamification_settings->withHighscoreTopNum((int) $metadata["entry"]);
3010  break;
3011  case "use_previous_answers":
3012  $participant_functionality_settings = $participant_functionality_settings->withUsePreviousAnswerAllowed((bool) $metadata["entry"]);
3013  break;
3014  case 'question_list_enabled':
3015  $participant_functionality_settings = $participant_functionality_settings->withQuestionListEnabled((bool) $metadata['entry']);
3016  // no break
3017  case "title_output":
3018  $question_behaviour_settings = $question_behaviour_settings->withQuestionTitleOutputMode((int) $metadata["entry"]);
3019  break;
3020  case "question_set_type":
3021  if ($metadata['entry'] === self::QUESTION_SET_TYPE_RANDOM) {
3022  $this->questions = [];
3023  }
3024  $general_settings = $general_settings->withQuestionSetType($metadata["entry"]);
3025  break;
3026  case "anonymity":
3027  $general_settings = $general_settings->withAnonymity((bool) $metadata["entry"]);
3028  break;
3029  case "results_presentation":
3030  $result_details_settings = $result_details_settings->withResultsPresentation((int) $metadata["entry"]);
3031  break;
3032  case "reset_processing_time":
3033  $test_behaviour_settings = $test_behaviour_settings->withResetProcessingTime($metadata["entry"] === '1');
3034  break;
3035  case "answer_feedback_points":
3036  $question_behaviour_settings = $question_behaviour_settings->withInstantFeedbackPointsEnabled((bool) $metadata["entry"]);
3037  break;
3038  case "answer_feedback":
3039  $question_behaviour_settings = $question_behaviour_settings->withInstantFeedbackGenericEnabled((bool) $metadata["entry"]);
3040  break;
3041  case 'instant_feedback_specific':
3042  $question_behaviour_settings = $question_behaviour_settings->withInstantFeedbackSpecificEnabled((bool) $metadata['entry']);
3043  break;
3044  case "instant_verification":
3045  $question_behaviour_settings = $question_behaviour_settings->withInstantFeedbackSolutionEnabled((bool) $metadata["entry"]);
3046  break;
3047  case "force_instant_feedback":
3048  $question_behaviour_settings = $question_behaviour_settings->withForceInstantFeedbackOnNextQuestion((bool) $metadata["entry"]);
3049  break;
3050  case "follow_qst_answer_fixation":
3051  $question_behaviour_settings = $question_behaviour_settings->withLockAnswerOnNextQuestionEnabled((bool) $metadata["entry"]);
3052  break;
3053  case "instant_feedback_answer_fixation":
3054  $question_behaviour_settings = $question_behaviour_settings->withLockAnswerOnInstantFeedbackEnabled((bool) $metadata["entry"]);
3055  break;
3056  case "show_cancel":
3057  case "suspend_test_allowed":
3058  $participant_functionality_settings = $participant_functionality_settings->withSuspendTestAllowed((bool) $metadata["entry"]);
3059  break;
3060  case "sequence_settings":
3061  $participant_functionality_settings = $participant_functionality_settings->withPostponedQuestionsMoveToEnd((bool) $metadata["entry"]);
3062  break;
3063  case "show_marker":
3064  $participant_functionality_settings = $participant_functionality_settings->withQuestionMarkingEnabled((bool) $metadata["entry"]);
3065  break;
3066  case "fixed_participants":
3067  $access_settings = $access_settings->withFixedParticipants((bool) $metadata["entry"]);
3068  break;
3069  case "score_reporting":
3070  if ($metadata['entry'] !== null) {
3071  $result_summary_settings = $result_summary_settings->withScoreReporting(
3072  ScoreReportingTypes::tryFrom((int) $metadata['entry']) ?? ScoreReportingTypes::SCORE_REPORTING_DISABLED
3073  );
3074  }
3075  break;
3076  case "shuffle_questions":
3077  $question_behaviour_settings = $question_behaviour_settings->withShuffleQuestions((bool) $metadata["entry"]);
3078  break;
3079  case "count_system":
3080  $scoring_settings = $scoring_settings->withCountSystem((int) $metadata["entry"]);
3081  break;
3082  case "mailnotification":
3083  $finishing_settings = $finishing_settings->withMailNotificationContentType((int) $metadata["entry"]);
3084  break;
3085  case "mailnottype":
3086  $finishing_settings = $finishing_settings->withAlwaysSendMailNotification((bool) $metadata["entry"]);
3087  break;
3088  case "exportsettings":
3089  $result_details_settings = $result_details_settings->withExportSettings((int) $metadata["entry"]);
3090  break;
3091  case "score_cutting":
3092  $scoring_settings = $scoring_settings->withScoreCutting((int) $metadata["entry"]);
3093  break;
3094  case "password":
3095  $access_settings = $access_settings->withPasswordEnabled(
3096  $metadata["entry"] !== null && $metadata["entry"] !== ''
3097  )->withPassword($metadata["entry"]);
3098  break;
3099  case 'ip_range_from':
3100  if ($metadata['entry'] !== '') {
3101  $access_settings = $access_settings->withIpRangeFrom($metadata['entry']);
3102  }
3103  break;
3104  case 'ip_range_to':
3105  if ($metadata['entry'] !== '') {
3106  $access_settings = $access_settings->withIpRangeTo($metadata['entry']);
3107  }
3108  break;
3109  case "pass_scoring":
3110  $scoring_settings = $scoring_settings->withPassScoring((int) $metadata["entry"]);
3111  break;
3112  case 'pass_deletion_allowed':
3113  $result_summary_settings = $result_summary_settings->withPassDeletionAllowed((bool) $metadata["entry"]);
3114  break;
3115  case "usr_pass_overview_mode":
3116  $participant_functionality_settings = $participant_functionality_settings->withUsrPassOverviewMode((int) $metadata["entry"]);
3117  break;
3118  case "question_list":
3119  $participant_functionality_settings = $participant_functionality_settings->withQuestionListEnabled((bool) $metadata["entry"]);
3120  break;
3121 
3122  case "reporting_date":
3123  $reporting_date = $this->buildDateTimeImmutableFromPeriod($metadata['entry']);
3124  if ($reporting_date !== null) {
3125  $result_summary_settings = $result_summary_settings->withReportingDate($reporting_date);
3126  }
3127  break;
3128  case 'enable_processing_time':
3129  $test_behaviour_settings = $test_behaviour_settings->withProcessingTimeEnabled((bool) $metadata['entry']);
3130  break;
3131  case "processing_time":
3132  $test_behaviour_settings = $test_behaviour_settings->withProcessingTime($metadata['entry']);
3133  break;
3134  case "starting_time":
3135  $starting_time = $this->buildDateTimeImmutableFromPeriod($metadata['entry']);
3136  if ($starting_time !== null) {
3137  $access_settings = $access_settings->withStartTime($starting_time)
3138  ->withStartTimeEnabled(true);
3139  }
3140  break;
3141  case "ending_time":
3142  $ending_time = $this->buildDateTimeImmutableFromPeriod($metadata['entry']);
3143  if ($ending_time !== null) {
3144  $access_settings = $access_settings->withEndTime($ending_time)
3145  ->withStartTimeEnabled(true);
3146  }
3147  break;
3148  case "enable_examview":
3149  $finishing_settings = $finishing_settings->withShowAnswerOverview((bool) $metadata["entry"]);
3150  break;
3151  case 'redirection_mode':
3152  $finishing_settings = $finishing_settings->withRedirectionMode((int) $metadata['entry']);
3153  break;
3154  case 'redirection_url':
3155  $finishing_settings = $finishing_settings->withRedirectionUrl($metadata['entry']);
3156  break;
3157  case 'examid_in_test_pass':
3158  $test_behaviour_settings = $test_behaviour_settings->withExamIdInTestAttemptEnabled((bool) $metadata['entry']);
3159  break;
3160  case 'examid_in_test_res':
3161  $result_details_settings = $result_details_settings->withShowExamIdInTestResults((bool) $metadata["entry"]);
3162  break;
3163  case 'skill_service':
3164  $additional_settings = $additional_settings->withSkillsServiceEnabled((bool) $metadata['entry']);
3165  break;
3166  case 'show_grading_status':
3167  $result_summary_settings = $result_summary_settings->withShowGradingStatusEnabled((bool) $metadata["entry"]);
3168  break;
3169  case 'show_grading_mark':
3170  $result_summary_settings = $result_summary_settings->withShowGradingMarkEnabled((bool) $metadata["entry"]);
3171  break;
3172  case 'activation_limited':
3173  $this->setActivationLimited((bool) $metadata['entry']);
3174  break;
3175  case 'activation_start_time':
3176  $this->setActivationStartingTime($metadata['entry'] !== 'null' ? (int) $metadata['entry'] : null);
3177  break;
3178  case 'activation_end_time':
3179  $this->setActivationEndingTime($metadata['entry'] !== 'null' ? (int) $metadata['entry'] : null);
3180  break;
3181  case 'activation_visibility':
3182  $this->setActivationVisibility($metadata['entry']);
3183  break;
3184  case 'autosave':
3185  $question_behaviour_settings = $question_behaviour_settings->withAutosaveEnabled((bool) $metadata['entry']);
3186  break;
3187  case 'autosave_ival':
3188  $question_behaviour_settings = $question_behaviour_settings->withAutosaveInterval((int) $metadata['entry']);
3189  break;
3190  case 'offer_question_hints':
3191  $question_behaviour_settings = $question_behaviour_settings->withQuestionHintsEnabled((bool) $metadata['entry']);
3192  break;
3193  case 'show_summary':
3194  $participant_functionality_settings = $participant_functionality_settings->withQuestionListEnabled(($metadata['entry'] & 1) > 0)
3195  ->withUsrPassOverviewMode((int) $metadata['entry']);
3196 
3197  // no break
3198  case 'hide_info_tab':
3199  $additional_settings = $additional_settings->withHideInfoTab($metadata['entry'] === '1');
3200  }
3201  if (preg_match("/mark_step_\d+/", $metadata["label"])) {
3202  $xmlmark = $metadata["entry"];
3203  preg_match("/<short>(.*?)<\/short>/", $xmlmark, $matches);
3204  $mark_short = $matches[1];
3205  preg_match("/<official>(.*?)<\/official>/", $xmlmark, $matches);
3206  $mark_official = $matches[1];
3207  preg_match("/<percentage>(.*?)<\/percentage>/", $xmlmark, $matches);
3208  $mark_percentage = (float) $matches[1];
3209  preg_match("/<passed>(.*?)<\/passed>/", $xmlmark, $matches);
3210  $mark_passed = (bool) $matches[1];
3211  $mark_steps[] = new Mark($mark_short, $mark_official, $mark_percentage, $mark_passed);
3212  }
3213  }
3214  $this->mark_schema = $this->getMarkSchema()->withMarkSteps($mark_steps);
3215  $this->saveToDb();
3216  $this->getObjectProperties()->storePropertyTitleAndDescription(
3217  $this->getObjectProperties()->getPropertyTitleAndDescription()
3218  ->withTitle($assessment->getTitle())
3219  ->withDescription($assessment->getComment())
3220  );
3221  $this->addToNewsOnOnline(false, $this->getObjectProperties()->getPropertyIsOnline()->getIsOnline());
3222  $main_settings = $main_settings
3223  ->withGeneralSettings($general_settings)
3224  ->withIntroductionSettings($introduction_settings)
3225  ->withAccessSettings($access_settings)
3226  ->withParticipantFunctionalitySettings($participant_functionality_settings)
3227  ->withTestBehaviourSettings($test_behaviour_settings)
3228  ->withQuestionBehaviourSettings($question_behaviour_settings)
3229  ->withFinishingSettings($finishing_settings)
3230  ->withAdditionalSettings($additional_settings);
3231  $this->getMainSettingsRepository()->store($main_settings);
3232  $this->main_settings = $main_settings;
3233 
3234  $score_settings = $score_settings
3235  ->withGamificationSettings($gamification_settings)
3236  ->withScoringSettings($scoring_settings)
3237  ->withResultDetailsSettings($result_details_settings)
3238  ->withResultSummarySettings($result_summary_settings);
3239  $this->getScoreSettingsRepository()->store($score_settings);
3240  $this->score_settings = $score_settings;
3241  $this->loadFromDb();
3242  }
3243 
3245  SettingsIntroduction $settings,
3246  array $material,
3247  string $importdir,
3248  array $mappings
3250  $text = $material['text'];
3251  $mobs = $material['mobs'];
3252  if (str_starts_with($text, '<PageObject>')) {
3254  $text,
3255  $mappings['components/ILIAS/MediaObjects']['mob'] ?? []
3256  );
3258  $text,
3259  $mappings['components/ILIAS/File']['file'] ?? []
3260  );
3261  $page_object = new ilTestPage();
3262  $page_object->setParentId($this->getId());
3263  $page_object->setXMLContent($text);
3264  $new_page_id = $page_object->createPageWithNextId();
3265  return $settings->withIntroductionPageId($new_page_id);
3266  }
3267 
3268  $text = $this->retrieveMobsFromLegacyImports($text, $mobs, $importdir);
3269 
3270  return new SettingsIntroduction(
3271  $settings->getTestId(),
3272  $text !== '',
3273  $text
3274  );
3275  }
3276 
3278  SettingsFinishing $settings,
3279  array $material,
3280  string $importdir,
3281  array $mappings
3282  ): SettingsFinishing {
3283  $file_to_import = ilSession::get('path_to_import_file');
3284  $text = $material['text'];
3285  $mobs = $material['mobs'];
3286  if (str_starts_with($text, '<PageObject>')) {
3288  $text,
3289  $mappings['components/ILIAS/MediaObjects']['mob'] ?? []
3290  );
3292  $text,
3293  $mappings['components/ILIAS/File']['file'] ?? []
3294  );
3295  $page_object = new ilTestPage();
3296  $page_object->setParentId($this->getId());
3297  $page_object->setXMLContent($text);
3298  $new_page_id = $page_object->createPageWithNextId();
3299  return $settings->withConcludingRemarksPageId($new_page_id);
3300  }
3301 
3302  $text = $this->retrieveMobsFromLegacyImports($text, $mobs, $importdir);
3303 
3304  return new SettingsFinishing(
3305  $settings->getTestId(),
3306  $settings->getShowAnswerOverview(),
3307  strlen($text) > 0,
3308  $text,
3309  null,
3310  $settings->getRedirectionMode(),
3311  $settings->getRedirectionUrl(),
3312  $settings->getMailNotificationContentType(),
3313  $settings->getAlwaysSendMailNotification()
3314  );
3315  }
3316 
3317  private function replaceMobsInPageImports(string $text, array $mappings): string
3318  {
3319  preg_match_all('/il_(\d+)_mob_(\d+)/', $text, $matches);
3320  foreach ($matches[0] as $index => $match) {
3321  if (empty($mappings[$matches[2][$index]])) {
3322  continue;
3323  }
3324  $text = str_replace($match, "il__mob_{$mappings[$matches[2][$index]]}", $text);
3325  ilObjMediaObject::_saveUsage((int) $mappings[$matches[2][$index]], 'tst', $this->getId());
3326  }
3327  return $text;
3328  }
3329 
3330  private function replaceFilesInPageImports(string $text, array $mappings): string
3331  {
3332  preg_match_all('/il_(\d+)_file_(\d+)/', $text, $matches);
3333  foreach ($matches[0] as $index => $match) {
3334  if (empty($mappings[$matches[2][$index]])) {
3335  continue;
3336  }
3337  $text = str_replace($match, "il__file_{$mappings[$matches[2][$index]]}", $text);
3338  }
3339  return $text;
3340  }
3341 
3342  private function retrieveMobsFromLegacyImports(string $text, array $mobs, string $importdir): string
3343  {
3344  foreach ($mobs as $mob) {
3345  $importfile = $importdir . DIRECTORY_SEPARATOR . $mob['uri'];
3346  if (file_exists($importfile)) {
3347  $media_object = ilObjMediaObject::_saveTempFileAsMediaObject(basename($importfile), $importfile, false);
3348  ilObjMediaObject::_saveUsage($media_object->getId(), 'tst:html', $this->getId());
3350  str_replace(
3351  'src="' . $mob['mob'] . '"',
3352  'src="' . 'il_' . IL_INST_ID . '_mob_' . $media_object->getId() . '"',
3353  $text
3354  ),
3355  1
3356  );
3357  }
3358  }
3359  return $text;
3360  }
3361 
3367  public function toXML(): string
3368  {
3369  $main_settings = $this->getMainSettings();
3370  $a_xml_writer = new ilXmlWriter();
3371  // set xml header
3372  $a_xml_writer->xmlHeader();
3373  $a_xml_writer->xmlSetDtdDef("<!DOCTYPE questestinterop SYSTEM \"ims_qtiasiv1p2p1.dtd\">");
3374  $a_xml_writer->xmlStartTag("questestinterop");
3375 
3376  $attrs = [
3377  "ident" => "il_" . IL_INST_ID . "_tst_" . $this->getTestId(),
3378  "title" => $this->getTitle()
3379  ];
3380  $a_xml_writer->xmlStartTag("assessment", $attrs);
3381  $a_xml_writer->xmlElement("qticomment", null, $this->getDescription());
3382 
3383  if ($main_settings->getTestBehaviourSettings()->getProcessingTimeEnabled()) {
3384  $a_xml_writer->xmlElement(
3385  "duration",
3386  null,
3387  $this->getProcessingTimeForXML()
3388  );
3389  }
3390 
3391  $a_xml_writer->xmlStartTag("qtimetadata");
3392  $a_xml_writer->xmlStartTag("qtimetadatafield");
3393  $a_xml_writer->xmlElement("fieldlabel", null, "ILIAS_VERSION");
3394  $a_xml_writer->xmlElement("fieldentry", null, ILIAS_VERSION);
3395  $a_xml_writer->xmlEndTag("qtimetadatafield");
3396 
3397  $a_xml_writer->xmlStartTag("qtimetadatafield");
3398  $a_xml_writer->xmlElement("fieldlabel", null, "anonymity");
3399  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $main_settings->getGeneralSettings()->getAnonymity()));
3400  $a_xml_writer->xmlEndTag("qtimetadatafield");
3401 
3402  $a_xml_writer->xmlStartTag("qtimetadatafield");
3403  $a_xml_writer->xmlElement("fieldlabel", null, "question_set_type");
3404  $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getGeneralSettings()->getQuestionSetType());
3405  $a_xml_writer->xmlEndTag("qtimetadatafield");
3406 
3407  $a_xml_writer->xmlStartTag("qtimetadatafield");
3408  $a_xml_writer->xmlElement("fieldlabel", null, "sequence_settings");
3409  $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getParticipantFunctionalitySettings()->getPostponedQuestionsMoveToEnd());
3410  $a_xml_writer->xmlEndTag("qtimetadatafield");
3411 
3412  $a_xml_writer->xmlStartTag("qtimetadatafield");
3413  $a_xml_writer->xmlElement("fieldlabel", null, "author");
3414  $a_xml_writer->xmlElement("fieldentry", null, $this->getAuthor());
3415  $a_xml_writer->xmlEndTag("qtimetadatafield");
3416 
3417  $a_xml_writer->xmlStartTag("qtimetadatafield");
3418  $a_xml_writer->xmlElement("fieldlabel", null, "reset_processing_time");
3419  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getTestBehaviourSettings()->getResetProcessingTime());
3420  $a_xml_writer->xmlEndTag("qtimetadatafield");
3421 
3422  $a_xml_writer->xmlStartTag("qtimetadatafield");
3423  $a_xml_writer->xmlElement("fieldlabel", null, "count_system");
3424  $a_xml_writer->xmlElement("fieldentry", null, $this->getCountSystem());
3425  $a_xml_writer->xmlEndTag("qtimetadatafield");
3426 
3427  $a_xml_writer->xmlStartTag("qtimetadatafield");
3428  $a_xml_writer->xmlElement("fieldlabel", null, "score_cutting");
3429  $a_xml_writer->xmlElement("fieldentry", null, $this->getScoreCutting());
3430  $a_xml_writer->xmlEndTag("qtimetadatafield");
3431 
3432  $a_xml_writer->xmlStartTag("qtimetadatafield");
3433  $a_xml_writer->xmlElement("fieldlabel", null, "password");
3434  $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getAccessSettings()->getPassword() ?? '');
3435  $a_xml_writer->xmlEndTag("qtimetadatafield");
3436 
3437  $a_xml_writer->xmlStartTag("qtimetadatafield");
3438  $a_xml_writer->xmlElement("fieldlabel", null, "ip_range_from");
3439  $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getAccessSettings()->getIpRangeFrom());
3440  $a_xml_writer->xmlEndTag("qtimetadatafield");
3441 
3442  $a_xml_writer->xmlStartTag("qtimetadatafield");
3443  $a_xml_writer->xmlElement("fieldlabel", null, "ip_range_to");
3444  $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getAccessSettings()->getIpRangeTo());
3445  $a_xml_writer->xmlEndTag("qtimetadatafield");
3446 
3447  $a_xml_writer->xmlStartTag("qtimetadatafield");
3448  $a_xml_writer->xmlElement("fieldlabel", null, "pass_scoring");
3449  $a_xml_writer->xmlElement("fieldentry", null, $this->getPassScoring());
3450  $a_xml_writer->xmlEndTag("qtimetadatafield");
3451 
3452  $a_xml_writer->xmlStartTag('qtimetadatafield');
3453  $a_xml_writer->xmlElement('fieldlabel', null, 'pass_deletion_allowed');
3454  $a_xml_writer->xmlElement('fieldentry', null, (int) $this->isPassDeletionAllowed());
3455  $a_xml_writer->xmlEndTag('qtimetadatafield');
3456 
3457  if ($this->getScoreSettings()->getResultSummarySettings()->getReportingDate() !== null) {
3458  $a_xml_writer->xmlStartTag("qtimetadatafield");
3459  $a_xml_writer->xmlElement("fieldlabel", null, "reporting_date");
3460  $a_xml_writer->xmlElement(
3461  "fieldentry",
3462  null,
3464  $this->getScoreSettings()->getResultSummarySettings()->getReportingDate(),
3465  ),
3466  );
3467  $a_xml_writer->xmlEndTag("qtimetadatafield");
3468  }
3469 
3470  $a_xml_writer->xmlStartTag("qtimetadatafield");
3471  $a_xml_writer->xmlElement("fieldlabel", null, "nr_of_tries");
3472  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $main_settings->getTestBehaviourSettings()->getNumberOfTries()));
3473  $a_xml_writer->xmlEndTag("qtimetadatafield");
3474 
3475  $a_xml_writer->xmlStartTag('qtimetadatafield');
3476  $a_xml_writer->xmlElement('fieldlabel', null, 'block_after_passed');
3477  $a_xml_writer->xmlElement('fieldentry', null, (int) $main_settings->getTestBehaviourSettings()->getBlockAfterPassedEnabled());
3478  $a_xml_writer->xmlEndTag('qtimetadatafield');
3479 
3480  $a_xml_writer->xmlStartTag("qtimetadatafield");
3481  $a_xml_writer->xmlElement("fieldlabel", null, "pass_waiting");
3482  $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getTestBehaviourSettings()->getPassWaiting());
3483  $a_xml_writer->xmlEndTag("qtimetadatafield");
3484 
3485  $a_xml_writer->xmlStartTag("qtimetadatafield");
3486  $a_xml_writer->xmlElement("fieldlabel", null, "kiosk");
3487  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $main_settings->getTestBehaviourSettings()->getKioskMode()));
3488  $a_xml_writer->xmlEndTag("qtimetadatafield");
3489 
3490  $a_xml_writer->xmlStartTag('qtimetadatafield');
3491  $a_xml_writer->xmlElement("fieldlabel", null, "redirection_mode");
3492  $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getFinishingSettings()->getRedirectionMode());
3493  $a_xml_writer->xmlEndTag("qtimetadatafield");
3494 
3495  $a_xml_writer->xmlStartTag('qtimetadatafield');
3496  $a_xml_writer->xmlElement("fieldlabel", null, "redirection_url");
3497  $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getFinishingSettings()->getRedirectionUrl());
3498  $a_xml_writer->xmlEndTag("qtimetadatafield");
3499 
3500  $a_xml_writer->xmlStartTag("qtimetadatafield");
3501  $a_xml_writer->xmlElement("fieldlabel", null, "use_previous_answers");
3502  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getParticipantFunctionalitySettings()->getUsePreviousAnswerAllowed());
3503  $a_xml_writer->xmlEndTag("qtimetadatafield");
3504 
3505  $a_xml_writer->xmlStartTag('qtimetadatafield');
3506  $a_xml_writer->xmlElement('fieldlabel', null, 'question_list_enabled');
3507  $a_xml_writer->xmlElement('fieldentry', null, (int) $main_settings->getParticipantFunctionalitySettings()->getQuestionListEnabled());
3508  $a_xml_writer->xmlEndTag('qtimetadatafield');
3509 
3510  $a_xml_writer->xmlStartTag("qtimetadatafield");
3511  $a_xml_writer->xmlElement("fieldlabel", null, "title_output");
3512  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $main_settings->getQuestionBehaviourSettings()->getQuestionTitleOutputMode()));
3513  $a_xml_writer->xmlEndTag("qtimetadatafield");
3514 
3515  $a_xml_writer->xmlStartTag("qtimetadatafield");
3516  $a_xml_writer->xmlElement("fieldlabel", null, "results_presentation");
3517  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $this->getScoreSettings()->getResultDetailsSettings()->getResultsPresentation()));
3518  $a_xml_writer->xmlEndTag("qtimetadatafield");
3519 
3520  $a_xml_writer->xmlStartTag("qtimetadatafield");
3521  $a_xml_writer->xmlElement("fieldlabel", null, "examid_in_test_pass");
3522  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $main_settings->getTestBehaviourSettings()->getExamIdInTestAttemptEnabled()));
3523  $a_xml_writer->xmlEndTag("qtimetadatafield");
3524 
3525  $a_xml_writer->xmlStartTag("qtimetadatafield");
3526  $a_xml_writer->xmlElement("fieldlabel", null, "examid_in_test_res");
3527  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $this->getScoreSettings()->getResultDetailsSettings()->getShowExamIdInTestResults()));
3528  $a_xml_writer->xmlEndTag("qtimetadatafield");
3529 
3530  $a_xml_writer->xmlStartTag("qtimetadatafield");
3531  $a_xml_writer->xmlElement("fieldlabel", null, "usr_pass_overview_mode");
3532  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $main_settings->getParticipantFunctionalitySettings()->getUsrPassOverviewMode()));
3533  $a_xml_writer->xmlEndTag("qtimetadatafield");
3534 
3535  $a_xml_writer->xmlStartTag("qtimetadatafield");
3536  $a_xml_writer->xmlElement("fieldlabel", null, "score_reporting");
3537  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $this->getScoreSettings()->getResultSummarySettings()->getScoreReporting()->value));
3538  $a_xml_writer->xmlEndTag("qtimetadatafield");
3539 
3540  $a_xml_writer->xmlStartTag("qtimetadatafield");
3541  $a_xml_writer->xmlElement("fieldlabel", null, "show_solution_list_comparison");
3542  $a_xml_writer->xmlElement("fieldentry", null, (int) $this->score_settings->getResultDetailsSettings()->getShowSolutionListComparison());
3543  $a_xml_writer->xmlEndTag("qtimetadatafield");
3544 
3545  $a_xml_writer->xmlStartTag("qtimetadatafield");
3546  $a_xml_writer->xmlElement("fieldlabel", null, "instant_verification");
3547  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackSolutionEnabled()));
3548  $a_xml_writer->xmlEndTag("qtimetadatafield");
3549 
3550  $a_xml_writer->xmlStartTag("qtimetadatafield");
3551  $a_xml_writer->xmlElement("fieldlabel", null, "answer_feedback");
3552  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackGenericEnabled()));
3553  $a_xml_writer->xmlEndTag("qtimetadatafield");
3554 
3555  $a_xml_writer->xmlStartTag("qtimetadatafield");
3556  $a_xml_writer->xmlElement("fieldlabel", null, "instant_feedback_specific");
3557  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackSpecificEnabled()));
3558  $a_xml_writer->xmlEndTag("qtimetadatafield");
3559 
3560  $a_xml_writer->xmlStartTag("qtimetadatafield");
3561  $a_xml_writer->xmlElement("fieldlabel", null, "answer_feedback_points");
3562  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackPointsEnabled()));
3563  $a_xml_writer->xmlEndTag("qtimetadatafield");
3564 
3565  $a_xml_writer->xmlStartTag("qtimetadatafield");
3566  $a_xml_writer->xmlElement("fieldlabel", null, "follow_qst_answer_fixation");
3567  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getQuestionBehaviourSettings()->getLockAnswerOnNextQuestionEnabled());
3568  $a_xml_writer->xmlEndTag("qtimetadatafield");
3569 
3570  $a_xml_writer->xmlStartTag("qtimetadatafield");
3571  $a_xml_writer->xmlElement("fieldlabel", null, "instant_feedback_answer_fixation");
3572  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getQuestionBehaviourSettings()->getLockAnswerOnInstantFeedbackEnabled());
3573  $a_xml_writer->xmlEndTag("qtimetadatafield");
3574 
3575  $a_xml_writer->xmlStartTag("qtimetadatafield");
3576  $a_xml_writer->xmlElement("fieldlabel", null, "force_instant_feedback");
3577  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getQuestionBehaviourSettings()->getForceInstantFeedbackOnNextQuestion());
3578  $a_xml_writer->xmlEndTag("qtimetadatafield");
3579 
3580  $highscore_metadata = [
3581  'highscore_enabled' => ['value' => $this->getHighscoreEnabled()],
3582  'highscore_anon' => ['value' => $this->getHighscoreAnon()],
3583  'highscore_achieved_ts' => ['value' => $this->getHighscoreAchievedTS()],
3584  'highscore_score' => ['value' => $this->getHighscoreScore()],
3585  'highscore_percentage' => ['value' => $this->getHighscorePercentage()],
3586  'highscore_hints' => ['value' => $this->getHighscoreHints()],
3587  'highscore_wtime' => ['value' => $this->getHighscoreWTime()],
3588  'highscore_own_table' => ['value' => $this->getHighscoreOwnTable()],
3589  'highscore_top_table' => ['value' => $this->getHighscoreTopTable()],
3590  'highscore_top_num' => ['value' => $this->getHighscoreTopNum()],
3591  ];
3592  foreach ($highscore_metadata as $label => $data) {
3593  $a_xml_writer->xmlStartTag("qtimetadatafield");
3594  $a_xml_writer->xmlElement("fieldlabel", null, $label);
3595  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $data['value']));
3596  $a_xml_writer->xmlEndTag("qtimetadatafield");
3597  }
3598 
3599  $a_xml_writer->xmlStartTag("qtimetadatafield");
3600  $a_xml_writer->xmlElement("fieldlabel", null, "suspend_test_allowed");
3601  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getParticipantFunctionalitySettings()->getSuspendTestAllowed()));
3602  $a_xml_writer->xmlEndTag("qtimetadatafield");
3603 
3604  $a_xml_writer->xmlStartTag("qtimetadatafield");
3605  $a_xml_writer->xmlElement("fieldlabel", null, "show_marker");
3606  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getParticipantFunctionalitySettings()->getQuestionMarkingEnabled()));
3607  $a_xml_writer->xmlEndTag("qtimetadatafield");
3608 
3609  $a_xml_writer->xmlStartTag("qtimetadatafield");
3610  $a_xml_writer->xmlElement("fieldlabel", null, "fixed_participants");
3611  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getAccessSettings()->getFixedParticipants()));
3612  $a_xml_writer->xmlEndTag("qtimetadatafield");
3613 
3614  $a_xml_writer->xmlStartTag("qtimetadatafield");
3615  $a_xml_writer->xmlElement("fieldlabel", null, "show_introduction");
3616  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getIntroductionSettings()->getIntroductionEnabled()));
3617  $a_xml_writer->xmlEndTag("qtimetadatafield");
3618 
3619  $a_xml_writer->xmlStartTag("qtimetadatafield");
3620  $a_xml_writer->xmlElement("fieldlabel", null, 'exam_conditions');
3621  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getIntroductionSettings()->getExamConditionsCheckboxEnabled()));
3622  $a_xml_writer->xmlEndTag("qtimetadatafield");
3623 
3624  $a_xml_writer->xmlStartTag("qtimetadatafield");
3625  $a_xml_writer->xmlElement("fieldlabel", null, "show_concluding_remarks");
3626  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getFinishingSettings()->getConcludingRemarksEnabled()));
3627  $a_xml_writer->xmlEndTag("qtimetadatafield");
3628 
3629  $a_xml_writer->xmlStartTag("qtimetadatafield");
3630  $a_xml_writer->xmlElement("fieldlabel", null, "mailnotification");
3631  $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getFinishingSettings()->getMailNotificationContentType());
3632  $a_xml_writer->xmlEndTag("qtimetadatafield");
3633 
3634  $a_xml_writer->xmlStartTag("qtimetadatafield");
3635  $a_xml_writer->xmlElement("fieldlabel", null, "mailnottype");
3636  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getFinishingSettings()->getAlwaysSendMailNotification());
3637  $a_xml_writer->xmlEndTag("qtimetadatafield");
3638 
3639  $a_xml_writer->xmlStartTag("qtimetadatafield");
3640  $a_xml_writer->xmlElement("fieldlabel", null, "exportsettings");
3641  $a_xml_writer->xmlElement("fieldentry", null, $this->getExportSettings());
3642  $a_xml_writer->xmlEndTag("qtimetadatafield");
3643 
3644  $a_xml_writer->xmlStartTag("qtimetadatafield");
3645  $a_xml_writer->xmlElement("fieldlabel", null, "shuffle_questions");
3646  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getQuestionBehaviourSettings()->getShuffleQuestions()));
3647  $a_xml_writer->xmlEndTag("qtimetadatafield");
3648 
3649  $a_xml_writer->xmlStartTag("qtimetadatafield");
3650  $a_xml_writer->xmlElement("fieldlabel", null, "processing_time");
3651  $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getTestBehaviourSettings()->getProcessingTime());
3652  $a_xml_writer->xmlEndTag("qtimetadatafield");
3653 
3654  $a_xml_writer->xmlStartTag("qtimetadatafield");
3655  $a_xml_writer->xmlElement("fieldlabel", null, "enable_examview");
3656  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getFinishingSettings()->getShowAnswerOverview());
3657  $a_xml_writer->xmlEndTag("qtimetadatafield");
3658 
3659  $a_xml_writer->xmlStartTag("qtimetadatafield");
3660  $a_xml_writer->xmlElement("fieldlabel", null, "skill_service");
3661  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getAdditionalSettings()->getSkillsServiceEnabled());
3662  $a_xml_writer->xmlEndTag("qtimetadatafield");
3663 
3664  if ($this->getInstantFeedbackSolution() == 1) {
3665  $attrs = [
3666  "solutionswitch" => "Yes"
3667  ];
3668  } else {
3669  $attrs = null;
3670  }
3671  $a_xml_writer->xmlElement("assessmentcontrol", $attrs, null);
3672 
3673  $a_xml_writer->xmlStartTag("qtimetadatafield");
3674  $a_xml_writer->xmlElement("fieldlabel", null, "show_grading_status");
3675  $a_xml_writer->xmlElement("fieldentry", null, (int) $this->isShowGradingStatusEnabled());
3676  $a_xml_writer->xmlEndTag("qtimetadatafield");
3677 
3678  $a_xml_writer->xmlStartTag("qtimetadatafield");
3679  $a_xml_writer->xmlElement("fieldlabel", null, "show_grading_mark");
3680  $a_xml_writer->xmlElement("fieldentry", null, (int) $this->isShowGradingMarkEnabled());
3681  $a_xml_writer->xmlEndTag("qtimetadatafield");
3682 
3683  $a_xml_writer->xmlStartTag('qtimetadatafield');
3684  $a_xml_writer->xmlElement('fieldlabel', null, 'hide_info_tab');
3685  $a_xml_writer->xmlElement('fieldentry', null, (int) $this->getMainSettings()->getAdditionalSettings()->getHideInfoTab());
3686  $a_xml_writer->xmlEndTag("qtimetadatafield");
3687 
3688  if ($this->getStartingTime() > 0) {
3689  $a_xml_writer->xmlStartTag("qtimetadatafield");
3690  $a_xml_writer->xmlElement("fieldlabel", null, "starting_time");
3691  $a_xml_writer->xmlElement(
3692  "fieldentry",
3693  null,
3695  (new DateTimeImmutable())->setTimestamp($this->getStartingTime()),
3696  ),
3697  );
3698  $a_xml_writer->xmlEndTag("qtimetadatafield");
3699  }
3700 
3701  if ($this->getEndingTime() > 0) {
3702  $a_xml_writer->xmlStartTag("qtimetadatafield");
3703  $a_xml_writer->xmlElement("fieldlabel", null, "ending_time");
3704  $a_xml_writer->xmlElement(
3705  "fieldentry",
3706  null,
3708  (new DateTimeImmutable())->setTimestamp($this->getEndingTime()),
3709  ),
3710  );
3711  $a_xml_writer->xmlEndTag("qtimetadatafield");
3712  }
3713 
3714  $a_xml_writer->xmlStartTag("qtimetadatafield");
3715  $a_xml_writer->xmlElement("fieldlabel", null, "activation_limited");
3716  $a_xml_writer->xmlElement("fieldentry", null, (int) $this->isActivationLimited());
3717  $a_xml_writer->xmlEndTag("qtimetadatafield");
3718 
3719  $a_xml_writer->xmlStartTag("qtimetadatafield");
3720  $a_xml_writer->xmlElement("fieldlabel", null, "activation_start_time");
3721  $a_xml_writer->xmlElement("fieldentry", null, (int) $this->getActivationStartingTime());
3722  $a_xml_writer->xmlEndTag("qtimetadatafield");
3723 
3724  $a_xml_writer->xmlStartTag("qtimetadatafield");
3725  $a_xml_writer->xmlElement("fieldlabel", null, "activation_end_time");
3726  $a_xml_writer->xmlElement("fieldentry", null, (int) $this->getActivationEndingTime());
3727  $a_xml_writer->xmlEndTag("qtimetadatafield");
3728 
3729  $a_xml_writer->xmlStartTag("qtimetadatafield");
3730  $a_xml_writer->xmlElement("fieldlabel", null, "activation_visibility");
3731  $a_xml_writer->xmlElement("fieldentry", null, (int) $this->getActivationVisibility());
3732  $a_xml_writer->xmlEndTag("qtimetadatafield");
3733 
3734  $a_xml_writer->xmlStartTag("qtimetadatafield");
3735  $a_xml_writer->xmlElement("fieldlabel", null, "autosave");
3736  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getQuestionBehaviourSettings()->getAutosaveEnabled());
3737  $a_xml_writer->xmlEndTag("qtimetadatafield");
3738 
3739  $a_xml_writer->xmlStartTag("qtimetadatafield");
3740  $a_xml_writer->xmlElement("fieldlabel", null, "autosave_ival");
3741  $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getAutosaveInterval());
3742  $a_xml_writer->xmlEndTag("qtimetadatafield");
3743 
3744  $a_xml_writer->xmlStartTag("qtimetadatafield");
3745  $a_xml_writer->xmlElement("fieldlabel", null, "offer_question_hints");
3746  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getQuestionBehaviourSettings()->getQuestionHintsEnabled());
3747  $a_xml_writer->xmlEndTag("qtimetadatafield");
3748 
3749  $a_xml_writer->xmlStartTag("qtimetadatafield");
3750  $a_xml_writer->xmlElement("fieldlabel", null, "instant_feedback_specific");
3751  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackSpecificEnabled());
3752  $a_xml_writer->xmlEndTag("qtimetadatafield");
3753 
3754  $a_xml_writer->xmlStartTag("qtimetadatafield");
3755  $a_xml_writer->xmlElement("fieldlabel", null, "instant_feedback_answer_fixation");
3756  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getQuestionBehaviourSettings()->getLockAnswerOnInstantFeedbackEnabled());
3757  $a_xml_writer->xmlEndTag("qtimetadatafield");
3758 
3759  $a_xml_writer->xmlStartTag("qtimetadatafield");
3760  $a_xml_writer->xmlElement("fieldlabel", null, "enable_processing_time");
3761  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getTestBehaviourSettings()->getProcessingTimeEnabled());
3762  $a_xml_writer->xmlEndTag("qtimetadatafield");
3763 
3764  foreach ($this->getMarkSchema()->getMarkSteps() as $index => $mark) {
3765  $a_xml_writer->xmlStartTag("qtimetadatafield");
3766  $a_xml_writer->xmlElement("fieldlabel", null, "mark_step_$index");
3767  $a_xml_writer->xmlElement("fieldentry", null, sprintf(
3768  "<short>%s</short><official>%s</official><percentage>%.2f</percentage><passed>%d</passed>",
3769  $mark->getShortName(),
3770  $mark->getOfficialName(),
3771  $mark->getMinimumLevel(),
3772  $mark->getPassed()
3773  ));
3774  $a_xml_writer->xmlEndTag("qtimetadatafield");
3775  }
3776  $a_xml_writer->xmlEndTag("qtimetadata");
3777 
3778  $page_id = $main_settings->getIntroductionSettings()->getIntroductionPageId();
3779  $introduction = $page_id !== null
3780  ? (new ilTestPage($page_id))->getXMLContent()
3782 
3783  $a_xml_writer->xmlStartTag("objectives");
3784  $this->addQTIMaterial($a_xml_writer, $page_id, $introduction);
3785  $a_xml_writer->xmlEndTag("objectives");
3786 
3787  if ($this->getInstantFeedbackSolution() == 1) {
3788  $attrs = [
3789  "solutionswitch" => "Yes"
3790  ];
3791  } else {
3792  $attrs = null;
3793  }
3794  $a_xml_writer->xmlElement("assessmentcontrol", $attrs, null);
3795 
3796  if (strlen($this->getFinalStatement())) {
3797  $page_id = $main_settings->getFinishingSettings()->getConcludingRemarksPageId();
3798  $concluding_remarks = $page_id !== null
3799  ? (new ilTestPage($page_id))->getXMLContent()
3801  // add qti presentation_material
3802  $a_xml_writer->xmlStartTag("presentation_material");
3803  $a_xml_writer->xmlStartTag("flow_mat");
3804  $this->addQTIMaterial($a_xml_writer, $page_id, $concluding_remarks);
3805  $a_xml_writer->xmlEndTag("flow_mat");
3806  $a_xml_writer->xmlEndTag("presentation_material");
3807  }
3808 
3809  $attrs = [
3810  "ident" => "1"
3811  ];
3812  $a_xml_writer->xmlElement("section", $attrs, null);
3813  $a_xml_writer->xmlEndTag("assessment");
3814  $a_xml_writer->xmlEndTag("questestinterop");
3815 
3816  $xml = $a_xml_writer->xmlDumpMem(false);
3817  return $xml;
3818  }
3819 
3820  protected function buildIso8601PeriodForExportCompatibility(DateTimeImmutable $date_time): string
3821  {
3822  return $date_time->setTimezone(new DateTimeZone('UTC'))->format('\PY\Yn\Mj\D\TG\Hi\Ms\S');
3823  }
3824 
3825  protected function buildDateTimeImmutableFromPeriod(?string $period): ?DateTimeImmutable
3826  {
3827  if ($period === null) {
3828  return null;
3829  }
3830  if (preg_match("/P(\d+)Y(\d+)M(\d+)DT(\d+)H(\d+)M(\d+)S/", $period, $matches)) {
3831  return new DateTimeImmutable(
3832  sprintf(
3833  "%02d-%02d-%02d %02d:%02d:%02d",
3834  $matches[1],
3835  $matches[2],
3836  $matches[3],
3837  $matches[4],
3838  $matches[5],
3839  $matches[6]
3840  ),
3841  new \DateTimeZone('UTC')
3842  );
3843  }
3844  return null;
3845  }
3846 
3853  public function exportPagesXML(&$a_xml_writer, $a_inst, $a_target_dir, &$expLog): void
3854  {
3855  $this->mob_ids = [];
3856 
3857  // PageObjects
3858  $expLog->write(date("[y-m-d H:i:s] ") . "Start Export Page Objects");
3859  $this->bench->start("ContentObjectExport", "exportPageObjects");
3860  $this->exportXMLPageObjects($a_xml_writer, $a_inst, $expLog);
3861  $this->bench->stop("ContentObjectExport", "exportPageObjects");
3862  $expLog->write(date("[y-m-d H:i:s] ") . "Finished Export Page Objects");
3863 
3864  // MediaObjects
3865  $expLog->write(date("[y-m-d H:i:s] ") . "Start Export Media Objects");
3866  $this->bench->start("ContentObjectExport", "exportMediaObjects");
3867  $this->exportXMLMediaObjects($a_xml_writer, $a_inst, $a_target_dir, $expLog);
3868  $this->bench->stop("ContentObjectExport", "exportMediaObjects");
3869  $expLog->write(date("[y-m-d H:i:s] ") . "Finished Export Media Objects");
3870 
3871  // FileItems
3872  $expLog->write(date("[y-m-d H:i:s] ") . "Start Export File Items");
3873  $this->bench->start("ContentObjectExport", "exportFileItems");
3874  $this->exportFileItems($a_target_dir, $expLog);
3875  $this->bench->stop("ContentObjectExport", "exportFileItems");
3876  $expLog->write(date("[y-m-d H:i:s] ") . "Finished Export File Items");
3877  }
3878 
3884  public function modifyExportIdentifier($a_tag, $a_param, $a_value)
3885  {
3886  if ($a_tag == "Identifier" && $a_param == "Entry") {
3887  $a_value = ilUtil::insertInstIntoID($a_value);
3888  }
3889 
3890  return $a_value;
3891  }
3892 
3893 
3900  public function exportXMLPageObjects(&$a_xml_writer, $inst, &$expLog)
3901  {
3902  foreach ($this->questions as $question_id) {
3903  $this->bench->start("ContentObjectExport", "exportPageObject");
3904  $expLog->write(date("[y-m-d H:i:s] ") . "Page Object " . $question_id);
3905 
3906  $attrs = [];
3907  $a_xml_writer->xmlStartTag("PageObject", $attrs);
3908 
3909 
3910  // export xml to writer object
3911  $this->bench->start("ContentObjectExport", "exportPageObject_XML");
3912  $page_object = new ilAssQuestionPage($question_id);
3913  $page_object->buildDom();
3914  $page_object->insertInstIntoIDs((string) $inst);
3915  $mob_ids = $page_object->collectMediaObjects(false);
3916  $file_ids = ilPCFileList::collectFileItems($page_object, $page_object->getDomDoc());
3917  $xml = $page_object->getXMLFromDom(false, false, false, "", true);
3918  $xml = str_replace("&", "&amp;", $xml);
3919  $a_xml_writer->appendXML($xml);
3920  $page_object->freeDom();
3921  unset($page_object);
3922 
3923  $this->bench->stop("ContentObjectExport", "exportPageObject_XML");
3924 
3925  // collect media objects
3926  $this->bench->start("ContentObjectExport", "exportPageObject_CollectMedia");
3927  //$mob_ids = $page_obj->getMediaObjectIDs();
3928  foreach ($mob_ids as $mob_id) {
3929  $this->mob_ids[$mob_id] = $mob_id;
3930  }
3931  $this->bench->stop("ContentObjectExport", "exportPageObject_CollectMedia");
3932 
3933  // collect all file items
3934  $this->bench->start("ContentObjectExport", "exportPageObject_CollectFileItems");
3935  //$file_ids = $page_obj->getFileItemIds();
3936  foreach ($file_ids as $file_id) {
3937  $this->file_ids[$file_id] = $file_id;
3938  }
3939  $this->bench->stop("ContentObjectExport", "exportPageObject_CollectFileItems");
3940 
3941  $a_xml_writer->xmlEndTag("PageObject");
3942  //unset($page_obj);
3943 
3944  $this->bench->stop("ContentObjectExport", "exportPageObject");
3945  }
3946  }
3947 
3951  public function exportXMLMediaObjects(&$a_xml_writer, $a_inst, $a_target_dir, &$expLog)
3952  {
3953  foreach ($this->mob_ids as $mob_id) {
3954  $expLog->write(date("[y-m-d H:i:s] ") . "Media Object " . $mob_id);
3955  if (ilObjMediaObject::_exists((int) $mob_id)) {
3956  $target_dir = $a_target_dir . DIRECTORY_SEPARATOR . 'objects'
3957  . DIRECTORY_SEPARATOR . 'il_' . IL_INST_ID . '_mob_' . $mob_id;
3958  ilFileUtils::createDirectory($target_dir);
3959  $media_obj = new ilObjMediaObject((int) $mob_id);
3960  $media_obj->exportXML($a_xml_writer, (int) $a_inst);
3961  foreach ($media_obj->getMediaItems() as $item) {
3962  $stream = $item->getLocationStream();
3963  file_put_contents($target_dir . DIRECTORY_SEPARATOR . $item->getLocation(), $stream);
3964  $stream->close();
3965  }
3966  unset($media_obj);
3967  }
3968  }
3969  }
3970 
3975  public function exportFileItems($target_dir, &$expLog)
3976  {
3977  foreach ($this->file_ids as $file_id) {
3978  $expLog->write(date("[y-m-d H:i:s] ") . "File Item " . $file_id);
3979  $file_dir = $target_dir . '/objects/il_' . IL_INST_ID . '_file_' . $file_id;
3980  ilFileUtils::makeDir($file_dir);
3981  $file_obj = new ilObjFile((int) $file_id, false);
3982  $source_file = $file_obj->getFile($file_obj->getVersion());
3983  if (!is_file($source_file)) {
3984  $source_file = $file_obj->getFile();
3985  }
3986  if (is_file($source_file)) {
3987  copy($source_file, $file_dir . '/' . $file_obj->getFileName());
3988  }
3989  unset($file_obj);
3990  }
3991  }
3992 
3997  public function getImportMapping(): array
3998  {
3999  return [];
4000  }
4001 
4004  public function onMarkSchemaSaved(): void
4005  {
4006  $this->saveCompleteStatus($this->question_set_config_factory->getQuestionSetConfig());
4007 
4008  if ($this->participantDataExist()) {
4009  $this->recalculateScores(true);
4010  }
4011  }
4012 
4016  public function marksEditable(): bool
4017  {
4018  $total = $this->evalTotalPersons();
4019  $results_summary_settings = $this->getScoreSettings()->getResultSummarySettings();
4020  if ($total === 0
4021  || $results_summary_settings->getScoreReporting()->isReportingEnabled() === false) {
4022  return true;
4023  }
4024 
4025  if ($results_summary_settings->getScoreReporting() === ScoreReportingTypes::SCORE_REPORTING_DATE) {
4026  return $results_summary_settings->getReportingDate()
4027  >= new DateTimeImmutable('now', new DateTimeZone('UTC'));
4028  }
4029 
4030  return false;
4031  }
4032 
4042  public function saveAuthorToMetadata($author = "")
4043  {
4044  $path_to_lifecycle = $this->lo_metadata->paths()->custom()->withNextStep('lifeCycle')->get();
4045  $path_to_authors = $this->lo_metadata->paths()->authors();
4046 
4047  $reader = $this->lo_metadata->read($this->getId(), 0, $this->getType(), $path_to_lifecycle);
4048  if (!is_null($reader->allData($path_to_lifecycle)->current())) {
4049  return;
4050  }
4051 
4052  if ($author === '') {
4053  $author = $this->user->getFullname();
4054  }
4055  $this->lo_metadata->manipulate($this->getId(), 0, $this->getType())
4056  ->prepareCreateOrUpdate($path_to_authors, $author)
4057  ->execute();
4058  }
4059 
4063  protected function doCreateMetaData(): void
4064  {
4065  $this->saveAuthorToMetadata();
4066  }
4067 
4075  public function getAuthor(): string
4076  {
4077  $path_to_authors = $this->lo_metadata->paths()->authors();
4078  $author_data = $this->lo_metadata->read($this->getId(), 0, $this->getType(), $path_to_authors)
4079  ->allData($path_to_authors);
4080 
4081  return $this->lo_metadata->dataHelper()->makePresentableAsList(', ', ...$author_data);
4082  }
4083 
4091  public static function _lookupAuthor($obj_id): string
4092  {
4093  global $DIC;
4094 
4095  $lo_metadata = $DIC->learningObjectMetadata();
4096 
4097  $path_to_authors = $lo_metadata->paths()->authors();
4098  $author_data = $lo_metadata->read($obj_id, 0, "tst", $path_to_authors)
4099  ->allData($path_to_authors);
4100 
4101  return $lo_metadata->dataHelper()->makePresentableAsList(',', ...$author_data);
4102  }
4103 
4110  public static function _getAvailableTests($use_object_id = false): array
4111  {
4112  global $DIC;
4113  $ilUser = $DIC['ilUser'];
4114 
4115  $result_array = [];
4116  $tests = array_slice(
4117  array_reverse(
4118  ilUtil::_getObjectsByOperations("tst", "write", $ilUser->getId(), PHP_INT_MAX)
4119  ),
4120  0,
4121  10000
4122  );
4123 
4124  if (count($tests)) {
4125  $titles = ilObject::_prepareCloneSelection($tests, "tst");
4126  foreach ($tests as $ref_id) {
4127  if ($use_object_id) {
4128  $obj_id = ilObject::_lookupObjId($ref_id);
4129  $result_array[$obj_id] = $titles[$ref_id];
4130  } else {
4131  $result_array[$ref_id] = $titles[$ref_id];
4132  }
4133  }
4134  }
4135  return $result_array;
4136  }
4137 
4146  public function cloneObject(int $target_id, int $copy_id = 0, bool $omit_tree = false): ?ilObject
4147  {
4148  $this->loadFromDb();
4149 
4150  $new_obj = parent::cloneObject($target_id, $copy_id, $omit_tree);
4151  $new_obj->setTmpCopyWizardCopyId($copy_id);
4152  $this->cloneMetaData($new_obj);
4153 
4154  $new_obj->saveToDb();
4155  $new_obj->addToNewsOnOnline(false, $new_obj->getObjectProperties()->getPropertyIsOnline()->getIsOnline());
4156  $this->getMainSettingsRepository()->store(
4157  $this->getMainSettings()->withTestId($new_obj->getTestId())
4158  ->withIntroductionSettings(
4159  $this->getMainSettings()->getIntroductionSettings()->withIntroductionPageId(
4160  $this->cloneIntroduction()
4161  )->withTestId($new_obj->getTestId())
4162  )->withFinishingSettings(
4163  $this->getMainSettings()->getFinishingSettings()->withConcludingRemarksPageId(
4164  $this->cloneConcludingRemarks()
4165  )->withTestId($new_obj->getTestId())
4166  )
4167  );
4168  $this->getScoreSettingsRepository()->store(
4169  $this->getScoreSettings()->withTestId($new_obj->getTestId())
4170  );
4171  $this->marks_repository->storeMarkSchema(
4172  $this->getMarkSchema()->withTestId($new_obj->getTestId())
4173  );
4174 
4175  $new_obj->setTemplate($this->getTemplate());
4176 
4177  // clone certificate
4178  $pathFactory = new ilCertificatePathFactory();
4179  $templateRepository = new ilCertificateTemplateDatabaseRepository($this->db);
4180 
4181  $cloneAction = new ilCertificateCloneAction(
4182  $this->db,
4183  $pathFactory,
4184  $templateRepository,
4186  );
4187 
4188  $cloneAction->cloneCertificate($this, $new_obj);
4189 
4190  $this->question_set_config_factory->getQuestionSetConfig()->cloneQuestionSetRelatedData($new_obj);
4191  $new_obj->saveQuestionsToDb();
4192 
4193  $skillLevelThresholdList = new ilTestSkillLevelThresholdList($this->db);
4194  $skillLevelThresholdList->setTestId($this->getTestId());
4195  $skillLevelThresholdList->loadFromDb();
4196  $skillLevelThresholdList->cloneListForTest($new_obj->getTestId());
4197 
4198  $obj_settings = new ilLPObjSettings($this->getId());
4199  $obj_settings->cloneSettings($new_obj->getId());
4200 
4201  if ($new_obj->getTestLogger()->isLoggingEnabled()) {
4202  $new_obj->getTestLogger()->logTestAdministrationInteraction(
4203  $new_obj->getTestLogger()->getInteractionFactory()->buildTestAdministrationInteraction(
4204  $new_obj->getRefId(),
4205  $this->user->getId(),
4206  TestAdministrationInteractionTypes::NEW_TEST_CREATED,
4207  []
4208  )
4209  );
4210  }
4211 
4212  return $new_obj;
4213  }
4214 
4215  public function getQuestionCount(): int
4216  {
4217  $num = 0;
4218 
4219  if ($this->isRandomTest()) {
4220  $questionSetConfig = new ilTestRandomQuestionSetConfig(
4221  $this->tree,
4222  $this->db,
4223  $this->lng,
4224  $this->logger,
4225  $this->component_repository,
4226  $this,
4227  $this->questionrepository
4228  );
4229 
4230  $questionSetConfig->loadFromDb();
4231 
4232  if ($questionSetConfig->isQuestionAmountConfigurationModePerPool()) {
4233  $sourcePoolDefinitionList = new ilTestRandomQuestionSetSourcePoolDefinitionList(
4234  $this->db,
4235  $this,
4237  );
4238 
4239  $sourcePoolDefinitionList->loadDefinitions();
4240 
4241  if (is_int($sourcePoolDefinitionList->getQuestionAmount())) {
4242  $num = $sourcePoolDefinitionList->getQuestionAmount();
4243  }
4244  } elseif (is_int($questionSetConfig->getQuestionAmountPerTest())) {
4245  $num = $questionSetConfig->getQuestionAmountPerTest();
4246  }
4247  } else {
4248  $this->loadQuestions();
4249  $num = count($this->questions);
4250  }
4251 
4252  return $num;
4253  }
4254 
4256  {
4257  if ($this->isRandomTest()) {
4258  return $this->getQuestionCount();
4259  }
4260  return count($this->questions);
4261  }
4262 
4270  public static function _getObjectIDFromTestID($test_id)
4271  {
4272  global $DIC;
4273  $ilDB = $DIC['ilDB'];
4274  $object_id = false;
4275  $result = $ilDB->queryF(
4276  "SELECT obj_fi FROM tst_tests WHERE test_id = %s",
4277  ['integer'],
4278  [$test_id]
4279  );
4280  if ($result->numRows()) {
4281  $row = $ilDB->fetchAssoc($result);
4282  $object_id = $row["obj_fi"];
4283  }
4284  return $object_id;
4285  }
4286 
4294  public static function _getObjectIDFromActiveID($active_id)
4295  {
4296  global $DIC;
4297  $ilDB = $DIC['ilDB'];
4298  $object_id = false;
4299  $result = $ilDB->queryF(
4300  "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",
4301  ['integer'],
4302  [$active_id]
4303  );
4304  if ($result->numRows()) {
4305  $row = $ilDB->fetchAssoc($result);
4306  $object_id = $row["obj_fi"];
4307  }
4308  return $object_id;
4309  }
4310 
4318  public static function _getTestIDFromObjectID($object_id)
4319  {
4320  global $DIC;
4321  $ilDB = $DIC['ilDB'];
4322  $test_id = false;
4323  $result = $ilDB->queryF(
4324  "SELECT test_id FROM tst_tests WHERE obj_fi = %s",
4325  ['integer'],
4326  [$object_id]
4327  );
4328  if ($result->numRows()) {
4329  $row = $ilDB->fetchAssoc($result);
4330  $test_id = $row["test_id"];
4331  }
4332  return $test_id;
4333  }
4334 
4343  public function getTextAnswer($active_id, $question_id, $pass = null): string
4344  {
4345  if (($active_id) && ($question_id)) {
4346  if ($pass === null) {
4347  $pass = assQuestion::_getSolutionMaxPass($question_id, $active_id);
4348  }
4349  if ($pass === null) {
4350  return '';
4351  }
4352  $query = $this->db->queryF(
4353  "SELECT value1 FROM tst_solutions WHERE active_fi = %s AND question_fi = %s AND pass = %s",
4354  ['integer', 'integer', 'integer'],
4355  [$active_id, $question_id, $pass]
4356  );
4357  $result = $this->db->fetchAll($query);
4358  if (count($result) == 1) {
4359  return $result[0]["value1"];
4360  }
4361  }
4362  return '';
4363  }
4364 
4372  public function getQuestiontext($question_id): string
4373  {
4374  $res = "";
4375  if ($question_id) {
4376  $result = $this->db->queryF(
4377  "SELECT question_text FROM qpl_questions WHERE question_id = %s",
4378  ['integer'],
4379  [$question_id]
4380  );
4381  if ($result->numRows() == 1) {
4382  $row = $this->db->fetchAssoc($result);
4383  $res = $row["question_text"];
4384  }
4385  }
4386  return $res;
4387  }
4388 
4390  {
4391  $participant_list = new ilTestParticipantList($this, $this->user, $this->lng, $this->db);
4392  $participant_list->initializeFromDbRows($this->getTestParticipants());
4393 
4394  return $participant_list;
4395  }
4396 
4403  public function &getInvitedUsers(int $user_id = 0, $order = "login, lastname, firstname"): array
4404  {
4405  $result_array = [];
4406 
4407  if ($this->getAnonymity()) {
4408  if ($user_id !== 0) {
4409  $result = $this->db->queryF(
4410  "SELECT tst_active.active_id, tst_active.tries, usr_id, %s login, %s lastname, %s firstname, " .
4411  "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 " .
4412  "LEFT JOIN tst_active ON tst_active.user_fi = tst_invited_user.user_fi AND tst_active.test_fi = tst_invited_user.test_fi " .
4413  "WHERE tst_invited_user.test_fi = %s and tst_invited_user.user_fi=usr_data.usr_id AND usr_data.usr_id=%s " .
4414  "ORDER BY $order",
4415  ['text', 'text', 'text', 'integer', 'integer'],
4416  ['', $this->lng->txt('anonymous'), '', $this->getTestId(), $user_id]
4417  );
4418  } else {
4419  $result = $this->db->queryF(
4420  "SELECT tst_active.active_id, tst_active.tries, usr_id, %s login, %s lastname, %s firstname, " .
4421  "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 " .
4422  "LEFT JOIN tst_active ON tst_active.user_fi = tst_invited_user.user_fi AND tst_active.test_fi = tst_invited_user.test_fi " .
4423  "WHERE tst_invited_user.test_fi = %s and tst_invited_user.user_fi=usr_data.usr_id " .
4424  "ORDER BY $order",
4425  ['text', 'text', 'text', 'integer'],
4426  ['', $this->lng->txt('anonymous'), '', $this->getTestId()]
4427  );
4428  }
4429  } else {
4430  if ($user_id !== 0) {
4431  $result = $this->db->queryF(
4432  "SELECT tst_active.active_id, tst_active.tries, usr_id, login, lastname, firstname, " .
4433  "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 " .
4434  "LEFT JOIN tst_active ON tst_active.user_fi = tst_invited_user.user_fi AND tst_active.test_fi = tst_invited_user.test_fi " .
4435  "WHERE tst_invited_user.test_fi = %s and tst_invited_user.user_fi=usr_data.usr_id AND usr_data.usr_id=%s " .
4436  "ORDER BY $order",
4437  ['integer', 'integer'],
4438  [$this->getTestId(), $user_id]
4439  );
4440  } else {
4441  $result = $this->db->queryF(
4442  "SELECT tst_active.active_id, tst_active.tries, usr_id, login, lastname, firstname, " .
4443  "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 " .
4444  "LEFT JOIN tst_active ON tst_active.user_fi = tst_invited_user.user_fi AND tst_active.test_fi = tst_invited_user.test_fi " .
4445  "WHERE tst_invited_user.test_fi = %s and tst_invited_user.user_fi=usr_data.usr_id " .
4446  "ORDER BY $order",
4447  ['integer'],
4448  [$this->getTestId()]
4449  );
4450  }
4451  }
4452  $result_array = [];
4453  while ($row = $this->db->fetchAssoc($result)) {
4454  $result_array[$row['usr_id']] = $row;
4455  }
4456  return $result_array;
4457  }
4458 
4459  public function getTestParticipants(): array
4460  {
4461  if ($this->getMainSettings()->getGeneralSettings()->getAnonymity()) {
4462  $query = "
4463  SELECT tst_active.active_id,
4464  tst_active.tries,
4465  tst_active.user_fi usr_id,
4466  %s login,
4467  %s lastname,
4468  %s firstname,
4469  tst_active.submitted test_finished,
4470  usr_data.matriculation,
4471  usr_data.active,
4472  tst_active.lastindex,
4473  COALESCE(tst_active.last_finished_pass, -1) <> tst_active.last_started_pass unfinished_passes
4474  FROM tst_active
4475  LEFT JOIN usr_data
4476  ON tst_active.user_fi = usr_data.usr_id
4477  WHERE tst_active.test_fi = %s
4478  ORDER BY usr_data.lastname
4479  ";
4480  $result = $this->db->queryF(
4481  $query,
4482  ['text', 'text', 'text', 'integer'],
4483  ['', $this->lng->txt("anonymous"), "", $this->getTestId()]
4484  );
4485  } else {
4486  $query = "
4487  SELECT tst_active.active_id,
4488  tst_active.tries,
4489  tst_active.user_fi usr_id,
4490  usr_data.login,
4491  usr_data.lastname,
4492  usr_data.firstname,
4493  tst_active.submitted test_finished,
4494  usr_data.matriculation,
4495  usr_data.active,
4496  tst_active.lastindex,
4497  COALESCE(tst_active.last_finished_pass, -1) <> tst_active.last_started_pass unfinished_passes
4498  FROM tst_active
4499  LEFT JOIN usr_data
4500  ON tst_active.user_fi = usr_data.usr_id
4501  WHERE tst_active.test_fi = %s
4502  ORDER BY usr_data.lastname
4503  ";
4504  $result = $this->db->queryF(
4505  $query,
4506  ['integer'],
4507  [$this->getTestId()]
4508  );
4509  }
4510  $data = [];
4511  while ($row = $this->db->fetchAssoc($result)) {
4512  $data[$row['active_id']] = $row;
4513  }
4514  foreach ($data as $index => $participant) {
4515  if (strlen(trim($participant["firstname"] . $participant["lastname"])) == 0) {
4516  $data[$index]["lastname"] = $this->lng->txt("deleted_user");
4517  }
4518  }
4519  return $data;
4520  }
4521 
4522  public function getTestParticipantsForManualScoring($filter = null): array
4523  {
4524  if (!$this->getGlobalSettings()->isManualScoringEnabled()) {
4525  return [];
4526  }
4527 
4528  $filtered_participants = [];
4529  foreach ($this->getTestParticipants() as $active_id => $participant) {
4530  if ($participant['tries'] > 0) {
4531  switch ($filter) {
4532  case 4:
4533  if ($this->test_man_scoring_done_helper->isDone((int) $active_id)) {
4534  $filtered_participants[$active_id] = $participant;
4535  }
4536  break;
4537  case 5:
4538  if (!$this->test_man_scoring_done_helper->isDone((int) $active_id)) {
4539  $filtered_participants[$active_id] = $participant;
4540  }
4541  break;
4542  default:
4543  $filtered_participants[$active_id] = $participant;
4544  }
4545  }
4546  }
4547  return $filtered_participants;
4548  }
4549 
4556  public function getUserData($ids): array
4557  {
4558  if (!is_array($ids) || count($ids) == 0) {
4559  return [];
4560  }
4561 
4562  if ($this->getAnonymity()) {
4563  $result = $this->db->queryF(
4564  "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",
4565  ['text', 'text', 'text'],
4566  ["", $this->lng->txt("anonymous"), ""]
4567  );
4568  } else {
4569  $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");
4570  }
4571 
4572  $result_array = [];
4573  while ($row = $this->db->fetchAssoc($result)) {
4574  $result_array[$row["usr_id"]] = $row;
4575  }
4576  return $result_array;
4577  }
4578 
4579  public function getGroupData($ids): array
4580  {
4581  if (!is_array($ids) || count($ids) == 0) {
4582  return [];
4583  }
4584  $result = [];
4585  foreach ($ids as $ref_id) {
4586  $obj_id = ilObject::_lookupObjId($ref_id);
4587  $result[$ref_id] = ["ref_id" => $ref_id, "title" => ilObject::_lookupTitle($obj_id), "description" => ilObject::_lookupDescription($obj_id)];
4588  }
4589  return $result;
4590  }
4591 
4592  public function getRoleData($ids): array
4593  {
4594  if (!is_array($ids) || count($ids) == 0) {
4595  return [];
4596  }
4597  $result = [];
4598  foreach ($ids as $obj_id) {
4599  $result[$obj_id] = ["obj_id" => $obj_id, "title" => ilObject::_lookupTitle($obj_id), "description" => ilObject::_lookupDescription($obj_id)];
4600  }
4601  return $result;
4602  }
4603 
4610  public function inviteUser($user_id, $client_ip = "")
4611  {
4612  $this->db->manipulateF(
4613  "DELETE FROM tst_invited_user WHERE test_fi = %s AND user_fi = %s",
4614  ['integer', 'integer'],
4615  [$this->getTestId(), $user_id]
4616  );
4617  $this->db->manipulateF(
4618  "INSERT INTO tst_invited_user (test_fi, user_fi, ip_range_from, ip_range_to, tstamp) VALUES (%s, %s, %s, %s, %s)",
4619  ['integer', 'integer', 'text', 'text', 'integer'],
4620  [$this->getTestId(), $user_id, (strlen($client_ip)) ? $client_ip : null, (strlen($client_ip)) ? $client_ip : null,time()]
4621  );
4622  }
4623 
4629  public static function _getSolvedQuestions($active_id, $question_fi = null): array
4630  {
4631  global $DIC;
4632  $ilDB = $DIC['ilDB'];
4633  if (is_numeric($question_fi)) {
4634  $result = $ilDB->queryF(
4635  "SELECT question_fi, solved FROM tst_qst_solved WHERE active_fi = %s AND question_fi=%s",
4636  ['integer', 'integer'],
4637  [$active_id, $question_fi]
4638  );
4639  } else {
4640  $result = $ilDB->queryF(
4641  "SELECT question_fi, solved FROM tst_qst_solved WHERE active_fi = %s",
4642  ['integer'],
4643  [$active_id]
4644  );
4645  }
4646  $result_array = [];
4647  while ($row = $ilDB->fetchAssoc($result)) {
4648  $result_array[$row["question_fi"]] = $row;
4649  }
4650  return $result_array;
4651  }
4652 
4653 
4657  public function setQuestionSetSolved($value, $question_id, $user_id)
4658  {
4659  $active_id = $this->getActiveIdOfUser($user_id);
4660  $this->db->manipulateF(
4661  "DELETE FROM tst_qst_solved WHERE active_fi = %s AND question_fi = %s",
4662  ['integer', 'integer'],
4663  [$active_id, $question_id]
4664  );
4665  $this->db->manipulateF(
4666  "INSERT INTO tst_qst_solved (solved, question_fi, active_fi) VALUES (%s, %s, %s)",
4667  ['integer', 'integer', 'integer'],
4668  [$value, $question_id, $active_id]
4669  );
4670  }
4671 
4675  public function isTestFinished($active_id): bool
4676  {
4677  $result = $this->db->queryF(
4678  "SELECT submitted FROM tst_active WHERE active_id=%s AND submitted=%s",
4679  ['integer', 'integer'],
4680  [$active_id, 1]
4681  );
4682  return $result->numRows() == 1;
4683  }
4684 
4688  public function isActiveTestSubmitted($user_id = null): bool
4689  {
4690  if (!is_numeric($user_id)) {
4691  $user_id = $this->user->getId();
4692  }
4693 
4694  $result = $this->db->queryF(
4695  "SELECT submitted FROM tst_active WHERE test_fi=%s AND user_fi=%s AND submitted=%s",
4696  ['integer', 'integer', 'integer'],
4697  [$this->getTestId(), $user_id, 1]
4698  );
4699  return $result->numRows() == 1;
4700  }
4701 
4705  public function hasNrOfTriesRestriction(): bool
4706  {
4707  return $this->getNrOfTries() != 0;
4708  }
4709 
4710 
4716  public function isNrOfTriesReached($tries): bool
4717  {
4718  return $tries >= $this->getNrOfTries();
4719  }
4720 
4721 
4729  public function getAllTestResults($participants): array
4730  {
4731  $results = [];
4732  $row = [
4733  "user_id" => $this->lng->txt("user_id"),
4734  "matriculation" => $this->lng->txt("matriculation"),
4735  "lastname" => $this->lng->txt("lastname"),
4736  "firstname" => $this->lng->txt("firstname"),
4737  "login" => $this->lng->txt("login"),
4738  "reached_points" => $this->lng->txt("tst_reached_points"),
4739  "max_points" => $this->lng->txt("tst_maximum_points"),
4740  "percent_value" => $this->lng->txt("tst_percent_solved"),
4741  "mark" => $this->lng->txt("tst_mark"),
4742  "passed" => $this->lng->txt("tst_mark_passed"),
4743  ];
4744  $results[] = $row;
4745  if (count($participants)) {
4746  foreach ($participants as $active_id => $user_rec) {
4747  $mark = '';
4748  $row = [];
4749  $reached_points = 0;
4750  $max_points = 0;
4751  $pass = ilObjTest::_getResultPass($active_id);
4752  // abort if no valid pass can be found
4753  if (!is_int($pass)) {
4754  continue;
4755  }
4756  foreach ($this->questions as $value) {
4757  $question = ilObjTest::_instanciateQuestion($value);
4758  if (is_object($question)) {
4759  $max_points += $question->getMaximumPoints();
4760  $reached_points += $question->getReachedPoints($active_id, $pass);
4761  }
4762  }
4763  if ($max_points > 0) {
4764  $percentvalue = $reached_points / $max_points;
4765  if ($percentvalue < 0) {
4766  $percentvalue = 0.0;
4767  }
4768  } else {
4769  $percentvalue = 0;
4770  }
4771  $mark_obj = $this->getMarkSchema()->getMatchingMark($percentvalue * 100);
4772  $passed = "";
4773  if ($mark_obj !== null) {
4774  $mark = $mark_obj->getOfficialName();
4775  }
4776  if ($this->getAnonymity()) {
4777  $user_rec['firstname'] = "";
4778  $user_rec['lastname'] = $this->lng->txt("anonymous");
4779  }
4780  $results[] = [
4781  "user_id" => $user_rec['usr_id'],
4782  "matriculation" => $user_rec['matriculation'],
4783  "lastname" => $user_rec['lastname'],
4784  "firstname" => $user_rec['firstname'],
4785  "login" => $user_rec['login'],
4786  "reached_points" => $reached_points,
4787  "max_points" => $max_points,
4788  "percent_value" => $percentvalue,
4789  "mark" => $mark,
4790  "passed" => $user_rec['passed'] ? '1' : '0',
4791  ];
4792  }
4793  }
4794  return $results;
4795  }
4796 
4805  public static function _getPass($active_id): int
4806  {
4807  global $DIC;
4808  $ilDB = $DIC['ilDB'];
4809  $result = $ilDB->queryF(
4810  "SELECT tries FROM tst_active WHERE active_id = %s",
4811  ['integer'],
4812  [$active_id]
4813  );
4814  if ($result->numRows()) {
4815  $row = $ilDB->fetchAssoc($result);
4816  return $row["tries"];
4817  } else {
4818  return 0;
4819  }
4820  }
4821 
4831  public static function _getMaxPass($active_id): ?int
4832  {
4833  global $DIC;
4834  $ilDB = $DIC['ilDB'];
4835  $result = $ilDB->queryF(
4836  "SELECT MAX(pass) maxpass FROM tst_pass_result WHERE active_fi = %s",
4837  ['integer'],
4838  [$active_id]
4839  );
4840 
4841  if ($result->numRows()) {
4842  $row = $ilDB->fetchAssoc($result);
4843  return $row["maxpass"];
4844  }
4845 
4846  return null;
4847  }
4848 
4854  public static function _getBestPass($active_id): ?int
4855  {
4856  global $DIC;
4857  $ilDB = $DIC['ilDB'];
4858 
4859  $result = $ilDB->queryF(
4860  "SELECT * FROM tst_pass_result WHERE active_fi = %s",
4861  ['integer'],
4862  [$active_id]
4863  );
4864 
4865  if (!$result->numRows()) {
4866  return null;
4867  }
4868 
4869  $bestrow = null;
4870  $bestfactor = 0.0;
4871  while ($row = $ilDB->fetchAssoc($result)) {
4872  if ($row["maxpoints"] > 0.0) {
4873  $factor = (float) ($row["points"] / $row["maxpoints"]);
4874  } else {
4875  $factor = 0.0;
4876  }
4877  if ($factor === 0.0 && $bestfactor === 0.0
4878  || $factor > $bestfactor) {
4879  $bestrow = $row;
4880  $bestfactor = $factor;
4881  }
4882  }
4883 
4884  if (is_array($bestrow)) {
4885  return $bestrow["pass"];
4886  }
4887 
4888  return null;
4889  }
4890 
4899  public static function _getResultPass($active_id): ?int
4900  {
4901  $counted_pass = null;
4902  if (ilObjTest::_getPassScoring($active_id) == self::SCORE_BEST_PASS) {
4903  $counted_pass = ilObjTest::_getBestPass($active_id);
4904  } else {
4905  $counted_pass = ilObjTest::_getMaxPass($active_id);
4906  }
4907  return $counted_pass;
4908  }
4909 
4919  public function getAnsweredQuestionCount($active_id, $pass = null): int
4920  {
4921  if ($this->isRandomTest()) {
4922  $this->loadQuestions($active_id, $pass);
4923  }
4924  $workedthrough = 0;
4925  foreach ($this->questions as $value) {
4926  if ($this->questionrepository->lookupResultRecordExist($active_id, $value, $pass)) {
4927  $workedthrough += 1;
4928  }
4929  }
4930  return $workedthrough;
4931  }
4932 
4939  public static function lookupPassResultsUpdateTimestamp($active_id, $pass): int
4940  {
4941  global $DIC;
4942  $ilDB = $DIC['ilDB'];
4943 
4944  if (is_null($pass)) {
4945  $pass = 0;
4946  }
4947 
4948  $query = "
4949  SELECT tst_pass_result.tstamp pass_res_tstamp,
4950  tst_test_result.tstamp quest_res_tstamp
4951 
4952  FROM tst_pass_result
4953 
4954  LEFT JOIN tst_test_result
4955  ON tst_test_result.active_fi = tst_pass_result.active_fi
4956  AND tst_test_result.pass = tst_pass_result.pass
4957 
4958  WHERE tst_pass_result.active_fi = %s
4959  AND tst_pass_result.pass = %s
4960 
4961  ORDER BY tst_test_result.tstamp DESC
4962  ";
4963 
4964  $result = $ilDB->queryF(
4965  $query,
4966  ['integer', 'integer'],
4967  [$active_id, $pass]
4968  );
4969 
4970  while ($row = $ilDB->fetchAssoc($result)) {
4971  if ($row['quest_res_tstamp']) {
4972  return $row['quest_res_tstamp'];
4973  }
4974 
4975  return $row['pass_res_tstamp'];
4976  }
4977 
4978  return 0;
4979  }
4980 
4989  public function isExecutable($test_session, $user_id, $allow_pass_increase = false): array
4990  {
4991  $result = [
4992  "executable" => true,
4993  "errormessage" => ""
4994  ];
4995 
4996  if (!$this->getObjectProperties()->getPropertyIsOnline()->getIsOnline()) {
4997  $result["executable"] = false;
4998  $result["errormessage"] = $this->lng->txt('autosave_failed') . ': ' . $this->lng->txt('offline');
4999  return $result;
5000  }
5001 
5002  if (!$this->startingTimeReached()) {
5003  $result["executable"] = false;
5004  $result["errormessage"] = sprintf($this->lng->txt("detail_starting_time_not_reached"), ilDatePresentation::formatDate(new ilDateTime($this->getStartingTime(), IL_CAL_UNIX)));
5005  return $result;
5006  }
5007  if ($this->endingTimeReached()) {
5008  $result["executable"] = false;
5009  $result["errormessage"] = sprintf($this->lng->txt("detail_ending_time_reached"), ilDatePresentation::formatDate(new ilDateTime($this->getEndingTime(), IL_CAL_UNIX)));
5010  return $result;
5011  }
5012 
5013  $active_id = $this->getActiveIdOfUser($user_id);
5014 
5015  if ($this->getEnableProcessingTime()
5016  && $active_id > 0
5017  && ($starting_time = $this->getStartingTimeOfUser($active_id)) !== false
5018  && $this->isMaxProcessingTimeReached($starting_time, $active_id)) {
5019  $result["executable"] = false;
5020  $result["errormessage"] = $this->lng->txt("detail_max_processing_time_reached");
5021  return $result;
5022  }
5023 
5024  $testPassesSelector = new ilTestPassesSelector($this->db, $this);
5025  $testPassesSelector->setActiveId($active_id);
5026  $testPassesSelector->setLastFinishedPass($test_session->getLastFinishedPass());
5027 
5028  if ($this->hasNrOfTriesRestriction() && ($active_id > 0)) {
5029  $closedPasses = $testPassesSelector->getClosedPasses();
5030 
5031  if (count($closedPasses) >= $this->getNrOfTries()) {
5032  $result["executable"] = false;
5033  $result["errormessage"] = $this->lng->txt("maximum_nr_of_tries_reached");
5034  return $result;
5035  }
5036 
5037  if ($this->isBlockPassesAfterPassedEnabled() && !$testPassesSelector->openPassExists()) {
5038  if (ilObjTestAccess::_isPassed($user_id, $this->getId())) {
5039  $result['executable'] = false;
5040  $result['errormessage'] = $this->lng->txt("tst_addit_passes_blocked_after_passed_msg");
5041  return $result;
5042  }
5043  }
5044  }
5045 
5046  $next_pass_allowed_timestamp = 0;
5047  if (!$this->isNextPassAllowed($testPassesSelector, $next_pass_allowed_timestamp)) {
5048  $date = ilDatePresentation::formatDate(new ilDateTime($next_pass_allowed_timestamp, IL_CAL_UNIX));
5049 
5050  $result['executable'] = false;
5051  $result['errormessage'] = sprintf($this->lng->txt('wait_for_next_pass_hint_msg'), $date);
5052  return $result;
5053  }
5054  return $result;
5055  }
5056 
5057  public function isNextPassAllowed(ilTestPassesSelector $testPassesSelector, int &$next_pass_allowed_timestamp): bool
5058  {
5059  $waiting_between_passes = $this->getMainSettings()->getTestBehaviourSettings()->getPassWaiting();
5060  $last_finished_pass_timestamp = $testPassesSelector->getLastFinishedPassTimestamp();
5061 
5062  if (
5063  $this->getMainSettings()->getTestBehaviourSettings()->getPassWaitingEnabled()
5064  && ($waiting_between_passes !== '')
5065  && ($testPassesSelector->getLastFinishedPass() !== null)
5066  && ($last_finished_pass_timestamp !== null)
5067  ) {
5068  $time_values = explode(':', $waiting_between_passes);
5069  $next_pass_allowed_timestamp = strtotime('+ ' . $time_values[0] . ' Days + ' . $time_values[1] . ' Hours' . $time_values[2] . ' Minutes', $last_finished_pass_timestamp);
5070  return (time() > $next_pass_allowed_timestamp);
5071  }
5072 
5073  return true;
5074  }
5075 
5076  public function canShowTestResults(ilTestSession $test_session): bool
5077  {
5078  $pass_selector = new ilTestPassesSelector($this->db, $this);
5079 
5080  $pass_selector->setActiveId($test_session->getActiveId());
5081  $pass_selector->setLastFinishedPass($test_session->getLastFinishedPass());
5082 
5083  return $pass_selector->hasReportablePasses();
5084  }
5085 
5086  public function hasAnyTestResult(ilTestSession $test_session): bool
5087  {
5088  $pass_selector = new ilTestPassesSelector($this->db, $this);
5089 
5090  $pass_selector->setActiveId($test_session->getActiveId());
5091  $pass_selector->setLastFinishedPass($test_session->getLastFinishedPass());
5092 
5093  return $pass_selector->hasExistingPasses();
5094  }
5095 
5103  public function getStartingTimeOfUser($active_id, $pass = null)
5104  {
5105  if ($active_id < 1) {
5106  return false;
5107  }
5108  if ($pass === null) {
5109  $pass = ($this->getResetProcessingTime()) ? self::_getPass($active_id) : 0;
5110  }
5111  $result = $this->db->queryF(
5112  "SELECT tst_times.started FROM tst_times WHERE tst_times.active_fi = %s AND tst_times.pass = %s ORDER BY tst_times.started",
5113  ['integer', 'integer'],
5114  [$active_id, $pass]
5115  );
5116  if ($result->numRows()) {
5117  $row = $this->db->fetchAssoc($result);
5118  if (preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["started"], $matches)) {
5119  return mktime(
5120  (int) $matches[4],
5121  (int) $matches[5],
5122  (int) $matches[6],
5123  (int) $matches[2],
5124  (int) $matches[3],
5125  (int) $matches[1]
5126  );
5127  } else {
5128  return time();
5129  }
5130  } else {
5131  return time();
5132  }
5133  }
5134 
5141  public function isMaxProcessingTimeReached(int $starting_time, int $active_id): bool
5142  {
5143  if (!$this->getEnableProcessingTime()) {
5144  return false;
5145  }
5146 
5147  $processing_time = $this->getProcessingTimeInSeconds($active_id);
5148  $now = time();
5149  if ($now > ($starting_time + $processing_time)) {
5150  return true;
5151  }
5152 
5153  return false;
5154  }
5155 
5156  public function getTestQuestions(): array
5157  {
5158  $tags_trafo = $this->refinery->string()->stripTags();
5159 
5160  $query = "
5161  SELECT questions.*,
5162  questtypes.type_tag,
5163  tstquest.sequence,
5164  origquest.obj_fi orig_obj_fi
5165 
5166  FROM qpl_questions questions
5167 
5168  INNER JOIN qpl_qst_type questtypes
5169  ON questtypes.question_type_id = questions.question_type_fi
5170 
5171  INNER JOIN tst_test_question tstquest
5172  ON tstquest.question_fi = questions.question_id
5173 
5174  LEFT JOIN qpl_questions origquest
5175  ON origquest.question_id = questions.original_id
5176 
5177  WHERE tstquest.test_fi = %s
5178 
5179  ORDER BY tstquest.sequence
5180  ";
5181 
5182  $query_result = $this->db->queryF(
5183  $query,
5184  ['integer'],
5185  [$this->getTestId()]
5186  );
5187 
5188  $questions = [];
5189 
5190  while ($row = $this->db->fetchAssoc($query_result)) {
5191  $row['title'] = $tags_trafo->transform($row['title']);
5192  $row['description'] = $tags_trafo->transform($row['description'] !== '' && $row['description'] !== null ? $row['description'] : '&nbsp;');
5193  $row['author'] = $tags_trafo->transform($row['author']);
5194 
5195  $questions[] = $row;
5196  }
5197 
5198  return $questions;
5199  }
5200 
5201  public function isTestQuestion(int $question_id): bool
5202  {
5203  foreach ($this->getTestQuestions() as $questionData) {
5204  if ($questionData['question_id'] != $question_id) {
5205  continue;
5206  }
5207 
5208  return true;
5209  }
5210 
5211  return false;
5212  }
5213 
5214  public function checkQuestionParent(int $question_id): bool
5215  {
5216  $row = $this->db->fetchAssoc($this->db->queryF(
5217  "SELECT COUNT(question_id) cnt FROM qpl_questions WHERE question_id = %s AND obj_fi = %s",
5218  ['integer', 'integer'],
5219  [$question_id, $this->getId()]
5220  ));
5221 
5222  return (bool) $row['cnt'];
5223  }
5224 
5225  public function getFixedQuestionSetTotalPoints(): float
5226  {
5227  $points = 0;
5228 
5229  foreach ($this->getTestQuestions() as $question_data) {
5230  $points += $question_data['points'];
5231  }
5232 
5233  return $points;
5234  }
5235 
5239  public function getPotentialRandomTestQuestions(): array
5240  {
5241  $query = "
5242  SELECT questions.*,
5243  questtypes.type_tag,
5244  origquest.obj_fi orig_obj_fi
5245 
5246  FROM qpl_questions questions
5247 
5248  INNER JOIN qpl_qst_type questtypes
5249  ON questtypes.question_type_id = questions.question_type_fi
5250 
5251  INNER JOIN tst_rnd_cpy tstquest
5252  ON tstquest.qst_fi = questions.question_id
5253 
5254  LEFT JOIN qpl_questions origquest
5255  ON origquest.question_id = questions.original_id
5256 
5257  WHERE tstquest.tst_fi = %s
5258  ";
5259 
5260  $query_result = $this->db->queryF(
5261  $query,
5262  ['integer'],
5263  [$this->getTestId()]
5264  );
5265 
5266  return $this->db->fetchAll($query_result);
5267  }
5268 
5269  public function getShuffleQuestions(): bool
5270  {
5271  return $this->getMainSettings()->getQuestionBehaviourSettings()->getShuffleQuestions();
5272  }
5273 
5285  public function getListOfQuestionsSettings()
5286  {
5287  return $this->getMainSettings()->getParticipantFunctionalitySettings()->getUsrPassOverviewMode();
5288  }
5289 
5290  public function getListOfQuestions(): bool
5291  {
5292  return $this->getMainSettings()->getParticipantFunctionalitySettings()->getQuestionListEnabled();
5293  }
5294 
5295  public function getUsrPassOverviewEnabled(): bool
5296  {
5297  return $this->getMainSettings()->getParticipantFunctionalitySettings()->getUsrPassOverviewEnabled();
5298  }
5299 
5300  public function getListOfQuestionsStart(): bool
5301  {
5302  return $this->getMainSettings()->getParticipantFunctionalitySettings()->getShownQuestionListAtBeginning();
5303  }
5304 
5305  public function getListOfQuestionsEnd(): bool
5306  {
5307  return $this->getMainSettings()->getParticipantFunctionalitySettings()->getShownQuestionListAtEnd();
5308  }
5309 
5310  public function getListOfQuestionsDescription(): bool
5311  {
5312  return $this->getMainSettings()->getParticipantFunctionalitySettings()->getShowDescriptionInQuestionList();
5313  }
5314 
5318  public function getShowPassDetails(): bool
5319  {
5320  return $this->getScoreSettings()->getResultDetailsSettings()->getShowPassDetails();
5321  }
5322 
5326  public function getShowSolutionPrintview(): bool
5327  {
5328  return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionPrintview();
5329  }
5333  public function canShowSolutionPrintview($user_id = null): bool
5334  {
5335  return $this->getShowSolutionPrintview();
5336  }
5337 
5341  public function getShowSolutionFeedback(): bool
5342  {
5343  return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionFeedback();
5344  }
5345 
5349  public function getShowSolutionAnswersOnly(): bool
5350  {
5351  return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionAnswersOnly();
5352  }
5353 
5357  public function getShowSolutionSignature(): bool
5358  {
5359  return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionSignature();
5360  }
5361 
5365  public function getShowSolutionSuggested(): bool
5366  {
5367  return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionSuggested();
5368  }
5369 
5374  public function getShowSolutionListComparison(): bool
5375  {
5376  return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionListComparison();
5377  }
5378 
5379  public function getShowSolutionListOwnAnswers(): bool
5380  {
5381  return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionListOwnAnswers();
5382  }
5383 
5387  public static function _getUserIdFromActiveId(int $active_id): int
5388  {
5389  global $DIC;
5390  $ilDB = $DIC['ilDB'];
5391  $result = $ilDB->queryF(
5392  "SELECT user_fi FROM tst_active WHERE active_id = %s",
5393  ['integer'],
5394  [$active_id]
5395  );
5396  if ($result->numRows()) {
5397  $row = $ilDB->fetchAssoc($result);
5398  return $row["user_fi"];
5399  } else {
5400  return -1;
5401  }
5402  }
5403 
5404  public function _getLastAccess(int $active_id): string
5405  {
5406  $result = $this->db->queryF(
5407  "SELECT finished FROM tst_times WHERE active_fi = %s ORDER BY finished DESC",
5408  ['integer'],
5409  [$active_id]
5410  );
5411  if ($result->numRows()) {
5412  $row = $this->db->fetchAssoc($result);
5413  return $row["finished"];
5414  }
5415  return "";
5416  }
5417 
5418  public static function lookupLastTestPassAccess(int $active_id, int $pass_index): ?int
5419  {
5421  global $DIC;
5422  $ilDB = $DIC['ilDB'];
5423 
5424  $query = "
5425  SELECT MAX(tst_times.tstamp) as last_pass_access
5426  FROM tst_times
5427  WHERE active_fi = %s
5428  AND pass = %s
5429  ";
5430 
5431  $res = $ilDB->queryF(
5432  $query,
5433  ['integer', 'integer'],
5434  [$active_id, $pass_index]
5435  );
5436 
5437  while ($row = $ilDB->fetchAssoc($res)) {
5438  return $row['last_pass_access'];
5439  }
5440 
5441  return null;
5442  }
5443 
5451  public function isHTML($a_text): bool
5452  {
5453  if (preg_match("/<[^>]*?>/", $a_text)) {
5454  return true;
5455  } else {
5456  return false;
5457  }
5458  }
5459 
5466  public function qtiMaterialToArray($a_material): array
5467  {
5468  $result = '';
5469  $mobs = [];
5470  for ($i = 0; $i < $a_material->getMaterialCount(); $i++) {
5471  $material = $a_material->getMaterial($i);
5472  if ($material['type'] === 'mattext') {
5473  $result .= $material['material']->getContent();
5474  }
5475  if ($material['type'] === 'matimage') {
5476  $matimage = $material['material'];
5477  if (preg_match('/(il_([0-9]+)_mob_([0-9]+))/', $matimage->getLabel(), $matches)) {
5478  $mobs[] = [
5479  'mob' => $matimage->getLabel(),
5480  'uri' => $matimage->getUri()
5481  ];
5482  }
5483  }
5484  }
5485 
5486  $decoded_result = base64_decode($result);
5487  if (str_starts_with($decoded_result, '<PageObject>')) {
5488  $result = $decoded_result;
5489  }
5490 
5491  $this->logger->info(print_r(ilSession::get('import_mob_xhtml'), true));
5492  return [
5493  'text' => $result,
5494  'mobs' => $mobs
5495  ];
5496  }
5497 
5498  public function addQTIMaterial(ilXmlWriter &$xml_writer, ?int $page_id, string $material = ''): void
5499  {
5500  $xml_writer->xmlStartTag('material');
5501  $attrs = [
5502  'texttype' => 'text/plain'
5503  ];
5504  $file_ids = [];
5505  $mobs = [];
5506  if ($page_id !== null) {
5507  $attrs['texttype'] = 'text/xml';
5508  $mobs = ilObjMediaObject::_getMobsOfObject('tst:pg', $page_id);
5509  $page_object = new ilTestPage($page_id);
5510  $page_object->buildDom();
5511  $page_object->insertInstIntoIDs((string) IL_INST_ID);
5512  $material = base64_encode($page_object->getXMLFromDom());
5513  $file_ids = ilPCFileList::collectFileItems($page_object, $page_object->getDomDoc());
5514  foreach ($file_ids as $file_id) {
5515  $this->file_ids[] = (int) $file_id;
5516  };
5517  $mob_string = 'il_' . IL_INST_ID . '_mob_';
5518  } elseif ($this->isHTML($material)) {
5519  $attrs['texttype'] = 'text/xhtml';
5520  $mobs = ilObjMediaObject::_getMobsOfObject('tst:html', $this->getId());
5521  $mob_string = 'mm_';
5522  }
5523 
5524  $xml_writer->xmlElement('mattext', $attrs, $material);
5525  foreach ($mobs as $mob) {
5526  $mob_id_string = (string) $mob;
5527  $moblabel = 'il_' . IL_INST_ID . '_mob_' . $mob_id_string;
5528  if (strpos($material, $mob_string . $mob_id_string) !== false) {
5529  if (ilObjMediaObject::_exists($mob)) {
5530  $mob_obj = new ilObjMediaObject($mob);
5531  $imgattrs = [
5532  'label' => $moblabel,
5533  'uri' => 'objects/' . 'il_' . IL_INST_ID . '_mob_' . $mob_id_string . '/' . $mob_obj->getTitle()
5534  ];
5535  }
5536  $xml_writer->xmlElement('matimage', $imgattrs, null);
5537  }
5538  }
5539  $xml_writer->xmlEndTag('material');
5540  }
5541 
5548  public function prepareTextareaOutput($txt_output, $prepare_for_latex_output = false, $omitNl2BrWhenTextArea = false)
5549  {
5550  if ($txt_output == null) {
5551  $txt_output = '';
5552  }
5554  $txt_output,
5555  $prepare_for_latex_output,
5556  $omitNl2BrWhenTextArea
5557  );
5558  }
5559 
5560  public function getAnonymity(): bool
5561  {
5562  return $this->getMainSettings()->getGeneralSettings()->getAnonymity();
5563  }
5564 
5565 
5566  public static function _lookupAnonymity($a_obj_id): int
5567  {
5568  global $DIC;
5569  $ilDB = $DIC['ilDB'];
5570 
5571  $result = $ilDB->queryF(
5572  "SELECT anonymity FROM tst_tests WHERE obj_fi = %s",
5573  ['integer'],
5574  [$a_obj_id]
5575  );
5576  while ($row = $ilDB->fetchAssoc($result)) {
5577  return (int) $row['anonymity'];
5578  }
5579  return 0;
5580  }
5581 
5582  public function getShowCancel(): bool
5583  {
5584  return $this->getMainSettings()->getParticipantFunctionalitySettings()->getSuspendTestAllowed();
5585  }
5586 
5587  public function getShowMarker(): bool
5588  {
5589  return $this->getMainSettings()->getParticipantFunctionalitySettings()->getQuestionMarkingEnabled();
5590  }
5591 
5592  public function getFixedParticipants(): bool
5593  {
5594  return $this->getMainSettings()->getAccessSettings()->getFixedParticipants();
5595  }
5596 
5597  public function lookupQuestionSetTypeByActiveId(int $active_id): ?string
5598  {
5599  $query = "
5600  SELECT tst_tests.question_set_type
5601  FROM tst_active
5602  INNER JOIN tst_tests
5603  ON tst_active.test_fi = tst_tests.test_id
5604  WHERE tst_active.active_id = %s
5605  ";
5606 
5607  $res = $this->db->queryF($query, ['integer'], [$active_id]);
5608 
5609  while ($row = $this->db->fetchAssoc($res)) {
5610  return $row['question_set_type'];
5611  }
5612 
5613  return null;
5614  }
5615 
5626  public function userLookupFullName($user_id, $overwrite_anonymity = false, $sorted_order = false, $suffix = ""): string
5627  {
5628  if ($this->getAnonymity() && !$overwrite_anonymity) {
5629  return $this->lng->txt("anonymous") . $suffix;
5630  } else {
5631  $uname = ilObjUser::_lookupName($user_id);
5632  if (strlen($uname["firstname"] . $uname["lastname"]) == 0) {
5633  $uname["firstname"] = $this->lng->txt("deleted_user");
5634  }
5635  if ($sorted_order) {
5636  return trim($uname["lastname"] . ", " . $uname["firstname"]) . $suffix;
5637  } else {
5638  return trim($uname["firstname"] . " " . $uname["lastname"]) . $suffix;
5639  }
5640  }
5641  }
5642 
5648  public function getAvailableDefaults(): array
5649  {
5650  $result = $this->db->queryF(
5651  "SELECT * FROM tst_test_defaults WHERE user_fi = %s ORDER BY name ASC",
5652  ['integer'],
5653  [$this->user->getId()]
5654  );
5655  $defaults = [];
5656  while ($row = $this->db->fetchAssoc($result)) {
5657  $defaults[$row["test_defaults_id"]] = $row;
5658  }
5659  return $defaults;
5660  }
5661 
5662  public function getTestDefaults($test_defaults_id): ?array
5663  {
5664  $result = $this->db->queryF(
5665  "SELECT * FROM tst_test_defaults WHERE test_defaults_id = %s",
5666  ['integer'],
5667  [$test_defaults_id]
5668  );
5669  if ($result->numRows() == 1) {
5670  $row = $this->db->fetchAssoc($result);
5671  return $row;
5672  } else {
5673  return null;
5674  }
5675  }
5676 
5677  public function deleteDefaults($test_default_id)
5678  {
5679  $this->db->manipulateF(
5680  "DELETE FROM tst_test_defaults WHERE test_defaults_id = %s",
5681  ['integer'],
5682  [$test_default_id]
5683  );
5684  }
5685 
5692  public function addDefaults($a_name)
5693  {
5694  $main_settings = $this->getMainSettings();
5695  $score_settings = $this->getScoreSettings();
5696  $testsettings = [
5697  'questionSetType' => $main_settings->getGeneralSettings()->getQuestionSetType(),
5698  'Anonymity' => (int) $main_settings->getGeneralSettings()->getAnonymity(),
5699 
5700  'activation_limited' => $this->isActivationLimited(),
5701  'activation_start_time' => $this->getActivationStartingTime(),
5702  'activation_end_time' => $this->getActivationEndingTime(),
5703  'activation_visibility' => $this->getActivationVisibility(),
5704 
5705  'IntroEnabled' => (int) $main_settings->getIntroductionSettings()->getIntroductionEnabled(),
5706  'ExamConditionsCheckboxEnabled' => (int) $main_settings->getIntroductionSettings()->getExamConditionsCheckboxEnabled(),
5707 
5708  'StartingTimeEnabled' => (int) $main_settings->getAccessSettings()->getStartTimeEnabled(),
5709  'StartingTime' => $main_settings->getAccessSettings()->getStartTime(),
5710  'EndingTimeEnabled' => (int) $main_settings->getAccessSettings()->getEndTimeEnabled(),
5711  'EndingTime' => $main_settings->getAccessSettings()->getEndTime(),
5712  'password_enabled' => (int) $main_settings->getAccessSettings()->getPasswordEnabled(),
5713  'password' => $main_settings->getAccessSettings()->getPassword(),
5714  'fixed_participants' => (int) $main_settings->getAccessSettings()->getFixedParticipants(),
5715 
5716  'NrOfTries' => $main_settings->getTestBehaviourSettings()->getNumberOfTries(),
5717  'BlockAfterPassed' => (int) $main_settings->getTestBehaviourSettings()->getBlockAfterPassedEnabled(),
5718  'pass_waiting' => $main_settings->getTestBehaviourSettings()->getPassWaiting(),
5719  'EnableProcessingTime' => (int) $main_settings->getTestBehaviourSettings()->getProcessingTimeEnabled(),
5720  'ProcessingTime' => $main_settings->getTestBehaviourSettings()->getProcessingTime(),
5721  'ResetProcessingTime' => $main_settings->getTestBehaviourSettings()->getResetProcessingTime(),
5722  'Kiosk' => $main_settings->getTestBehaviourSettings()->getKioskMode(),
5723  'examid_in_test_pass' => (int) $main_settings->getTestBehaviourSettings()->getExamIdInTestAttemptEnabled(),
5724 
5725  'TitleOutput' => $main_settings->getQuestionBehaviourSettings()->getQuestionTitleOutputMode(),
5726  'autosave' => (int) $main_settings->getQuestionBehaviourSettings()->getAutosaveEnabled(),
5727  'autosave_ival' => $main_settings->getQuestionBehaviourSettings()->getAutosaveInterval(),
5728  'Shuffle' => (int) $main_settings->getQuestionBehaviourSettings()->getShuffleQuestions(),
5729  'offer_question_hints' => (int) $main_settings->getQuestionBehaviourSettings()->getQuestionHintsEnabled(),
5730  'AnswerFeedbackPoints' => (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackPointsEnabled(),
5731  'AnswerFeedback' => (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackGenericEnabled(),
5732  'SpecificAnswerFeedback' => (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackSpecificEnabled(),
5733  'InstantFeedbackSolution' => (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackSolutionEnabled(),
5734  'force_inst_fb' => (int) $main_settings->getQuestionBehaviourSettings()->getForceInstantFeedbackOnNextQuestion(),
5735  'follow_qst_answer_fixation' => (int) $main_settings->getQuestionBehaviourSettings()->getLockAnswerOnNextQuestionEnabled(),
5736  'inst_fb_answer_fixation' => (int) $main_settings->getQuestionBehaviourSettings()->getLockAnswerOnInstantFeedbackEnabled(),
5737 
5738  'use_previous_answers' => (int) $main_settings->getParticipantFunctionalitySettings()->getUsePreviousAnswerAllowed(),
5739  'ShowCancel' => (int) $main_settings->getParticipantFunctionalitySettings()->getSuspendTestAllowed(),
5740  'SequenceSettings' => (int) $main_settings->getParticipantFunctionalitySettings()->getPostponedQuestionsMoveToEnd(),
5741  'ListOfQuestionsSettings' => $main_settings->getParticipantFunctionalitySettings()->getUsrPassOverviewMode(),
5742  'ShowMarker' => (int) $main_settings->getParticipantFunctionalitySettings()->getQuestionMarkingEnabled(),
5743 
5744  'enable_examview' => $main_settings->getFinishingSettings()->getShowAnswerOverview(),
5745  'ShowFinalStatement' => (int) $main_settings->getFinishingSettings()->getConcludingRemarksEnabled(),
5746  'redirection_mode' => $main_settings->getFinishingSettings()->getRedirectionMode(),
5747  'redirection_url' => $main_settings->getFinishingSettings()->getRedirectionUrl(),
5748  'mailnotification' => $main_settings->getFinishingSettings()->getMailNotificationContentType(),
5749  'mailnottype' => (int) $main_settings->getFinishingSettings()->getAlwaysSendMailNotification(),
5750 
5751  'skill_service' => (int) $main_settings->getAdditionalSettings()->getSkillsServiceEnabled(),
5752 
5753  'PassScoring' => $score_settings->getScoringSettings()->getPassScoring(),
5754  'ScoreCutting' => $score_settings->getScoringSettings()->getScoreCutting(),
5755  'CountSystem' => $score_settings->getScoringSettings()->getCountSystem(),
5756 
5757  'ScoreReporting' => $score_settings->getResultSummarySettings()->getScoreReporting()->value,
5758  'ReportingDate' => $score_settings->getResultSummarySettings()->getReportingDate(),
5759  'pass_deletion_allowed' => (int) $score_settings->getResultSummarySettings()->getPassDeletionAllowed(),
5760  'show_grading_status' => (int) $score_settings->getResultSummarySettings()->getShowGradingStatusEnabled(),
5761  'show_grading_mark' => (int) $score_settings->getResultSummarySettings()->getShowGradingMarkEnabled(),
5762 
5763  'ResultsPresentation' => $score_settings->getResultDetailsSettings()->getResultsPresentation(),
5764  'show_solution_list_comparison' => (int) $score_settings->getResultDetailsSettings()->getShowSolutionListComparison(),
5765  'examid_in_test_res' => (int) $score_settings->getResultDetailsSettings()->getShowExamIdInTestResults(),
5766 
5767  'highscore_enabled' => (int) $score_settings->getGamificationSettings()->getHighscoreEnabled(),
5768  'highscore_anon' => (int) $score_settings->getGamificationSettings()->getHighscoreAnon(),
5769  'highscore_achieved_ts' => $score_settings->getGamificationSettings()->getHighscoreAchievedTS(),
5770  'highscore_score' => $score_settings->getGamificationSettings()->getHighscoreScore(),
5771  'highscore_percentage' => $score_settings->getGamificationSettings()->getHighscorePercentage(),
5772  'highscore_hints' => $score_settings->getGamificationSettings()->getHighscoreHints(),
5773  'highscore_wtime' => $score_settings->getGamificationSettings()->getHighscoreWTime(),
5774  'highscore_own_table' => $score_settings->getGamificationSettings()->getHighscoreOwnTable(),
5775  'highscore_top_table' => $score_settings->getGamificationSettings()->getHighscoreTopTable(),
5776  'highscore_top_num' => $score_settings->getGamificationSettings()->getHighscoreTopNum(),
5777 
5778  'HideInfoTab' => (int) $main_settings->getAdditionalSettings()->getHideInfoTab(),
5779  ];
5780 
5781  $marks = array_map(
5782  fn(Mark $v): array => [
5783  'short_name' => $v->getShortName(),
5784  'official_name' => $v->getOfficialName(),
5785  'minimum_level' => $v->getMinimumLevel(),
5786  'passed' => $v->getPassed()
5787  ],
5788  $this->getMarkSchema()->getMarkSteps()
5789  );
5790 
5791  $next_id = $this->db->nextId('tst_test_defaults');
5792  $this->db->insert(
5793  'tst_test_defaults',
5794  [
5795  'test_defaults_id' => ['integer', $next_id],
5796  'name' => ['text', $a_name],
5797  'user_fi' => ['integer', $this->user->getId()],
5798  'defaults' => ['clob', serialize($testsettings)],
5799  'marks' => ['clob', json_encode($marks)],
5800  'tstamp' => ['integer', time()]
5801  ]
5802  );
5803  }
5804 
5805  public function applyDefaults(array $test_defaults): string
5806  {
5807  $testsettings = unserialize($test_defaults['defaults'], ['allowed_classes' => [DateTimeImmutable::class]]);
5808  $activation_starting_time = is_numeric($testsettings['activation_starting_time'] ?? false)
5809  ? (int) $testsettings['activation_starting_time']
5810  : null;
5811  $activation_ending_time = is_numeric($testsettings['activation_ending_time'] ?? false)
5812  ? (int) $testsettings['activation_ending_time']
5813  : null;
5814  $unserialized_marks = json_decode($test_defaults['marks'], true);
5815 
5816  $info = '';
5817  if (is_array($unserialized_marks)
5818  && is_array($unserialized_marks[0])) {
5819  $this->mark_schema = $this->getMarkSchema()->withMarkSteps(
5820  array_map(
5821  fn(array $v): Mark => new Mark(
5822  $v['short_name'],
5823  $v['official_name'],
5824  $v['minimum_level'],
5825  $v['passed']
5826  ),
5827  $unserialized_marks
5828  )
5829  );
5830  } else {
5831  $info = 'old_mark_default_not_applied';
5832  }
5833 
5834 
5835  $this->storeActivationSettings(
5836  (bool) ($testsettings['is_activation_limited'] ?? false),
5837  $activation_starting_time,
5838  $activation_ending_time,
5839  (bool) ($testsettings['activation_visibility'] ?? false),
5840  );
5841 
5842  $main_settings = $this->getMainSettings();
5843 
5844  $general_settings = $main_settings->getGeneralSettings();
5845  $introduction_settings = $main_settings->getIntroductionSettings();
5846  $access_settings = $main_settings->getAccessSettings();
5847  $test_behavior_settings = $main_settings->getTestBehaviourSettings();
5848  $question_behavior_settings = $main_settings->getQuestionBehaviourSettings();
5849  $participant_functionality_settings = $main_settings->getParticipantFunctionalitySettings();
5850  $finishing_settings = $main_settings->getFinishingSettings();
5851  $additional_settings = $main_settings->getAdditionalSettings();
5852 
5853  $main_settings = $main_settings
5855  $general_settings
5856  ->withQuestionSetType(
5857  $testsettings['questionSetType'] ?? $general_settings->getQuestionSetType()
5858  )->withAnonymity(
5859  (bool) ($testsettings['Anonymity'] ?? $general_settings->getAnonymity())
5860  )
5861  )->withIntroductionSettings(
5862  $introduction_settings
5863  ->withIntroductionEnabled(
5864  (bool) $testsettings['IntroEnabled'] ?? $introduction_settings->getIntroductionEnabled()
5865  )->withExamConditionsCheckboxEnabled(
5866  (bool) ($testsettings['ExamConditionsCheckboxEnabled'] ?? $introduction_settings->getExamConditionsCheckboxEnabled())
5867  )
5868  )->withAccessSettings(
5869  $access_settings
5870  ->withStartTimeEnabled(
5871  (bool) ($testsettings['StartingTimeEnabled'] ?? $access_settings->getStartTimeEnabled())
5872  )->withStartTime(
5874  $testsettings['StartingTime'] ?? $access_settings->getStartTime()
5875  )
5876  )->withEndTimeEnabled(
5877  (bool) $testsettings['EndingTimeEnabled'] ?? $access_settings->getEndTimeEnabled()
5878  )->withEndTime(
5880  $testsettings['EndingTime'] ?? $access_settings->getEndTime()
5881  )
5882  )->withPasswordEnabled(
5883  (bool) ($testsettings['password_enabled'] ?? $access_settings->getPasswordEnabled())
5884  )->withPassword(
5885  $testsettings['password'] ?? $access_settings->getPassword()
5886  )->withFixedParticipants(
5887  (bool) ($testsettings['fixed_participants'] ?? $access_settings->getFixedParticipants())
5888  )
5889  )->withTestBehaviourSettings(
5890  $test_behavior_settings
5891  ->withNumberOfTries(
5892  (int) ($testsettings['NrOfTries'] ?? $test_behavior_settings->getNumberOfTries())
5893  )->withBlockAfterPassedEnabled(
5894  (bool) ($testsettings['BlockAfterPassed'] ?? $test_behavior_settings->getBlockAfterPassedEnabled())
5895  )->withPassWaiting(
5896  $testsettings['pass_waiting'] ?? $test_behavior_settings->getPassWaiting()
5897  )->withKioskMode(
5898  $testsettings['Kiosk'] ?? $test_behavior_settings->getKioskMode()
5899  )->withProcessingTimeEnabled(
5900  (bool) ($testsettings['EnableProcessingTime'] ?? $test_behavior_settings->getProcessingTimeEnabled())
5901  )->withProcessingTime(
5902  $testsettings['ProcessingTime'] ?? $test_behavior_settings->getProcessingTime()
5903  )->withResetProcessingTime(
5904  (bool) ($testsettings['ResetProcessingTime'] ?? $test_behavior_settings->getResetProcessingTime())
5905  )->withExamIdInTestAttemptEnabled(
5906  (bool) ($testsettings['examid_in_test_pass'] ?? $test_behavior_settings->getExamIdInTestAttemptEnabled())
5907  )
5908  )->withQuestionBehaviourSettings(
5909  $question_behavior_settings
5910  ->withQuestionTitleOutputMode(
5911  $testsettings['TitleOutput'] ?? $question_behavior_settings->getQuestionTitleOutputMode()
5912  )->withAutosaveEnabled(
5913  (bool) ($testsettings['autosave'] ?? $question_behavior_settings->getAutosaveEnabled())
5914  )->withAutosaveInterval(
5915  $testsettings['autosave_ival'] ?? $question_behavior_settings->getAutosaveInterval()
5916  )->withShuffleQuestions(
5917  (bool) ($testsettings['Shuffle'] ?? $question_behavior_settings->getShuffleQuestions())
5918  )->withQuestionHintsEnabled(
5919  (bool) ($testsettings['offer_question_hints'] ?? $question_behavior_settings->getQuestionHintsEnabled())
5920  )->withInstantFeedbackPointsEnabled(
5921  (bool) ($testsettings['AnswerFeedbackPoints'] ?? $question_behavior_settings->getInstantFeedbackPointsEnabled())
5922  )->withInstantFeedbackGenericEnabled(
5923  (bool) ($testsettings['AnswerFeedback'] ?? $question_behavior_settings->getInstantFeedbackGenericEnabled())
5924  )->withInstantFeedbackSpecificEnabled(
5925  (bool) ($testsettings['SpecificAnswerFeedback'] ?? $question_behavior_settings->getInstantFeedbackSpecificEnabled())
5926  )->withInstantFeedbackSolutionEnabled(
5927  (bool) ($testsettings['InstantFeedbackSolution'] ?? $question_behavior_settings->getInstantFeedbackSolutionEnabled())
5928  )->withForceInstantFeedbackOnNextQuestion(
5929  (bool) ($testsettings['force_inst_fb'] ?? $question_behavior_settings->getForceInstantFeedbackOnNextQuestion())
5930  )->withLockAnswerOnInstantFeedbackEnabled(
5931  (bool) ($testsettings['inst_fb_answer_fixation'] ?? $question_behavior_settings->getLockAnswerOnInstantFeedbackEnabled())
5932  )->withLockAnswerOnNextQuestionEnabled(
5933  (bool) ($testsettings['follow_qst_answer_fixation'] ?? $question_behavior_settings->getLockAnswerOnNextQuestionEnabled())
5934  )
5935  )->withParticipantFunctionalitySettings(
5936  $participant_functionality_settings
5937  ->withUsePreviousAnswerAllowed(
5938  (bool) ($testsettings['use_previous_answers'] ?? $participant_functionality_settings->getUsePreviousAnswerAllowed())
5939  )->withSuspendTestAllowed(
5940  (bool) ($testsettings['ShowCancel'] ?? $participant_functionality_settings->getSuspendTestAllowed())
5941  )->withPostponedQuestionsMoveToEnd(
5942  (bool) ($testsettings['SequenceSettings'] ?? $participant_functionality_settings->getPostponedQuestionsMoveToEnd())
5943  )->withUsrPassOverviewMode(
5944  (int) ($testsettings['ListOfQuestionsSettings'] ?? $participant_functionality_settings->getUsrPassOverviewMode())
5945  )->withQuestionMarkingEnabled(
5946  (bool) ($testsettings['ShowMarker'] ?? $participant_functionality_settings->getQuestionMarkingEnabled())
5947  )
5948  )->withFinishingSettings(
5949  $finishing_settings
5950  ->withShowAnswerOverview(
5951  (bool) ($testsettings['enable_examview'] ?? $finishing_settings->getShowAnswerOverview())
5952  )->withConcludingRemarksEnabled(
5953  (bool) ($testsettings['ShowFinalStatement'] ?? $finishing_settings->getConcludingRemarksEnabled())
5954  )->withRedirectionMode(
5955  (int) ($testsettings['redirection_mode'] ?? $finishing_settings->getRedirectionMode())
5956  )->withRedirectionUrl(
5957  $testsettings['redirection_url'] ?? $finishing_settings->getRedirectionUrl()
5958  )->withMailNotificationContentType(
5959  (int) ($testsettings['mailnotification'] ?? $finishing_settings->getMailNotificationContentType())
5960  )->withAlwaysSendMailNotification(
5961  (bool) ($testsettings['mailnottype'] ?? $finishing_settings->getAlwaysSendMailNotification())
5962  )
5963  )->withAdditionalSettings(
5964  $additional_settings
5965  ->withSkillsServiceEnabled(
5966  (bool) ($testsettings['skill_service'] ?? $additional_settings->getSkillsServiceEnabled())
5967  )->withHideInfoTab(
5968  (bool) ($testsettings['HideInfoTab'] ?? $additional_settings->getHideInfoTab())
5969  )
5970  );
5971 
5972  $this->getMainSettingsRepository()->store($main_settings);
5973 
5974  $score_reporting = ScoreReportingTypes::SCORE_REPORTING_DISABLED;
5975  if ($testsettings['ScoreReporting'] !== null) {
5976  $score_reporting = ScoreReportingTypes::tryFrom($testsettings['ScoreReporting'])
5977  ?? ScoreReportingTypes::SCORE_REPORTING_DISABLED;
5978  }
5979 
5980  $reporting_date = $testsettings['ReportingDate'];
5981  if (is_string($reporting_date)) {
5982  $reporting_date = new DateTimeImmutable($testsettings['ReportingDate'], new DateTimeZone('UTC'));
5983  }
5984 
5985  $score_settings = $this->getScoreSettings();
5986 
5987  $scoring_settings = $score_settings->getScoringSettings();
5988  $result_summary_settings = $score_settings->getResultSummarySettings();
5989  $result_details_settings = $score_settings->getResultDetailsSettings();
5990  $gamification_settings = $score_settings->getGamificationSettings();
5991 
5992  $score_settings = $score_settings
5994  $scoring_settings
5995  ->withPassScoring(
5996  $testsettings['PassScoring'] ?? $scoring_settings->getPassScoring()
5997  )->withScoreCutting(
5998  $testsettings['ScoreCutting'] ?? $scoring_settings->getScoreCutting()
5999  )->withCountSystem(
6000  $testsettings['CountSystem'] ?? $scoring_settings->getCountSystem()
6001  )
6002  )->withResultSummarySettings(
6003  $result_summary_settings
6004  ->withPassDeletionAllowed(
6005  (bool) ($testsettings['pass_deletion_allowed'] ?? $result_summary_settings->getPassDeletionAllowed())
6006  )->withShowGradingStatusEnabled(
6007  (bool) ($testsettings['show_grading_status'] ?? $result_summary_settings->getShowGradingStatusEnabled())
6008  )->withShowGradingMarkEnabled(
6009  (bool) ($testsettings['show_grading_mark'] ?? $result_summary_settings->getShowGradingMarkEnabled())
6010  )->withScoreReporting(
6011  $score_reporting
6012  )->withReportingDate(
6013  $reporting_date
6014  )
6015  )->withResultDetailsSettings(
6016  $result_details_settings
6017  ->withResultsPresentation(
6018  (int) ($testsettings['ResultsPresentation'] ?? $result_details_settings->getResultsPresentation())
6019  )->withShowSolutionListComparison(
6020  (bool) ($testsettings['show_solution_list_comparison'] ?? $result_details_settings->getShowSolutionListComparison())
6021  )->withShowExamIdInTestResults(
6022  (bool) ($testsettings['examid_in_test_res'] ?? $result_details_settings->getShowExamIdInTestResults())
6023  )
6024  )->withGamificationSettings(
6025  $gamification_settings
6026  ->withHighscoreEnabled(
6027  (bool) ($testsettings['highscore_enabled'] ?? $gamification_settings->getHighscoreEnabled())
6028  )->withHighscoreAnon(
6029  (bool) ($testsettings['highscore_anon'] ?? $gamification_settings->getHighscoreAnon())
6030  )->withHighscoreAchievedTS(
6031  $testsettings['highscore_achieved_ts'] ?? $gamification_settings->getHighscoreAchievedTS()
6032  )->withHighscoreScore(
6033  (bool) ($testsettings['highscore_score'] ?? $gamification_settings->getHighscoreScore())
6034  )->withHighscorePercentage(
6035  $testsettings['highscore_percentage'] ?? $gamification_settings->getHighscorePercentage()
6036  )->withHighscoreHints(
6037  (bool) ($testsettings['highscore_hints'] ?? $gamification_settings->getHighscoreHints())
6038  )->withHighscoreWTime(
6039  (bool) ($testsettings['highscore_wtime'] ?? $gamification_settings->getHighscoreWTime())
6040  )->withHighscoreOwnTable(
6041  (bool) ($testsettings['highscore_own_table'] ?? $gamification_settings->getHighscoreOwnTable())
6042  )->withHighscoreTopTable(
6043  (bool) ($testsettings['highscore_top_table'] ?? $gamification_settings->getHighscoreTopTable())
6044  )->withHighscoreTopNum(
6045  $testsettings['highscore_top_num'] ?? $gamification_settings->getHighscoreTopNum()
6046  )
6047  )
6048  ;
6049  $this->getScoreSettingsRepository()->store($score_settings);
6050  $this->saveToDb();
6051 
6052  return $info;
6053  }
6054 
6056  DateTimeImmutable|int|null $date_time
6057  ): ?DateTimeImmutable {
6058  if ($date_time === null || $date_time instanceof DateTimeImmutable) {
6059  return $date_time;
6060  }
6061 
6062  return DateTimeImmutable::createFromFormat('U', (string) $date_time);
6063  }
6064 
6072  public function processPrintoutput2FO($print_output): string
6073  {
6074  if (extension_loaded("tidy")) {
6075  $config = [
6076  "indent" => false,
6077  "output-xml" => true,
6078  "numeric-entities" => true
6079  ];
6080  $tidy = new tidy();
6081  $tidy->parseString($print_output, $config, 'utf8');
6082  $tidy->cleanRepair();
6083  $print_output = tidy_get_output($tidy);
6084  $print_output = preg_replace("/^.*?(<html)/", "\\1", $print_output);
6085  } else {
6086  $print_output = str_replace("&nbsp;", "&#160;", $print_output);
6087  $print_output = str_replace("&otimes;", "X", $print_output);
6088  }
6089  $xsl = file_get_contents("./components/ILIAS/Test/xml/question2fo.xsl");
6090 
6091  // additional font support
6092 
6093  $xsl = str_replace(
6094  'font-family="Helvetica, unifont"',
6095  'font-family="' . $this->settings->get('rpc_pdf_font', 'Helvetica, unifont') . '"',
6096  $xsl
6097  );
6098 
6099  $args = [ '/_xml' => $print_output, '/_xsl' => $xsl ];
6100  $xh = xslt_create();
6101  $params = [];
6102  $output = xslt_process($xh, "arg:/_xml", "arg:/_xsl", null, $args, $params);
6103  xslt_error($xh);
6104  xslt_free($xh);
6105  return $output;
6106  }
6107 
6114  public function deliverPDFfromHTML($content, $title = null)
6115  {
6116  $content = preg_replace("/href=\".*?\"/", "", $content);
6117  $printbody = new ilTemplate("tpl.il_as_tst_print_body.html", true, true, "components/ILIAS/Test");
6118  $printbody->setVariable("TITLE", ilLegacyFormElementsUtil::prepareFormOutput($this->getTitle()));
6119  $printbody->setVariable("ADM_CONTENT", $content);
6120  $printbody->setCurrentBlock("css_file");
6121  $printbody->setVariable("CSS_FILE", ilUtil::getStyleSheetLocation("filesystem", "delos.css"));
6122  $printbody->parseCurrentBlock();
6123  $printoutput = $printbody->get();
6124  $html = str_replace("href=\"./", "href=\"" . ILIAS_HTTP_PATH . "/", $printoutput);
6125  $html = preg_replace("/<div id=\"dontprint\">.*?<\\/div>/ims", "", $html);
6126  if (extension_loaded("tidy")) {
6127  $config = [
6128  "indent" => false,
6129  "output-xml" => true,
6130  "numeric-entities" => true
6131  ];
6132  $tidy = new tidy();
6133  $tidy->parseString($html, $config, 'utf8');
6134  $tidy->cleanRepair();
6135  $html = tidy_get_output($tidy);
6136  $html = preg_replace("/^.*?(<html)/", "\\1", $html);
6137  } else {
6138  $html = str_replace("&nbsp;", "&#160;", $html);
6139  $html = str_replace("&otimes;", "X", $html);
6140  }
6141  $html = preg_replace("/src=\".\\//ims", "src=\"" . ILIAS_HTTP_PATH . "/", $html);
6142  $this->deliverPDFfromFO($this->processPrintoutput2FO($html), $title);
6143  }
6144 
6150  public function deliverPDFfromFO($fo, $title = null): bool
6151  {
6152  $fo_file = ilFileUtils::ilTempnam() . ".fo";
6153  $fp = fopen($fo_file, "w");
6154  fwrite($fp, $fo);
6155  fclose($fp);
6156 
6157  try {
6158  $pdf_base64 = ilRpcClientFactory::factory('RPCTransformationHandler')->ilFO2PDF($fo);
6159  $filename = (strlen($title)) ? $title : $this->getTitle();
6162  $pdf_base64->scalar,
6164  "application/pdf"
6165  );
6166  return true;
6167  } catch (Exception $e) {
6168  $this->logger->info(__METHOD__ . ': ' . $e->getMessage());
6169  return false;
6170  }
6171  }
6172 
6182  public static function getManualFeedback(int $active_id, int $question_id, ?int $pass): string
6183  {
6184  if ($pass === null) {
6185  return '';
6186  }
6187  $feedback = '';
6188  $row = self::getSingleManualFeedback((int) $active_id, (int) $question_id, (int) $pass);
6189 
6190  if ($row !== [] && ($row['finalized_evaluation'] || \ilTestService::isManScoringDone((int) $active_id))) {
6191  $feedback = $row['feedback'] ?? '';
6192  }
6193 
6194  return $feedback;
6195  }
6196 
6197  public static function getSingleManualFeedback(int $active_id, int $question_id, int $pass): array
6198  {
6199  global $DIC;
6200  $ilDB = $DIC['ilDB'];
6201  $row = [];
6202  $result = $ilDB->queryF(
6203  "SELECT * FROM tst_manual_fb WHERE active_fi = %s AND question_fi = %s AND pass = %s",
6204  ['integer', 'integer', 'integer'],
6205  [$active_id, $question_id, $pass]
6206  );
6207 
6208  if ($ilDB->numRows($result) === 1) {
6209  $row = $ilDB->fetchAssoc($result);
6210  $row['feedback'] = ilRTE::_replaceMediaObjectImageSrc($row['feedback'] ?? '', 1);
6211  } elseif ($ilDB->numRows($result) > 1) {
6212  $DIC->logger()->root()->warning(
6213  "WARNING: Multiple feedback entries on tst_manual_fb for " .
6214  "active_fi = $active_id , question_fi = $question_id and pass = $pass"
6215  );
6216  }
6217 
6218  return $row;
6219  }
6220 
6228  public function getCompleteManualFeedback(int $question_id): array
6229  {
6230  global $DIC;
6231  $ilDB = $DIC['ilDB'];
6232 
6233  $feedback = [];
6234  $result = $ilDB->queryF(
6235  "SELECT * FROM tst_manual_fb WHERE question_fi = %s",
6236  ['integer'],
6237  [$question_id]
6238  );
6239 
6240  while ($row = $ilDB->fetchAssoc($result)) {
6241  $active = $row['active_fi'];
6242  $pass = $row['pass'];
6243  $question = $row['question_fi'];
6244 
6245  $row['feedback'] = ilRTE::_replaceMediaObjectImageSrc($row['feedback'] ?? '', 1);
6246 
6247  $feedback[$active][$pass][$question] = $row;
6248  }
6249 
6250  return $feedback;
6251  }
6252 
6253  public function saveManualFeedback(
6254  int $active_id,
6255  int $question_id,
6256  int $pass,
6257  ?string $feedback,
6258  bool $finalized = false
6259  ): void {
6260  $feedback_old = self::getSingleManualFeedback($active_id, $question_id, $pass);
6261  $this->db->manipulateF(
6262  'DELETE FROM tst_manual_fb WHERE active_fi = %s AND question_fi = %s AND pass = %s',
6263  ['integer', 'integer', 'integer'],
6264  [$active_id, $question_id, $pass]
6265  );
6266 
6267  $this->insertManualFeedback($active_id, $question_id, $pass, $feedback, $finalized, $feedback_old);
6268 
6269  }
6270 
6271  private function insertManualFeedback(
6272  int $active_id,
6273  int $question_id,
6274  int $pass,
6275  ?string $feedback,
6276  bool $finalized,
6277  array $feedback_old
6278  ): void {
6279  $next_id = $this->db->nextId('tst_manual_fb');
6280  $user = $this->user->getId();
6281  $finalized_time = time();
6282 
6283  $update_default = [
6284  'manual_feedback_id' => [ 'integer', $next_id],
6285  'active_fi' => [ 'integer', $active_id],
6286  'question_fi' => [ 'integer', $question_id],
6287  'pass' => [ 'integer', $pass],
6288  'feedback' => [ 'clob', $feedback ? ilRTE::_replaceMediaObjectImageSrc($feedback, 0) : null],
6289  'tstamp' => [ 'integer', time()]
6290  ];
6291 
6292  if ($feedback_old !== [] && (int) $feedback_old['finalized_evaluation'] === 1) {
6293  $user = $feedback_old['finalized_by_usr_id'];
6294  $finalized_time = $feedback_old['finalized_tstamp'];
6295  }
6296 
6297  if ($finalized === false) {
6298  $update_default['finalized_evaluation'] = ['integer', 0];
6299  $update_default['finalized_by_usr_id'] = ['integer', 0];
6300  $update_default['finalized_tstamp'] = ['integer', 0];
6301  } elseif ($finalized === true) {
6302  $update_default['finalized_evaluation'] = ['integer', 1];
6303  $update_default['finalized_by_usr_id'] = ['integer', $user];
6304  $update_default['finalized_tstamp'] = ['integer', $finalized_time];
6305  }
6306 
6307  $this->db->insert('tst_manual_fb', $update_default);
6308 
6309  if ($this->logger->isLoggingEnabled()) {
6310  $this->logger->logScoringInteraction(
6311  $this->logger->getInteractionFactory()->buildScoringInteraction(
6312  $this->getRefId(),
6313  $question_id,
6314  $this->user->getId(),
6315  self::_getUserIdFromActiveId($active_id),
6316  TestScoringInteractionTypes::QUESTION_GRADED,
6317  [
6318  AdditionalInformationGenerator::KEY_EVAL_FINALIZED => $this->logger
6319  ->getAdditionalInformationGenerator()->getTrueFalseTagForBool($finalized),
6320  AdditionalInformationGenerator::KEY_FEEDBACK => $feedback ? ilRTE::_replaceMediaObjectImageSrc($feedback, 0) : ''
6321  ]
6322  )
6323  );
6324  }
6325  }
6326 
6334  public function getJavaScriptOutput(): bool
6335  {
6336  return true;
6337  }
6338 
6339  public function &createTestSequence($active_id, $pass, $shuffle)
6340  {
6341  $this->test_sequence = new ilTestSequence($active_id, $pass, $this->isRandomTest(), $this->questionrepository);
6342  }
6343 
6349  public function setTestId($a_id)
6350  {
6351  $this->test_id = $a_id;
6352  }
6353 
6361  public function getDetailedTestResults($participants): array
6362  {
6363  $results = [];
6364  if (count($participants)) {
6365  foreach ($participants as $active_id => $user_rec) {
6366  $row = [];
6367  $reached_points = 0;
6368  $max_points = 0;
6369  $pass = ilObjTest::_getResultPass($active_id);
6370  foreach ($this->questions as $value) {
6371  $question = ilObjTest::_instanciateQuestion($value);
6372  if (is_object($question)) {
6373  $max_points += $question->getMaximumPoints();
6374  $reached_points += $question->getReachedPoints($active_id, $pass);
6375  if ($max_points > 0) {
6376  $percentvalue = $reached_points / $max_points;
6377  if ($percentvalue < 0) {
6378  $percentvalue = 0.0;
6379  }
6380  } else {
6381  $percentvalue = 0;
6382  }
6383  if ($this->getAnonymity()) {
6384  $user_rec['firstname'] = "";
6385  $user_rec['lastname'] = $this->lng->txt("anonymous");
6386  }
6387  $results[] = [
6388  "user_id" => $user_rec['usr_id'],
6389  "matriculation" => $user_rec['matriculation'],
6390  "lastname" => $user_rec['lastname'],
6391  "firstname" => $user_rec['firstname'],
6392  "login" => $user_rec['login'],
6393  "question_id" => $question->getId(),
6394  "question_title" => $question->getTitle(),
6395  "reached_points" => $reached_points,
6396  "max_points" => $max_points,
6397  "passed" => $user_rec['passed'] ? '1' : '0',
6398  ];
6399  }
6400  }
6401  }
6402  }
6403  return $results;
6404  }
6405 
6409  public static function _lookupTestObjIdForQuestionId(int $q_id): ?int
6410  {
6411  global $DIC;
6412  $ilDB = $DIC['ilDB'];
6413 
6414  $result = $ilDB->queryF(
6415  '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',
6416  ['integer'],
6417  [$q_id]
6418  );
6419  $rec = $ilDB->fetchAssoc($result);
6420  return $rec['obj_id'] ?? null;
6421  }
6422 
6429  public function isPluginActive($a_pname): bool
6430  {
6431  if (!$this->component_repository->getComponentByTypeAndName(
6433  'TestQuestionPool'
6434  )->getPluginSlotById('qst')->hasPluginName($a_pname)) {
6435  return false;
6436  }
6437 
6438  return $this->component_repository
6439  ->getComponentByTypeAndName(
6441  'TestQuestionPool'
6442  )
6443  ->getPluginSlotById(
6444  'qst'
6445  )
6446  ->getPluginByName(
6447  $a_pname
6448  )->isActive();
6449  }
6450 
6451  public function getPassed($active_id)
6452  {
6453  $result = $this->db->queryF(
6454  "SELECT passed FROM tst_result_cache WHERE active_fi = %s",
6455  ['integer'],
6456  [$active_id]
6457  );
6458  if ($result->numRows()) {
6459  $row = $this->db->fetchAssoc($result);
6460  return $row['passed'];
6461  } else {
6462  $counted_pass = ilObjTest::_getResultPass($active_id);
6463  $result_array = &$this->getTestResult($active_id, $counted_pass);
6464  return $result_array["test"]["passed"];
6465  }
6466  }
6467 
6471  public function getParticipantsForTestAndQuestion($test_id, $question_id): array
6472  {
6473  $query = "
6474  SELECT tst_test_result.active_fi, tst_test_result.question_fi, tst_test_result.pass
6475  FROM tst_test_result
6476  INNER JOIN tst_active ON tst_active.active_id = tst_test_result.active_fi AND tst_active.test_fi = %s
6477  INNER JOIN qpl_questions ON qpl_questions.question_id = tst_test_result.question_fi
6478  LEFT JOIN usr_data ON usr_data.usr_id = tst_active.user_fi
6479  WHERE tst_test_result.question_fi = %s
6480  ORDER BY usr_data.lastname ASC, usr_data.firstname ASC
6481  ";
6482 
6483  $result = $this->db->queryF(
6484  $query,
6485  ['integer', 'integer'],
6486  [$test_id, $question_id]
6487  );
6488  $foundusers = [];
6490  while ($row = $this->db->fetchAssoc($result)) {
6491  if ($this->getAccessFilteredParticipantList() && !$this->getAccessFilteredParticipantList()->isActiveIdInList($row["active_fi"])) {
6492  continue;
6493  }
6494 
6495  if (!array_key_exists($row["active_fi"], $foundusers)) {
6496  $foundusers[$row["active_fi"]] = [];
6497  }
6498  array_push($foundusers[$row["active_fi"]], ["pass" => $row["pass"], "qid" => $row["question_fi"]]);
6499  }
6500  return $foundusers;
6501  }
6502 
6503  public function getAggregatedResultsData(): array
6504  {
6505  $data = $this->getCompleteEvaluationData();
6506  $found_participants = $data->getParticipants();
6507  $results = ['overview' => [], 'questions' => []];
6508  if ($found_participants !== []) {
6509  $results['overview']['tst_stat_result_mark_median'] = $data->getStatistics()->getEvaluationDataOfMedianUser()?->getMark()?->getShortName() ?? '';
6510  $results['overview']['tst_stat_result_rank_median'] = $data->getStatistics()->rankMedian();
6511  $results['overview']['tst_stat_result_total_participants'] = $data->getStatistics()->count();
6512  $results['overview']['tst_stat_result_median'] = $data->getStatistics()->median();
6513  $results['overview']['tst_eval_total_persons'] = count($found_participants);
6514  $total_finished = $data->getTotalFinishedParticipants();
6515  $results['overview']['tst_eval_total_finished'] = $total_finished;
6516  $results['overview']['tst_eval_total_finished_average_time'] =
6518  $this->evalTotalStartedAverageTime($data->getParticipantIds())
6519  );
6520  $total_passed = 0;
6521  $total_passed_reached = 0;
6522  $total_passed_max = 0;
6523  $total_passed_time = 0;
6524  foreach ($found_participants as $userdata) {
6525  if ($userdata->getMark()?->getPassed()) {
6526  $total_passed++;
6527  $total_passed_reached += $userdata->getReached();
6528  $total_passed_max += $userdata->getMaxpoints();
6529  $total_passed_time += $userdata->getTimeOnTask();
6530  }
6531  }
6532  $average_passed_reached = $total_passed ? $total_passed_reached / $total_passed : 0;
6533  $average_passed_max = $total_passed ? $total_passed_max / $total_passed : 0;
6534  $average_passed_time = $total_passed ? $total_passed_time / $total_passed : 0;
6535  $results['overview']['tst_eval_total_passed'] = $total_passed;
6536  $results['overview']['tst_eval_total_passed_average_points'] = sprintf('%2.2f', $average_passed_reached)
6537  . ' ' . strtolower('of') . ' ' . sprintf('%2.2f', $average_passed_max);
6538  $results['overview']['tst_eval_total_passed_average_time'] =
6539  $this->secondsToHoursMinutesSecondsString($average_passed_time);
6540  }
6541 
6542  foreach ($data->getQuestionTitles() as $question_id => $question_title) {
6543  $answered = 0;
6544  $reached = 0;
6545  $max = 0;
6546  foreach ($found_participants as $userdata) {
6547  for ($i = 0; $i <= $userdata->getLastPass(); $i++) {
6548  if (is_object($userdata->getPass($i))) {
6549  $question = $userdata->getPass($i)->getAnsweredQuestionByQuestionId($question_id);
6550  if (is_array($question)) {
6551  $answered++;
6552  $reached += $question['reached'];
6553  $max += $question['points'];
6554  }
6555  }
6556  }
6557  }
6558  $percent = $max ? $reached / $max * 100.0 : 0;
6559  $results['questions'][$question_id] = [
6560  $question_title,
6561  sprintf('%.2f', $answered ? $reached / $answered : 0) . ' ' . strtolower($this->lng->txt('of')) . ' ' . sprintf('%.2f', $answered ? $max / $answered : 0),
6562  sprintf('%.2f', $percent) . '%',
6563  $answered,
6564  sprintf('%.2f', $answered ? $reached / $answered : 0),
6565  sprintf('%.2f', $answered ? $max / $answered : 0),
6566  $percent / 100.0
6567  ];
6568  }
6569  return $results;
6570  }
6571 
6572  protected function secondsToHoursMinutesSecondsString(int $seconds): string
6573  {
6574  $diff_hours = floor($seconds / 3600);
6575  $seconds -= $diff_hours * 3600;
6576  $diff_minutes = floor($seconds / 60);
6577  $seconds -= $diff_minutes * 60;
6578  return sprintf('%02d:%02d:%02d', $diff_hours, $diff_minutes, $seconds);
6579  }
6580 
6584  public function getXMLZip(): string
6585  {
6586  return $this->export_factory->getExporter($this, 'xml')
6587  ->write();
6588  }
6589 
6590  public function getMailNotification(): int
6591  {
6592  return $this->getMainSettings()->getFinishingSettings()->getMailNotificationContentType();
6593  }
6594 
6595  public function sendSimpleNotification($active_id)
6596  {
6597  $mail = new ilTestMailNotification();
6598  $owner_id = $this->getOwner();
6599  $usr_data = $this->userLookupFullName(ilObjTest::_getUserIdFromActiveId($active_id));
6600  $mail->sendSimpleNotification($owner_id, $this->getTitle(), $usr_data);
6601  }
6602 
6603  public function sendAdvancedNotification(int $active_id): void
6604  {
6605  $mail = new ilTestMailNotification();
6606  $owner_id = $this->getOwner();
6607  $usr_data = $this->userLookupFullName(ilObjTest::_getUserIdFromActiveId($active_id));
6608 
6609  $path = $this->export_factory->getExporter(
6610  $this,
6611  ExportImportTypes::SCORED_ATTEMPT
6612  )->withFilterByActiveId($active_id)
6613  ->write();
6614 
6615  $delivered_file_name = 'result_' . $active_id . '.xlsx';
6616  $fd = new ilFileDataMail(ANONYMOUS_USER_ID);
6617  $fd->copyAttachmentFile($path, $delivered_file_name);
6618  $file_names[] = $delivered_file_name;
6619 
6620  $mail->sendAdvancedNotification($owner_id, $this->getTitle(), $usr_data, $file_names);
6621 
6622  if (count($file_names)) {
6623  $fd->unlinkFiles($file_names);
6624  unset($fd);
6625  @unlink($path);
6626  }
6627  }
6628 
6629  public function getResultsForActiveId(int $active_id): array
6630  {
6631  $query = "
6632  SELECT *
6633  FROM tst_result_cache
6634  WHERE active_fi = %s
6635  ";
6636 
6637  $result = $this->db->queryF(
6638  $query,
6639  ['integer'],
6640  [$active_id]
6641  );
6642 
6643  if (!$result->numRows()) {
6644  $this->updateTestResultCache($active_id);
6645 
6646  $query = "
6647  SELECT *
6648  FROM tst_result_cache
6649  WHERE active_fi = %s
6650  ";
6651 
6652  $result = $this->db->queryF(
6653  $query,
6654  ['integer'],
6655  [$active_id]
6656  );
6657  }
6658 
6659  $row = $this->db->fetchAssoc($result);
6660 
6661  return $row;
6662  }
6663 
6664  public function getMailNotificationType(): bool
6665  {
6666  return $this->getMainSettings()->getFinishingSettings()->getAlwaysSendMailNotification();
6667  }
6668 
6669  public function getExportSettings(): int
6670  {
6671  return $this->getScoreSettings()->getResultDetailsSettings()->getExportSettings();
6672  }
6673 
6674  public function setTemplate(int $template_id)
6675  {
6676  $this->template_id = $template_id;
6677  }
6678 
6679  public function getTemplate(): int
6680  {
6681  return $this->template_id;
6682  }
6683 
6685  {
6686  $question_set_config = $this->question_set_config_factory->getQuestionSetConfig();
6687  $reindexed_sequence_position_map = $question_set_config->reindexQuestionOrdering();
6688 
6689  $this->loadQuestions();
6690 
6691  return $reindexed_sequence_position_map;
6692  }
6693 
6694  public function setQuestionOrder(array $order)
6695  {
6696  asort($order);
6697 
6698  $i = 0;
6699 
6700  foreach (array_keys($order) as $id) {
6701  $i++;
6702 
6703  $query = "
6704  UPDATE tst_test_question
6705  SET sequence = %s
6706  WHERE question_fi = %s
6707  ";
6708 
6709  $this->db->manipulateF(
6710  $query,
6711  ['integer', 'integer'],
6712  [$i, $id]
6713  );
6714  }
6715 
6716  if ($this->logger->isLoggingEnabled()) {
6717  $this->logger->logTestAdministrationInteraction(
6718  $this->logger->getInteractionFactory()->buildTestAdministrationInteraction(
6719  $this->getRefId(),
6720  $this->user->getId(),
6721  TestAdministrationInteractionTypes::QUESTION_MOVED,
6722  [
6723  AdditionalInformationGenerator::KEY_QUESTION_ORDER => $order
6724  ]
6725  )
6726  );
6727  }
6728 
6729  $this->loadQuestions();
6730  }
6731 
6732  public function hasQuestionsWithoutQuestionpool(): bool
6733  {
6734  $questions = $this->getQuestionTitlesAndIndexes();
6735 
6736  $IN_questions = $this->db->in('q1.question_id', array_keys($questions), false, 'integer');
6737 
6738  $query = "
6739  SELECT count(q1.question_id) cnt
6740 
6741  FROM qpl_questions q1
6742 
6743  INNER JOIN qpl_questions q2
6744  ON q2.question_id = q1.original_id
6745 
6746  WHERE $IN_questions
6747  AND q1.obj_fi = q2.obj_fi
6748  ";
6749  $rset = $this->db->query($query);
6750  $row = $this->db->fetchAssoc($rset);
6751 
6752  return $row['cnt'] > 0;
6753  }
6754 
6761  public static function _lookupFinishedUserTests($a_user_id): array
6762  {
6763  global $DIC;
6764  $ilDB = $DIC['ilDB'];
6765 
6766  $result = $ilDB->queryF(
6767  "SELECT test_fi,MAX(pass) AS pass FROM tst_active" .
6768  " JOIN tst_pass_result ON (tst_pass_result.active_fi = tst_active.active_id)" .
6769  " WHERE user_fi=%s" .
6770  " GROUP BY test_fi",
6771  ['integer', 'integer'],
6772  [$a_user_id, 1]
6773  );
6774  $all = [];
6775  while ($row = $ilDB->fetchAssoc($result)) {
6776  $obj_id = self::_getObjectIDFromTestID($row["test_fi"]);
6777  $all[$obj_id] = (bool) $row["pass"];
6778  }
6779  return $all;
6780  }
6781 
6782  public function getQuestions(): array
6783  {
6784  return $this->questions;
6785  }
6786 
6787  public function isOnline(): bool
6788  {
6789  return $this->online;
6790  }
6791 
6792  public function isOfferingQuestionHintsEnabled(): bool
6793  {
6794  return $this->getMainSettings()->getQuestionBehaviourSettings()->getQuestionHintsEnabled();
6795  }
6796 
6797  public function setActivationVisibility($a_value)
6798  {
6799  $this->activation_visibility = (bool) $a_value;
6800  }
6801 
6802  public function getActivationVisibility(): bool
6803  {
6805  }
6806 
6807  public function isActivationLimited(): ?bool
6808  {
6810  }
6811 
6812  public function setActivationLimited($a_value)
6813  {
6814  $this->activation_limited = (bool) $a_value;
6815  }
6816 
6817  public function storeActivationSettings(
6818  ?bool $is_activation_limited = false,
6819  ?int $activation_starting_time = null,
6820  ?int $activation_ending_time = null,
6821  bool $activation_visibility = false,
6822  ): void {
6823  if (!$this->ref_id) {
6824  return;
6825  }
6826 
6827  $item = new ilObjectActivation();
6828  $is_activation_limited ??= false;
6829 
6830  if (!$is_activation_limited) {
6831  $item->setTimingType(ilObjectActivation::TIMINGS_DEACTIVATED);
6832  } else {
6833  $item->setTimingType(ilObjectActivation::TIMINGS_ACTIVATION);
6834  $item->setTimingStart($activation_starting_time);
6835  $item->setTimingEnd($activation_ending_time);
6836  $item->toggleVisible($activation_visibility);
6837  }
6838 
6839  $item->update($this->ref_id);
6840 
6841  $this->setActivationLimited($is_activation_limited);
6842  $this->setActivationStartingTime($activation_starting_time);
6843  $this->setActivationStartingTime($activation_ending_time);
6844  $this->setActivationVisibility($activation_visibility);
6845  }
6846 
6847  public function getIntroductionPageId(): int
6848  {
6849  $page_id = $this->getMainSettings()->getIntroductionSettings()->getIntroductionPageId();
6850  if ($page_id !== null) {
6851  return $page_id;
6852  }
6853 
6854  $page_object = new ilTestPage();
6855  $page_object->setParentId($this->getId());
6856  $new_page_id = $page_object->createPageWithNextId();
6857  $settings = $this->getMainSettings()->getIntroductionSettings()
6858  ->withIntroductionPageId($new_page_id);
6859  $this->getMainSettingsRepository()->store(
6860  $this->getMainSettings()->withIntroductionSettings($settings)
6861  );
6862  return $new_page_id;
6863  }
6864 
6865  public function getConcludingRemarksPageId(): int
6866  {
6867  $page_id = $this->getMainSettings()->getFinishingSettings()->getConcludingRemarksPageId();
6868  if ($page_id !== null) {
6869  return $page_id;
6870  }
6871 
6872  $page_object = new ilTestPage();
6873  $page_object->setParentId($this->getId());
6874  $new_page_id = $page_object->createPageWithNextId();
6875  $settings = $this->getMainSettings()->getFinishingSettings()
6876  ->withConcludingRemarksPageId($new_page_id);
6877  $this->getMainSettingsRepository()->store(
6878  $this->getMainSettings()->withFinishingSettings($settings)
6879  );
6880  return $new_page_id;
6881  }
6882 
6883  public function getHighscoreEnabled(): bool
6884  {
6885  return $this->getScoreSettings()->getGamificationSettings()->getHighscoreEnabled();
6886  }
6887 
6897  public function getHighscoreAnon(): bool
6898  {
6899  return $this->getScoreSettings()->getGamificationSettings()->getHighscoreAnon();
6900  }
6901 
6910  public function isHighscoreAnon(): bool
6911  {
6912  return $this->getAnonymity() == 1 || $this->getHighscoreAnon();
6913  }
6914 
6918  public function getHighscoreAchievedTS(): bool
6919  {
6920  return $this->getScoreSettings()->getGamificationSettings()->getHighscoreAchievedTS();
6921  }
6922 
6926  public function getHighscoreScore(): bool
6927  {
6928  return $this->getScoreSettings()->getGamificationSettings()->getHighscoreScore();
6929  }
6930 
6934  public function getHighscorePercentage(): bool
6935  {
6936  return $this->getScoreSettings()->getGamificationSettings()->getHighscorePercentage();
6937  }
6938 
6942  public function getHighscoreHints(): bool
6943  {
6944  return $this->getScoreSettings()->getGamificationSettings()->getHighscoreHints();
6945  }
6946 
6950  public function getHighscoreWTime(): bool
6951  {
6952  return $this->getScoreSettings()->getGamificationSettings()->getHighscoreWTime();
6953  }
6954 
6958  public function getHighscoreOwnTable(): bool
6959  {
6960  return $this->getScoreSettings()->getGamificationSettings()->getHighscoreOwnTable();
6961  }
6962 
6966  public function getHighscoreTopTable(): bool
6967  {
6968  return $this->getScoreSettings()->getGamificationSettings()->getHighscoreTopTable();
6969  }
6970 
6975  public function getHighscoreTopNum(int $a_retval = 10): int
6976  {
6977  return $this->getScoreSettings()->getGamificationSettings()->getHighscoreTopNum();
6978  }
6979 
6980  public function getHighscoreMode(): int
6981  {
6982  return $this->getScoreSettings()->getGamificationSettings()->getHighScoreMode();
6983  }
6984 
6985  public function getSpecificAnswerFeedback(): bool
6986  {
6987  return $this->getMainSettings()->getQuestionBehaviourSettings()->getInstantFeedbackSpecificEnabled();
6988  }
6989 
6990  public function getAutosave(): bool
6991  {
6992  return $this->getMainSettings()->getQuestionBehaviourSettings()->getAutosaveEnabled();
6993  }
6994 
6995  public function isPassDeletionAllowed(): bool
6996  {
6997  return $this->getScoreSettings()->getResultSummarySettings()->getPassDeletionAllowed();
6998  }
6999 
7000  public function getEnableExamview(): bool
7001  {
7002  return $this->getMainSettings()->getFinishingSettings()->getShowAnswerOverview();
7003  }
7004 
7005  public function setActivationStartingTime(?int $starting_time = null)
7006  {
7007  $this->activation_starting_time = $starting_time;
7008  }
7009 
7010  public function setActivationEndingTime(?int $ending_time = null)
7011  {
7012  $this->activation_ending_time = $ending_time;
7013  }
7014 
7015  public function getActivationStartingTime(): ?int
7016  {
7018  }
7019 
7020  public function getActivationEndingTime(): ?int
7021  {
7023  }
7024 
7031  public function getStartingTimeOfParticipants(): array
7032  {
7033  $times = [];
7034  $result = $this->db->queryF(
7035  "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",
7036  ['integer'],
7037  [$this->getTestId()]
7038  );
7039  while ($row = $this->db->fetchAssoc($result)) {
7040  $times[$row['active_fi']] = $row['started'];
7041  }
7042  return $times;
7043  }
7044 
7045  public function getTimeExtensionsOfParticipants(): array
7046  {
7047  $times = [];
7048  $result = $this->db->queryF(
7049  "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",
7050  ['integer'],
7051  [$this->getTestId()]
7052  );
7053  while ($row = $this->db->fetchAssoc($result)) {
7054  $times[$row['active_fi']] = $row['additionaltime'];
7055  }
7056  return $times;
7057  }
7058 
7059  private function getExtraTime(int $active_id): int
7060  {
7061  if ($active_id === 0) {
7062  return 0;
7063  }
7064  return $this->participant_repository
7065  ->getParticipantByActiveId($this->getTestId(), $active_id)
7066  ?->getExtraTime() ?? 0;
7067  }
7068 
7069  public function getMaxPassOfTest(): int
7070  {
7071  $query = '
7072  SELECT MAX(tst_pass_result.pass) + 1 max_res
7073  FROM tst_pass_result
7074  INNER JOIN tst_active ON tst_active.active_id = tst_pass_result.active_fi
7075  WHERE test_fi = ' . $this->db->quote($this->getTestId(), 'integer') . '
7076  ';
7077  $res = $this->db->query($query);
7078  $data = $this->db->fetchAssoc($res);
7079  return (int) $data['max_res'];
7080  }
7081 
7082  public static function lookupExamId($active_id, $pass)
7083  {
7084  global $DIC;
7085  $ilDB = $DIC['ilDB'];
7086 
7087  $exam_id_query = 'SELECT exam_id FROM tst_pass_result WHERE active_fi = %s AND pass = %s';
7088  $exam_id_result = $ilDB->queryF($exam_id_query, [ 'integer', 'integer' ], [ $active_id, $pass ]);
7089  if ($ilDB->numRows($exam_id_result) == 1) {
7090  $exam_id_row = $ilDB->fetchAssoc($exam_id_result);
7091 
7092  if ($exam_id_row['exam_id'] != null) {
7093  return $exam_id_row['exam_id'];
7094  }
7095  }
7096 
7097  return null;
7098  }
7099 
7100  public static function buildExamId($active_id, $pass, $test_obj_id = null): string
7101  {
7102  global $DIC;
7103  $ilSetting = $DIC['ilSetting'];
7104 
7105  $inst_id = $ilSetting->get('inst_id', null);
7106 
7107  if ($test_obj_id === null) {
7108  $obj_id = self::_getObjectIDFromActiveID($active_id);
7109  } else {
7110  $obj_id = $test_obj_id;
7111  }
7112 
7113  $examId = 'I' . $inst_id . '_T' . $obj_id . '_A' . $active_id . '_P' . $pass;
7114 
7115  return $examId;
7116  }
7117 
7118  public function isShowExamIdInTestPassEnabled(): bool
7119  {
7120  return $this->getMainSettings()->getTestBehaviourSettings()->getExamIdInTestAttemptEnabled();
7121  }
7122 
7123  public function isShowExamIdInTestResultsEnabled(): bool
7124  {
7125  return $this->getScoreSettings()->getResultDetailsSettings()->getShowExamIdInTestResults();
7126  }
7127 
7128 
7129  public function setQuestionSetType(string $question_set_type)
7130  {
7131  $this->main_settings = $this->getMainSettings()->withGeneralSettings(
7133  ->withQuestionSetType($question_set_type)
7134  );
7135  }
7136 
7137  public function getQuestionSetType(): string
7138  {
7139  return $this->getMainSettings()->getGeneralSettings()->getQuestionSetType();
7140  }
7141 
7147  public function isFixedTest(): bool
7148  {
7149  return $this->getQuestionSetType() == self::QUESTION_SET_TYPE_FIXED;
7150  }
7151 
7157  public function isRandomTest(): bool
7158  {
7159  return $this->getQuestionSetType() == self::QUESTION_SET_TYPE_RANDOM;
7160  }
7161 
7162  public function getQuestionSetTypeTranslation(ilLanguage $lng, $questionSetType): string
7163  {
7164  switch ($questionSetType) {
7166  return $lng->txt('tst_question_set_type_fixed');
7167 
7169  return $lng->txt('tst_question_set_type_random');
7170  }
7171 
7172  throw new ilTestException('invalid question set type value given: ' . $questionSetType);
7173  }
7174 
7175  public function participantDataExist(): bool
7176  {
7177  if ($this->participantDataExist === null) {
7178  $this->participantDataExist = (bool) $this->evalTotalPersons();
7179  }
7180 
7182  }
7183 
7184  public function recalculateScores($preserve_manscoring = false)
7185  {
7186  $scoring = new TestScoring($this, $this->user, $this->db, $this->lng);
7187  $scoring->setPreserveManualScores($preserve_manscoring);
7188  $scoring->recalculateSolutions();
7189  }
7190 
7191  public static function getTestObjIdsWithActiveForUserId($userId): array
7192  {
7193  global $DIC;
7194  $ilDB = $DIC['ilDB'];
7195 
7196  $query = "
7197  SELECT obj_fi
7198  FROM tst_active
7199  INNER JOIN tst_tests
7200  ON test_id = test_fi
7201  WHERE user_fi = %s
7202  ";
7203 
7204  $res = $ilDB->queryF($query, ['integer'], [$userId]);
7205 
7206  $objIds = [];
7207 
7208  while ($row = $ilDB->fetchAssoc($res)) {
7209  $objIds[] = (int) $row['obj_fi'];
7210  }
7211 
7212  return $objIds;
7213  }
7214 
7215  public function isSkillServiceEnabled(): bool
7216  {
7217  return $this->getMainSettings()->getAdditionalSettings()->getSkillsServiceEnabled();
7218  }
7219 
7231  public function isSkillServiceToBeConsidered(): bool
7232  {
7233  if (!$this->getMainSettings()->getAdditionalSettings()->getSkillsServiceEnabled()) {
7234  return false;
7235  }
7236 
7237  if (!self::isSkillManagementGloballyActivated()) {
7238  return false;
7239  }
7240 
7241  return true;
7242  }
7243 
7245 
7246  public static function isSkillManagementGloballyActivated(): ?bool
7247  {
7248  if (self::$isSkillManagementGloballyActivated === null) {
7249  $skmgSet = new ilSkillManagementSettings();
7250 
7251  self::$isSkillManagementGloballyActivated = $skmgSet->isActivated();
7252  }
7253 
7254  return self::$isSkillManagementGloballyActivated;
7255  }
7256 
7257  public function isShowGradingStatusEnabled(): bool
7258  {
7259  return $this->getScoreSettings()->getResultSummarySettings()->getShowGradingStatusEnabled();
7260  }
7261 
7262  public function isShowGradingMarkEnabled(): bool
7263  {
7264  return $this->getScoreSettings()->getResultSummarySettings()->getShowGradingMarkEnabled();
7265  }
7266 
7268  {
7269  return $this->getMainSettings()->getQuestionBehaviourSettings()->getLockAnswerOnNextQuestionEnabled();
7270  }
7271 
7273  {
7274  return $this->getMainSettings()->getQuestionBehaviourSettings()->getLockAnswerOnInstantFeedbackEnabled();
7275  }
7276 
7277  public function isForceInstantFeedbackEnabled(): ?bool
7278  {
7279  return $this->getMainSettings()->getQuestionBehaviourSettings()->getForceInstantFeedbackOnNextQuestion();
7280  }
7281 
7282 
7283  public static function isParticipantsLastPassActive(int $test_ref_id, int $user_id): bool
7284  {
7285  global $DIC;
7286  $ilDB = $DIC['ilDB'];
7287  $ilUser = $DIC['ilUser'];
7288 
7289  $test_obj = ilObjectFactory::getInstanceByRefId($test_ref_id, false);
7290 
7291  $active_id = $test_obj->getActiveIdOfUser($user_id);
7292 
7293  $test_session_factory = new ilTestSessionFactory($test_obj, $ilDB, $ilUser);
7294 
7295  // Added temporarily bugfix smeyer
7296  $test_session_factory->reset();
7297 
7298  $test_sequence_factory = new ilTestSequenceFactory($test_obj, $ilDB, TestDIC::dic()['question.general_properties.repository']);
7299 
7300  $test_session = $test_session_factory->getSession($active_id);
7301  $test_sequence = $test_sequence_factory->getSequenceByActiveIdAndPass($active_id, $test_session->getPass());
7302  $test_sequence->loadFromDb();
7303 
7304  return $test_sequence->hasSequence();
7305  }
7306 
7307  public function adjustTestSequence()
7308  {
7309  $query = "
7310  SELECT COUNT(test_question_id) cnt
7311  FROM tst_test_question
7312  WHERE test_fi = %s
7313  ORDER BY sequence
7314  ";
7315 
7316  $questRes = $this->db->queryF($query, ['integer'], [$this->getTestId()]);
7317 
7318  $row = $this->db->fetchAssoc($questRes);
7319  $questCount = $row['cnt'];
7320 
7321  if ($this->getShuffleQuestions()) {
7322  $query = "
7323  SELECT tseq.*
7324  FROM tst_active tac
7325  INNER JOIN tst_sequence tseq
7326  ON tseq.active_fi = tac.active_id
7327  WHERE tac.test_fi = %s
7328  ";
7329 
7330  $partRes = $this->db->queryF(
7331  $query,
7332  ['integer'],
7333  [$this->getTestId()]
7334  );
7335 
7336  while ($row = $this->db->fetchAssoc($partRes)) {
7337  $sequence = @unserialize($row['sequence']);
7338 
7339  if (!$sequence) {
7340  $sequence = [];
7341  }
7342 
7343  $sequence = array_filter($sequence, function ($value) use ($questCount) {
7344  return $value <= $questCount;
7345  });
7346 
7347  $num_seq = count($sequence);
7348  if ($questCount > $num_seq) {
7349  $diff = $questCount - $num_seq;
7350  for ($i = 1; $i <= $diff; $i++) {
7351  $sequence[$num_seq + $i - 1] = $num_seq + $i;
7352  }
7353  }
7354 
7355  $new_sequence = serialize($sequence);
7356 
7357  $this->db->update('tst_sequence', [
7358  'sequence' => ['clob', $new_sequence]
7359  ], [
7360  'active_fi' => ['integer', $row['active_fi']],
7361  'pass' => ['integer', $row['pass']]
7362  ]);
7363  }
7364  } else {
7365  $new_sequence = serialize($questCount > 0 ? range(1, $questCount) : []);
7366 
7367  $query = "
7368  SELECT tseq.*
7369  FROM tst_active tac
7370  INNER JOIN tst_sequence tseq
7371  ON tseq.active_fi = tac.active_id
7372  WHERE tac.test_fi = %s
7373  ";
7374 
7375  $part_rest = $this->db->queryF(
7376  $query,
7377  ['integer'],
7378  [$this->getTestId()]
7379  );
7380 
7381  while ($row = $this->db->fetchAssoc($part_rest)) {
7382  $this->db->update('tst_sequence', [
7383  'sequence' => ['clob', $new_sequence]
7384  ], [
7385  'active_fi' => ['integer', $row['active_fi']],
7386  'pass' => ['integer', $row['pass']]
7387  ]);
7388  }
7389  }
7390  }
7391 
7396  {
7397  return ilHtmlPurifierFactory::getInstanceByType('qpl_usersolution');
7398  }
7399 
7401  {
7403  }
7404 
7406  {
7407  return $this->global_settings_repo->getGlobalSettings();
7408  }
7409 
7410  public function getMainSettings(): MainSettings
7411  {
7412  if (!$this->main_settings) {
7413  $this->main_settings = $this->getMainSettingsRepository()
7414  ->getFor($this->getTestId());
7415  }
7416  return $this->main_settings;
7417  }
7418 
7420  {
7421  if (!$this->main_settings_repo) {
7422  $this->main_settings_repo = new MainSettingsDatabaseRepository($this->db);
7423  }
7425  }
7426 
7427  public function getScoreSettings(): ScoreSettings
7428  {
7429  if (!$this->score_settings) {
7430  $this->score_settings = $this->getScoreSettingsRepository()
7431  ->getFor($this->getTestId());
7432  }
7433  return $this->score_settings;
7434  }
7435 
7437  {
7438  if (!$this->score_settings_repo) {
7439  $this->score_settings_repo = new ScoreSettingsDatabaseRepository($this->db);
7440  }
7442  }
7443 
7444  public function updateTestResultCache(int $active_id, ilAssQuestionProcessLocker $process_locker = null): void
7445  {
7446  $pass = ilObjTest::_getResultPass($active_id);
7447 
7448  if ($pass !== null) {
7449  $query = '
7450  SELECT tst_pass_result.*,
7451  tst_active.last_finished_pass
7452  FROM tst_pass_result
7453  INNER JOIN tst_active
7454  on tst_pass_result.active_fi = tst_active.active_id
7455  WHERE active_fi = %s
7456  AND pass = %s
7457  ';
7458 
7459  $result = $this->db->queryF(
7460  $query,
7461  ['integer','integer'],
7462  [$active_id, $pass]
7463  );
7464 
7465  $test_pass_result_row = $this->db->fetchAssoc($result);
7466 
7467  if (!is_array($test_pass_result_row)) {
7468  $test_pass_result_row = [];
7469  }
7470  $max = (float) ($test_pass_result_row['maxpoints'] ?? 0);
7471  $reached = (float) ($test_pass_result_row['points'] ?? 0);
7472  $percentage = ($max <= 0.0 || $reached <= 0.0) ? 0 : ($reached / $max) * 100.0;
7473 
7474  $mark = $this->getMarkSchema()->getMatchingMark($percentage);
7475  $is_passed = $test_pass_result_row['last_finished_pass'] !== null
7476  && $pass <= $test_pass_result_row['last_finished_pass']
7477  && $mark->getPassed();
7478 
7479  $hint_count = $test_pass_result_row['hint_count'] ?? 0;
7480  $hint_points = $test_pass_result_row['hint_points'] ?? 0.0;
7481 
7482  $user_test_result_update_callback = function () use ($active_id, $pass, $max, $reached, $is_passed, $hint_count, $hint_points, $mark) {
7483  $passed_once_before = 0;
7484  $query = 'SELECT passed_once FROM tst_result_cache WHERE active_fi = %s';
7485  $res = $this->db->queryF($query, ['integer'], [$active_id]);
7486  while ($passed_once_result_row = $this->db->fetchAssoc($res)) {
7487  $passed_once_before = (int) $passed_once_result_row['passed_once'];
7488  }
7489 
7490  $passed_once = (int) ($is_passed || $passed_once_before);
7491 
7492  $this->db->manipulateF(
7493  'DELETE FROM tst_result_cache WHERE active_fi = %s',
7494  ['integer'],
7495  [$active_id]
7496  );
7497 
7498  if ($reached < 0.0) {
7499  $reached = 0.0;
7500  }
7501 
7502  $mark_short_name = $mark->getShortName();
7503  if ($mark_short_name === '') {
7504  $mark_short_name = ' ';
7505  }
7506 
7507  $mark_official_name = $mark->getOfficialName();
7508  if ($mark_official_name === '') {
7509  $mark_official_name = ' ';
7510  }
7511 
7512  $this->db->insert(
7513  'tst_result_cache',
7514  [
7515  'active_fi' => ['integer', $active_id],
7516  'pass' => ['integer', $pass ?? 0],
7517  'max_points' => ['float', $max],
7518  'reached_points' => ['float', $reached],
7519  'mark_short' => ['text', $mark_short_name],
7520  'mark_official' => ['text', $mark_official_name],
7521  'passed_once' => ['integer', $passed_once],
7522  'passed' => ['integer', (int) $is_passed],
7523  'failed' => ['integer', (int) !$is_passed],
7524  'tstamp' => ['integer', time()],
7525  'hint_count' => ['integer', $hint_count],
7526  'hint_points' => ['float', $hint_points]
7527  ]
7528  );
7529  };
7530 
7531  if (is_object($process_locker)) {
7532  $process_locker->executeUserTestResultUpdateLockOperation($user_test_result_update_callback);
7533  } else {
7534  $user_test_result_update_callback();
7535  }
7536  }
7537  }
7538 
7539  public function updateTestPassResults(
7540  int $active_id,
7541  int $pass,
7542  ilAssQuestionProcessLocker $process_locker = null,
7543  int $test_obj_id = null
7544  ): array {
7545  $data = $this->getQuestionCountAndPointsForPassOfParticipant($active_id, $pass);
7546  $time = $this->getWorkingTimeOfParticipantForPass($active_id, $pass);
7547 
7548  $result = $this->db->queryF(
7549  '
7550  SELECT SUM(r.points) reachedpoints,
7551  SUM(r.hint_count) hint_count,
7552  SUM(r.hint_points) hint_points,
7553  COUNT(DISTINCT(r.question_fi)) answeredquestions,
7554  pr.finalized_by finalized_by
7555  FROM tst_test_result r
7556  INNER JOIN tst_pass_result pr
7557  ON r.active_fi = pr.active_fi AND r.pass = pr.pass
7558  WHERE r.active_fi = %s
7559  AND r.pass = %s
7560  ',
7561  ['integer','integer'],
7562  [$active_id, $pass]
7563  );
7564 
7565  if ($result->numRows() > 0) {
7566  $row = $this->db->fetchAssoc($result);
7567 
7568  if ($row['reachedpoints'] === null
7569  || $row['reachedpoints'] < 0.0) {
7570  $row['reachedpoints'] = 0.0;
7571  }
7572  if ($row['hint_count'] === null) {
7573  $row['hint_count'] = 0;
7574  }
7575  if ($row['hint_points'] === null) {
7576  $row['hint_points'] = 0.0;
7577  }
7578 
7579  $exam_identifier = ilObjTest::buildExamId($active_id, $pass, $test_obj_id);
7580 
7581  $update_pass_result_callback = function () use ($data, $active_id, $pass, $row, $time, $exam_identifier) {
7582  $this->db->replace(
7583  'tst_pass_result',
7584  [
7585  'active_fi' => ['integer', $active_id],
7586  'pass' => ['integer', $pass]
7587  ],
7588  [
7589  'points' => ['float', $row['reachedpoints']],
7590  'maxpoints' => ['float', $data['points']],
7591  'questioncount' => ['integer', $data['count']],
7592  'answeredquestions' => ['integer', $row['answeredquestions']],
7593  'workingtime' => ['integer', $time],
7594  'tstamp' => ['integer', time()],
7595  'hint_count' => ['integer', $row['hint_count']],
7596  'hint_points' => ['float', $row['hint_points']],
7597  'exam_id' => ['text', $exam_identifier],
7598  'finalized_by' => ['text', $row['finalized_by']]
7599  ]
7600  );
7601  };
7602 
7603  if (is_object($process_locker) && $process_locker instanceof ilAssQuestionProcessLocker) {
7604  $process_locker->executeUserPassResultUpdateLockOperation($update_pass_result_callback);
7605  } else {
7606  $update_pass_result_callback();
7607  }
7608  }
7609 
7610  $this->updateTestResultCache($active_id, $process_locker);
7611 
7612  return [
7613  'active_fi' => $active_id,
7614  'pass' => $pass,
7615  'points' => $row['reachedpoints'],
7616  'maxpoints' => $data['points'],
7617  'questioncount' => $data['count'],
7618  'answeredquestions' => $row['answeredquestions'],
7619  'workingtime' => $time,
7620  'tstamp' => time(),
7621  'hint_count' => $row['hint_count'],
7622  'hint_points' => $row['hint_points'],
7623  'exam_id' => $exam_identifier
7624  ];
7625  }
7626 
7627  public function addToNewsOnOnline(
7628  bool $old_online_status,
7629  bool $new_online_status
7630  ): void {
7631  if (!$old_online_status && $new_online_status) {
7632  $newsItem = new ilNewsItem();
7633  $newsItem->setContext($this->getId(), 'tst');
7634  $newsItem->setPriority(NEWS_NOTICE);
7635  $newsItem->setTitle('new_test_online');
7636  $newsItem->setContentIsLangVar(true);
7637  $newsItem->setContent('');
7638  $newsItem->setUserId($this->user->getId());
7639  $newsItem->setVisibility(NEWS_USERS);
7640  $newsItem->create();
7641  return;
7642  }
7643 
7644  if ($old_online_status && !$new_online_status) {
7645  ilNewsItem::deleteNewsOfContext($this->getId(), 'tst');
7646  return;
7647  }
7648 
7649  $newsId = ilNewsItem::getFirstNewsIdForContext($this->getId(), 'tst');
7650  if (!$new_online_status && $newsId > 0) {
7651  $newsItem = new ilNewsItem($newsId);
7652  $newsItem->setTitle('new_test_online');
7653  $newsItem->setContentIsLangVar(true);
7654  $newsItem->setContent('');
7655  $newsItem->update();
7656  }
7657  }
7658 
7662  public static function _lookupRandomTest(int $obj_id): bool
7663  {
7664  global $DIC;
7665 
7666  $query = 'SELECT question_set_type FROM tst_tests WHERE obj_fi = %s';
7667 
7668  $res = $DIC['ilDB']->queryF($query, ['integer'], [$obj_id]);
7669 
7670  $question_set_type = null;
7671 
7672  while ($row = $DIC['ilDB']->fetchAssoc($res)) {
7673  $question_set_type = $row['question_set_type'];
7674  }
7675 
7676  return $question_set_type === self::QUESTION_SET_TYPE_RANDOM;
7677  }
7678 
7679  public function getVisitingTimeOfParticipant(int $active_id): array
7680  {
7681  return $this->participant_repository->getFirstAndLastVisitForActiveId($active_id);
7682  }
7683 }
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 isParticipantsLastPassActive(int $test_ref_id, int $user_id)
replaceFilesInPageImports(string $text, array $mappings)
string $title
addConcludingRemarksToSettingsFromImport(SettingsFinishing $settings, array $material, string $importdir, array $mappings)
withGamificationSettings(SettingsGamification $settings)
setQuestionSetType(string $question_set_type)
& getWorkedQuestions($active_id, $pass=null)
Gets the id&#39;s of all questions a user already worked through.
isNextPassAllowed(ilTestPassesSelector $testPassesSelector, int &$next_pass_allowed_timestamp)
getListOfQuestionsDescription()
raiseError(string $a_msg, int $a_err_obj)
wrapper for downward compability
static get(string $a_var)
buildStatisticsAccessFilteredParticipantList()
getTimeExtensionsOfParticipants()
isHTML($a_text)
Checks if a given string contains HTML or not.
static getStyleSheetLocation(string $mode="output", string $a_css_name="")
get full style sheet file name (path inclusive) of current user
ILIAS $ilias
static completeMissingPluginName(array $question_type_data)
$res
Definition: ltiservices.php:69
isBlockPassesAfterPassedEnabled()
isTestFinished($active_id)
returns if the active for user_id has been submitted
MainSettingsRepository $main_settings_repo
getExtraTime(int $active_id)
Readable part of repository interface to ilComponentDataDB.
getHighscoreOwnTable()
Gets if the own rankings table should be shown.
int $activation_starting_time
getHighscoreTopNum(int $a_retval=10)
Gets the number of entries which are to be shown in the top-rankings table.
static formatDate(ilDateTime $date, bool $a_skip_day=false, bool $a_include_wd=false, bool $include_seconds=false, ilObjUser $user=null,)
exportFileItems($target_dir, &$expLog)
export files of file itmes
static _getObjectIDFromTestID($test_id)
Returns the ILIAS test object id for a given test id.
A class defining mark schemas for assessment test objects.
Definition: MarkSchema.php:35
deliverPDFfromFO($fo, $title=null)
Delivers a PDF file from a XSL-FO string.
isComplete(ilTestQuestionSetConfig $test_question_set_config)
storePropertyIsOnline(ilObjectPropertyIsOnline $property_is_online)
ilComponentRepository $component_repository
static _getPass($active_id)
Retrieves the actual pass of a given user for a given test.
exportXMLPageObjects(&$a_xml_writer, $inst, &$expLog)
export page objects to xml (see ilias_co.dtd)
const IL_INST_ID
Definition: constants.php:40
getUnfilteredEvaluationData()
isShowExamIdInTestPassEnabled()
processPrintoutput2FO($print_output)
Convert a print output to XSL-FO.
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.
const ANONYMOUS_USER_ID
Definition: constants.php:27
applyDefaults(array $test_defaults)
txt(string $a_topic, string $a_default_lang_fallback_mod="")
gets the text for a given topic if the topic is not in the list, the topic itself with "-" will be re...
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
const QUESTION_SET_TYPE_RANDOM
static _getSuggestedSolutionOutput(int $question_id)
MarksRepository $marks_repository
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
A class defining marks for assessment test objects.
Definition: Mark.php:35
getCompleteWorkingTimeOfParticipant($active_id)
Returns the complete working time in seconds for a test participant.
This class handles all operations on files (attachments) in directory ilias_data/mail.
setActivationStartingTime(?int $starting_time=null)
qtiMaterialToArray($a_material)
Reads an QTI material tag and creates a text string.
TestManScoringDoneHelper $test_man_scoring_done_helper
if(! $DIC->user() ->getId()||!ilLTIConsumerAccess::hasCustomProviderCreationAccess()) $params
Definition: ltiregstart.php:31
getShowSolutionListOwnAnswers()
getShowSolutionFeedback()
Returns if the feedback should be presented to the solution or not.
removeTestResults(ilTestParticipantData $participant_data)
static lookupPassResultsUpdateTimestamp($active_id, $pass)
& getExistingQuestions($pass=null)
Get the id&#39;s of the questions which are already part of the test.
bool $print_best_solution_with_result
isExecutable($test_session, $user_id, $allow_pass_increase=false)
Checks if the test is executable by the given user.
loadQuestions(int $active_id=0, ?int $pass=null)
Load the test question id&#39;s from the database.
getShowKioskModeParticipant()
static $isSkillManagementGloballyActivated
buildDateTimeImmutableFromPeriod(?string $period)
getQuestionSetTypeTranslation(ilLanguage $lng, $questionSetType)
saveCompleteStatus(ilTestQuestionSetConfig $test_question_set_config)
getHighscoreAchievedTS()
Returns if date and time of the scores achievement should be displayed.
getTestId()
Gets the database id of the additional test data.
moveQuestions(array $move_questions, int $target_index, int $insert_mode)
static factory(string $a_package, int $a_timeout=0)
Creates an ilRpcClient instance to our ilServer.
Class ilTestMailNotification.
getListOfQuestionsSettings()
Returns the settings for the list of questions options in the test properties This could contain one ...
createQuestionGUI($question_type, $question_id=-1)
Creates a question GUI instance of a given question type.
TestLogViewer $log_viewer
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.
getJavaScriptOutput()
Returns if Javascript should be chosen for drag & drop actions for the active user.
getShowSolutionAnswersOnly()
Returns if the full solution (including ILIAS content) should be presented to the solution or not...
static _lookupName(int $a_user_id)
lookup user name
& createTestSequence($active_id, $pass, $shuffle)
getQuestionTitle($title, $nr=null, $points=null)
Returns the title of a test question and checks if the title output is allowed.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
$test_sequence
contains the test sequence data
getXMLZip()
Get zipped xml file for test.
getGeneralSettings()
inviteUser($user_id, $client_ip="")
Invites a user to a test.
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.
evalTotalPersonsArray(string $name_sort_order='asc')
const INVITATION_ON
& getInvitedUsers(int $user_id=0, $order="login, lastname, firstname")
Returns a list of all invited users in a test.
ParticipantRepository $participant_repository
toXML()
Returns a QTI xml representation of the test.
ilCtrlInterface $ctrl
GlobalSettingsRepository $global_settings_repo
setTemplate(int $template_id)
setActivationLimited($a_value)
static isManScoringDone(int $active_id)
setActivationEndingTime(?int $ending_time=null)
setTestId($a_id)
Sets the test ID.
static _lookupRandomTest(int $obj_id)
isShowExamIdInTestResultsEnabled()
getProcessingTimeInSeconds(int $active_id=0)
static deliverData(string $a_data, string $a_filename, string $mime="application/octet-stream")
getCompleteWorkingTime($user_id)
Returns the complete working time in seconds a user worked on the test.
getQuestionsOfPass(int $active_id, int $pass)
getImagePathWeb()
Returns the web image path for web accessable images of a test The image path is under the web access...
static prepareFormOutput($a_str, bool $a_strip=false)
const IL_CAL_UNIX
startingTimeReached()
Returns true if the starting time of a test is reached A starting time is not available for self asse...
questionMoveDown($question_id)
Moves a question down in order.
getHighscoreWTime()
Gets if the column with the workingtime should be shown.
hasAnyTestResult(ilTestSession $test_session)
getActiveIdOfUser($user_id="", $anonymous_id="")
Gets the active id of a given user.
static _getObjectIDFromActiveID($active_id)
Returns the ILIAS test object id for a given active id.
$participantDataExist
holds the fact wether participant data exists or not DO NOT USE TIS PROPERTY DRIRECTLY ALWAYS USE ilO...
getPassed($active_id)
static _isPassed($user_id, $a_obj_id)
Returns TRUE if the user with the user id $user_id passed the test with the object id $a_obj_id...
buildName(?int $user_id, ?string $firstname, ?string $lastname)
Builds a user name for the output depending on test type and existence of the user.
static _getBestPass($active_id)
Retrieves the best pass of a given user for a given test.
evalTotalStartedAverageTime(?array $active_ids_to_filter=null)
saveToDb(bool $properties_only=false)
static _lookupTestObjIdForQuestionId(int $q_id)
Get test Object ID for question ID.
withGeneralSettings(SettingsGeneral $settings)
static _lookupAnonymity($a_obj_id)
getTestResult(int $active_id, ?int $pass=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...
Refinery $refinery
sort()
description: > Example for rendering a Sort Glyph.
Definition: sort.php:25
getPotentialRandomTestQuestions()
getShowSolutionListComparison()
setQuestionSetSolved($value, $question_id, $user_id)
sets question solved state to value for given user_id
Base Exception for all Exceptions relating to Modules/Test.
getHighscoreTopTable()
Gets, if the top-rankings table should be shown.
$path
Definition: ltiservices.php:30
lookupQuestionSetTypeByActiveId(int $active_id)
updateTestPassResults(int $active_id, int $pass, ilAssQuestionProcessLocker $process_locker=null, int $test_obj_id=null)
getQuestionsOfTest(int $active_id)
startWorkingTime($active_id, $pass)
Write the initial entry for the tests working time to the database.
static removeTrailingPathSeparators(string $path)
Test sequence handler.
$evaluation_data
Contains the evaluation data settings the tutor defines for the user.
int $tmpCopyWizardCopyId
array $questions
static _lookupObjId(int $ref_id)
ilBenchmark $bench
static _instanciateQuestion($question_id)
Creates an instance of a question with a given question id.
setQuestionOrder(array $order)
& _getCompleteWorkingTimeOfParticipants($test_id)
Returns the complete working time in seconds for all test participants.
isRandomTest()
Returns the fact wether this test is a random questions test or not.
ilSetting $settings
static getASCIIFilename(string $a_filename)
getHighscorePercentage()
Gets if the percentage column should be shown.
xmlEndTag(string $tag)
Writes an endtag.
isFixedTest()
Returns the fact wether this test is a fixed question set test or not.
const ILIAS_VERSION
static _getUserIdFromActiveId(int $active_id)
updateWorkingTime($times_id)
Update the working time of a test when a question is answered.
getFixedQuestionSetTotalPoints()
secondsToHoursMinutesSecondsString(int $seconds)
getHtmlQuestionContentPurifier()
ExportImportFactory $export_factory
ilTestParticipantList $access_filtered_participant_list
getImportMapping()
get array of (two) new created questions for import id
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...
$objectives
addDefaults($a_name)
Adds the defaults of this test to the test defaults.
cloneObject(int $target_id, int $copy_id=0, bool $omit_tree=false)
Clone object.
removeTestResultsByUserIds(array $user_ids)
static collectFileItems(ilPageObject $a_page, DOMDocument $a_domdoc)
Get all file items that are used within the page.
static instantiateQuestion(int $question_id)
$metadata
A reference to an IMS compatible matadata set.
checkQuestionParent(int $question_id)
Interface for html sanitizing functionality.
evalStatistical($active_id)
Returns the statistical evaluation of the test for a specified user.
removeQuestionWithResults(int $question_id, TestScoring $scoring)
static getSingleManualFeedback(int $active_id, int $question_id, int $pass)
getHighscoreHints()
Gets, if the column with the number of requested hints should be shown.
getTotalPointsPassedArray()
Returns an array with the total points of all users who passed the test This array could be used for ...
cloneMetaData(ilObject $target_obj)
Copy meta data.
getAccessFilteredParticipantList()
getVisitingTimeOfParticipant(int $active_id)
getQuestiontext($question_id)
Returns the question text for a given question.
replaceMobsInPageImports(string $text, array $mappings)
static _lookupTitle(int $obj_id)
getAllQuestions($pass=null)
Returns all questions of a test in test order.
getHighscoreAnon()
Gets if the highscores should be anonymized per setting.
sendAdvancedNotification(int $active_id)
getTestParticipantsForManualScoring($filter=null)
fromXML(ilQTIAssessment $assessment, array $mappings)
Receives parameters from a QTI parser and creates a valid ILIAS test object.
storeMarkSchema(MarkSchema $mark_schema)
static _prepareCloneSelection(array $ref_ids, string $new_type, bool $show_path=true)
Prepare copy wizard object selection.
setActivationVisibility($a_value)
hasQuestionsWithoutQuestionpool()
evalTotalPersons()
Returns the number of persons who started the test.
retrieveMobsFromLegacyImports(string $text, array $mobs, string $importdir)
getStartingTimeOfUser($active_id, $pass=null)
Returns the unix timestamp of the time a user started a test.
sendSimpleNotification($active_id)
getShowSolutionPrintview()
Returns if the solution printview should be presented to the user or not.
updateTestResultCache(int $active_id, ilAssQuestionProcessLocker $process_locker=null)
hasNrOfTriesRestriction()
returns if the numbers of tries have to be checked
getAuthor()
Gets the authors name of the ilObjTest object.
static _getResultPass($active_id)
Retrieves the pass number that should be counted for a given user.
ilLanguage $lng
getTestDefaults($test_defaults_id)
getShowPassDetails()
Returns if the pass details should be shown when a test is not finished.
MarkSchema $mark_schema
$text
Definition: xapiexit.php:21
ilTestPageGUI: ilPageEditorGUI, ilEditClipboardGUI, ilMDEditorGUI ilTestPageGUI: ilPublicUserProfile...
removeQuestion(int $question_id)
isTestFinishedToViewResults($active_id, $currentpass)
Returns true if an active user completed a test pass and did not start a new pass.
getUserData($ids)
Returns a data of all users specified by id list.
questionMoveUp($question_id)
Moves a question up in order.
static delDir(string $a_dir, bool $a_clean_only=false)
removes a dir and all its content (subdirs and files) recursively
const NEWS_NOTICE
MainSettings $main_settings
const REDIRECT_NONE
static getInstanceByRefId(int $ref_id, bool $stop_on_error=true)
get an instance of an Ilias object by reference id
static _getSolutionMaxPass(int $question_id, int $active_id)
Returns the maximum pass a users question solution.
isSkillServiceToBeConsidered()
Returns whether this test must consider skills, usually by providing appropriate extensions in the us...
Class ilObjFile.
static _getCountSystem($active_id)
global $DIC
Definition: shib_login.php:25
static _getSolvedQuestions($active_id, $question_fi=null)
get solved questions
getResultsForActiveId(int $active_id)
static createDirectory(string $a_dir, int $a_mod=0755)
create directory
ilTestEditPageGUI: ilPageEditorGUI, ilEditClipboardGUI, ilMDEditorGUI ilTestEditPageGUI: ilPublicUse...
isOfferingQuestionHintsEnabled()
static _refreshStatus(int $a_obj_id, ?array $a_users=null)
Filesystem $filesystem_web
exportXMLMediaObjects(&$a_xml_writer, $a_inst, $a_target_dir, &$expLog)
export media objects to xml (see ilias_co.dtd)
Class ilObjForumAdministration.
getAnsweredQuestionCount($active_id, $pass=null)
Retrieves the number of answered questions for a given user in a given test.
getAvailableDefaults()
Returns the available test defaults for the active user.
convertTimeToDateTimeImmutableIfNecessary(DateTimeImmutable|int|null $date_time)
static getInstanceByType(string $type)
static isSkillManagementGloballyActivated()
canShowTestResults(ilTestSession $test_session)
createExportDirectory()
creates data directory for export files (data_dir/tst_data/tst_<id>/export, depending on data directo...
const CLIENT_WEB_DIR
Definition: constants.php:47
static _lookupAuthor($obj_id)
Gets the authors name of the ilObjTest object.
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, instead it parses the serialized rbac_pa entries.
removeTestActives(array $active_ids)
$results
copyQuestions(array $question_ids)
getStartingTimeOfParticipants()
Note, this function should only be used if absolutely necessary, since it perform joins on tables tha...
RequestDataCollector $testrequest
static getDataDir()
get data directory (outside webspace)
getShowSolutionSignature()
Returns if the signature field should be shown in the test results.
$txt
Definition: error.php:30
bool $activation_limited
static _exists(int $id, bool $reference=false, ?string $type=null)
static _lookupFinishedUserTests($a_user_id)
Gather all finished tests for user.
pcArrayShuffle($array)
Shuffles the values of a given array.
ilTestQuestionSetConfigFactory $question_set_config_factory
static _getMobsOfObject(string $a_type, int $a_id, int $a_usage_hist_nr=0, string $a_lang="-")
const SCORE_LAST_PASS
getCompleteEvaluationData($filterby='', $filtertext='')
static _lookupDescription(int $obj_id)
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
static _getTestIDFromObjectID($object_id)
Returns the ILIAS test id for a given object id.
getInstantFeedbackSolution()
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.
getAllTestResults($participants)
returns all test results for all participants
removeQuestionsWithResults(array $question_ids)
isHighscoreAnon()
Gets if the highscores should be displayed anonymized.
$filename
Definition: buildRTE.php:78
isInstantFeedbackAnswerFixationEnabled()
prepareTextareaOutput($txt_output, $prepare_for_latex_output=false, $omitNl2BrWhenTextArea=false)
Prepares a string for a text area output in tests.
storeActivationSettings(?bool $is_activation_limited=false, ?int $activation_starting_time=null, ?int $activation_ending_time=null, bool $activation_visibility=false,)
static _createImportDirectory()
creates data directory for import files (data_dir/tst_data/tst_<id>/import, depending on data directo...
buildImportDirectoryFromImportFile(string $file_to_import)
updatePassAndTestResults(array $active_ids)
duplicateQuestionForTest($question_id)
Takes a question and creates a copy of the question for use in the test.
addIntroductionToSettingsFromImport(SettingsIntroduction $settings, array $material, string $importdir, array $mappings)
evalTotalParticipantsArray(string $name_sort_order='asc')
getAvailableQuestions($arr_filter, $completeonly=0)
Calculates the available questions for a test.
isForceInstantFeedbackEnabled()
static _saveTempFileAsMediaObject(string $name, string $tmp_name, bool $upload=true)
getQuestionDataset($question_id)
Returns the dataset for a given question id.
static ilTempnam(?string $a_temp_path=null)
Returns a unique and non existing Path for e temporary file or directory.
withConcludingRemarksPageId(?int $concluding_remarks_page_id)
ilObjectProperties $object_properties
removeQuestions(array $question_ids)
removeTestResultsByActiveIds(array $active_ids)
A news item can be created by different sources.
getGenericAnswerFeedback()
static _getScoreCutting(int $active_id)
Determines if the score of a question should be cut at 0 points or the score of the whole test...
removeQuestionFromSequences(int $question_id, array $active_ids, ilTestReindexedSequencePositionMap $reindexed_sequence_position_map)
addToNewsOnOnline(bool $old_online_status, bool $new_online_status)
static getItem(int $ref_id)
buildIso8601PeriodForExportCompatibility(DateTimeImmutable $date_time)
int $activation_ending_time
getTextAnswer($active_id, $question_id, $pass=null)
Returns the text answer of a given user for a given question.
getScoreCutting()
Determines if the score of a question should be cut at 0 points or the score of the whole test...
getTitleFilenameCompliant()
returns the object title prepared to be used as a filename
bool $activation_visibility
getParticipantsForTestAndQuestion($test_id, $question_id)
Creates an associated array with all active id&#39;s for a given test and original question id...
setTmpCopyWizardCopyId(int $tmpCopyWizardCopyId)
global $ilSetting
Definition: privfeed.php:32
static _getAvailableTests($use_object_id=false)
Returns the available tests for the active user.
$id
plugin.php for ilComponentBuildPluginInfoObjectiveTest::testAddPlugins
Definition: plugin.php:24
__construct(Container $dic, ilPlugin $plugin)
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.
getQuestionCountAndPointsForPassOfParticipant(int $active_id, int $pass)
exportPagesXML(&$a_xml_writer, $a_inst, $a_target_dir, &$expLog)
export pages of test to xml (see ilias_co.dtd)
recalculateScores($preserve_manscoring=false)
clonePage(int $source_page_id)
ScoreSettings $score_settings
ScoreSettingsRepository $score_settings_repo
deleteDefaults($test_default_id)
endingTimeReached()
Returns true if the ending time of a test is reached An ending time is not available for self assessm...
LOMetadata $lo_metadata
getGeneralQuestionPropertiesRepository()
getCompleteManualFeedback(int $question_id)
Retrieves the manual feedback for a question in a test.
getParticipants()
Returns all persons who started the test.
const QUESTION_SET_TYPE_FIXED
getConcludingRemarksPageId()
isActiveTestSubmitted($user_id=null)
returns if the active for user_id has been submitted
const REDIRECT_ALWAYS
modifyExportIdentifier($a_tag, $a_param, $a_value)
Returns the installation id for a given identifier.
getQuestionTitlesAndIndexes()
Returns the titles of the test questions in question sequence.
getQuestionType($question_id)
Returns the question type of a question with a given id.
static buildExamId($active_id, $pass, $test_obj_id=null)
xmlStartTag(string $tag, ?array $attrs=null, bool $empty=false, bool $encode=true, bool $escape=true)
Writes a starttag.
Class ilBenchmark.
GeneralQuestionPropertiesRepository $questionrepository
string $author
isTestQuestion(int $question_id)
static _getPassScoring(int $active_id)
Gets the pass scoring type.
& getCompleteWorkingTimeOfParticipants()
Returns the complete working time in seconds for all test participants.
TestLogger $logger
getNrOfResultsForPass($active_id, $pass)
Calculates the number of user results for a specific test pass.
ilComponentFactory $component_factory
addQTIMaterial(ilXmlWriter &$xml_writer, ?int $page_id, string $material='')
const NEWS_USERS
xmlElement(string $tag, $attrs=null, $data=null, $encode=true, $escape=true)
Writes a basic element (no children, just textual content)
isShowGradingStatusEnabled()
canShowSolutionPrintview($user_id=null)
getQuestionCountWithoutReloading()
getHighscoreScore()
Gets if the score column should be shown.
static getTestObjIdsWithActiveForUserId($userId)
isPluginActive($a_pname)
Checks wheather or not a question plugin with a given name is active.
isFollowupQuestionAnswerFixationEnabled()
saveManualFeedback(int $active_id, int $question_id, int $pass, ?string $feedback, bool $finalized=false)
saveAuthorToMetadata($author="")
Saves an authors name into the lifecycle metadata if no lifecycle metadata exists This will only be c...
static getManualFeedback(int $active_id, int $question_id, ?int $pass)
Retrieves the feedback comment for a question in a test if it is finalized.
getScoreSettingsRepository()
static insertInstIntoID(string $a_value)
inserts installation id into ILIAS id
getWorkingTimeOfParticipantForPass(int $active_id, int $pass)
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...
deliverPDFfromHTML($content, $title=null)
Delivers a PDF file from XHTML.
getDetailedTestResults($participants)
returns all test results for all participants
static getInstance(int $obj_id)
static clear(string $a_var)
isNrOfTriesReached($tries)
returns if number of tries are reached
Class ilObjectActivation.
_getLastAccess(int $active_id)
setAccessFilteredParticipantList(ilTestParticipantList $access_filtered_participant_list)
static deleteRequestsByActiveIds($activeIds)
Deletes all hint requests relating to a testactive included in given active ids.
const INVITATION_OFF
insertManualFeedback(int $active_id, int $question_id, int $pass, ?string $feedback, bool $finalized, array $feedback_old)
const REDIRECT_KIOSK
insertQuestion(int $question_id, bool $link_only=false)
reindexFixedQuestionOrdering()
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.
ilTestParticipantAccessFilterFactory $participant_access_filter
const SCORE_BEST_PASS
userLookupFullName($user_id, $overwrite_anonymity=false, $sorted_order=false, $suffix="")
Returns the full name of a test user according to the anonymity status.
static _getActiveIdOfUser($user_id="", $test_id="")
static makeDir(string $a_dir)
creates a new directory and inherits all filesystem permissions of the parent directory You may pass ...
isMaxProcessingTimeReached(int $starting_time, int $active_id)
Returns whether the maximum processing time for a test is reached or not.
isPreviousSolutionReuseEnabled($active_id)
ilObjUser $user
getImagePath()
Returns the image path for web accessable images of a test The image path is under the CLIENT_WEB_DIR...
static lookupExamId($active_id, $pass)
removeTestResultsFromSoapLpAdministration(array $user_ids)
getPassScoring()
Gets the pass scoring type.