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