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