ILIAS  release_9 Revision v9.13-25-g2c18ec4c24f
class.ilObjTest.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
26 
27 require_once 'Modules/Test/classes/inc.AssessmentConstants.php';
28 
39 class ilObjTest extends ilObject implements ilMarkSchemaAware
40 {
41  public const QUESTION_SET_TYPE_FIXED = 'FIXED_QUEST_SET';
42  public const QUESTION_SET_TYPE_RANDOM = 'RANDOM_QUEST_SET';
43 
44 
45  public const REDIRECT_NONE = 0;
46  public const REDIRECT_ALWAYS = 1;
47  public const REDIRECT_KIOSK = 2;
48 
49  private ?bool $activation_limited = null;
50  private array $mob_ids;
51  private array $file_ids = [];
52  private bool $online;
53  protected \ILIAS\TestQuestionPool\QuestionInfoService $questioninfo;
56  public int $test_id = -1;
58  public string $author;
59 
63  public $metadata;
64  public array $questions = [];
65 
70 
74  public $test_sequence = false;
75 
76  private ?bool $has_obligations = null;
78 
79  private int $template_id = 0;
80 
81  protected bool $print_best_solution_with_result = true;
82 
83  protected bool $activation_visibility = false;
84  protected ?int $activation_starting_time = null;
85  protected ?int $activation_ending_time = null;
86 
92  private $participantDataExist = null;
93 
94  protected bool $testFinalBroken = false;
95 
96  private ?int $tmpCopyWizardCopyId = null;
97 
100  protected Refinery $refinery;
101  protected ilSetting $settings;
102  protected ilBenchmark $bench;
108 
110 
114 
116 
123  public function __construct(int $id = 0, bool $a_call_by_reference = true)
124  {
125  $this->type = "tst";
126 
128  global $DIC;
129  $this->ctrl = $DIC['ilCtrl'];
130  $this->refinery = $DIC['refinery'];
131  $this->settings = $DIC['ilSetting'];
132  $this->bench = $DIC['ilBench'];
133  $this->testrequest = $DIC->test()->internal()->request();
134  $this->component_repository = $DIC['component.repository'];
135  $this->component_factory = $DIC['component.factory'];
136  $this->filesystem_web = $DIC->filesystem()->web();
137 
138  $local_dic = $this->getLocalDIC();
139  $this->participant_access_filter = $local_dic['participantAccessFilterFactory'];
140  $this->testManScoringDoneHelper = $local_dic['manScoringDoneHelper'];
141 
142  $this->mark_schema = new ASS_MarkSchema($DIC['ilDB'], $DIC['lng'], $DIC['ilUser']->getId());
143  $this->mark_schema->createSimpleSchema(
144  $DIC->language()->txt("failed_short"),
145  $DIC->language()->txt("failed_official"),
146  0,
147  0,
148  $DIC->language()->txt("passed_short"),
149  $DIC->language()->txt("passed_official"),
150  50,
151  1
152  );
153 
154  parent::__construct($id, $a_call_by_reference);
155 
156  $this->lng->loadLanguageModule("assessment");
157  $this->questioninfo = $DIC->testQuestionPool()->questionInfo();
158  $this->score_settings = null;
159 
160  $this->question_set_config_factory = new ilTestQuestionSetConfigFactory(
161  $this->tree,
162  $this->db,
163  $this->lng,
164  $this->log,
165  $this->component_repository,
166  $this,
167  $this->questioninfo
168  );
169  }
170 
171  public function getLocalDIC(): ILIAS\DI\Container
172  {
173  return ilTestDIC::dic();
174  }
175 
177  {
178  return $this->question_set_config_factory->getQuestionSetConfig();
179  }
180 
184  public function getTitleFilenameCompliant(): string
185  {
186  return ilFileUtils::getASCIIFilename($this->getTitle());
187  }
188 
189  public function getTmpCopyWizardCopyId(): ?int
190  {
192  }
193 
194  public function setTmpCopyWizardCopyId(int $tmpCopyWizardCopyId): void
195  {
196  $this->tmpCopyWizardCopyId = $tmpCopyWizardCopyId;
197  }
198 
199  public function create(): int
200  {
201  $id = parent::create();
202  $this->createMetaData();
203  return $id;
204  }
205 
206  public function update(): bool
207  {
208  if (!parent::update()) {
209  return false;
210  }
211 
212  // put here object specific stuff
213  $this->updateMetaData();
214  return true;
215  }
216 
217  public function read(): void
218  {
219  parent::read();
220  $this->main_settings = null;
221  $this->score_settings = null;
222  $this->loadFromDb();
223  }
224 
225  public function delete(): bool
226  {
227  // always call parent delete function first!!
228  if (!parent::delete()) {
229  return false;
230  }
231 
232  // delet meta data
233  $this->deleteMetaData();
234 
235  //put here your module specific stuff
236  $this->deleteTest();
237 
238  $qsaImportFails = new ilAssQuestionSkillAssignmentImportFails($this->getId());
239  $qsaImportFails->deleteRegisteredImportFails();
240  $sltImportFails = new ilTestSkillLevelThresholdImportFails($this->getId());
241  $sltImportFails->deleteRegisteredImportFails();
242 
243  return true;
244  }
245 
246  public function deleteTest(): void
247  {
248  $participantData = new ilTestParticipantData($this->db, $this->lng);
249  $participantData->load($this->getTestId());
250  $this->removeTestResults($participantData);
251 
252  $this->db->manipulateF(
253  "DELETE FROM tst_mark WHERE test_fi = %s",
254  ['integer'],
255  [$this->getTestId()]
256  );
257 
258  $this->db->manipulateF(
259  "DELETE FROM tst_tests WHERE test_id = %s",
260  ['integer'],
261  [$this->getTestId()]
262  );
263 
269  if ($this->main_settings !== null
270  && ($this->isFixedTest() || $this->isRandomTest())) {
271  $this->question_set_config_factory->getQuestionSetConfig()->removeQuestionSetRelatedData();
272  }
273 
274  $tst_data_dir = ilFileUtils::getDataDir() . "/tst_data";
275  $directory = $tst_data_dir . "/tst_" . $this->getId();
276  if (is_dir($directory)) {
277  ilFileUtils::delDir($directory);
278  }
279  $mobs = ilObjMediaObject::_getMobsOfObject("tst:html", $this->getId());
280  // remaining usages are not in text anymore -> delete them
281  // and media objects (note: delete method of ilObjMediaObject
282  // checks whether object is used in another context; if yes,
283  // the object is not deleted!)
284  foreach ($mobs as $mob) {
285  ilObjMediaObject::_removeUsage($mob, "tst:html", $this->getId());
286  if (ilObjMediaObject::_exists($mob)) {
287  $mob_obj = new ilObjMediaObject($mob);
288  $mob_obj->delete();
289  }
290  }
291  }
292 
298  public function createExportDirectory(): void
299  {
300  $tst_data_dir = ilFileUtils::getDataDir() . "/tst_data";
301  ilFileUtils::makeDir($tst_data_dir);
302  if (!is_writable($tst_data_dir)) {
303  $this->ilias->raiseError("Test Data Directory (" . $tst_data_dir
304  . ") not writeable.", $this->ilias->error_obj->MESSAGE);
305  }
306 
307  // create learning module directory (data_dir/lm_data/lm_<id>)
308  $tst_dir = $tst_data_dir . "/tst_" . $this->getId();
309  ilFileUtils::makeDir($tst_dir);
310  if (!@is_dir($tst_dir)) {
311  $this->ilias->raiseError("Creation of Test Directory failed.", $this->ilias->error_obj->MESSAGE);
312  }
313  // create Export subdirectory (data_dir/lm_data/lm_<id>/Export)
314  $export_dir = $tst_dir . "/export";
315  ilFileUtils::makeDir($export_dir);
316  if (!@is_dir($export_dir)) {
317  $this->ilias->raiseError("Creation of Export Directory failed.", $this->ilias->error_obj->MESSAGE);
318  }
319  }
320 
321  public function getExportDirectory(): string
322  {
323  $export_dir = ilFileUtils::getDataDir() . "/tst_data" . "/tst_" . $this->getId() . "/export";
324  return $export_dir;
325  }
326 
327  public function getExportFiles(string $dir = ''): array
328  {
329  // quit if import dir not available
330  if (!@is_dir($dir) || !is_writable($dir)) {
331  return [];
332  }
333 
334  $files = [];
335  foreach (new DirectoryIterator($dir) as $file) {
339  if ($file->isDir()) {
340  continue;
341  }
342 
343  $files[] = $file->getBasename();
344  }
345 
346  sort($files);
347 
348  return $files;
349  }
350 
351  public static function _setImportDirectory($a_import_dir = null): void
352  {
353  if ($a_import_dir !== null) {
354  ilSession::set('tst_import_dir', $a_import_dir);
355  return;
356  }
357 
358  ilSession::clear('tst_import_dir');
359  }
360 
366  public static function _getImportDirectory()
367  {
368  if (strlen(ilSession::get('tst_import_dir'))) {
369  return ilSession::get('tst_import_dir');
370  }
371  return null;
372  }
373 
375  public function getImportDirectory()
376  {
378  }
379 
385  public static function _createImportDirectory(): string
386  {
387  global $DIC;
388  $ilias = $DIC['ilias'];
389  $tst_data_dir = ilFileUtils::getDataDir() . "/tst_data";
390  ilFileUtils::makeDir($tst_data_dir);
391 
392  if (!is_writable($tst_data_dir)) {
393  $ilias->raiseError("Test Data Directory (" . $tst_data_dir
394  . ") not writeable.", $ilias->error_obj->FATAL);
395  }
396 
397  // create test directory (data_dir/tst_data/tst_import)
398  $tst_dir = $tst_data_dir . "/tst_import";
399  ilFileUtils::makeDir($tst_dir);
400  if (!@is_dir($tst_dir)) {
401  $ilias->raiseError("Creation of test import directory failed.", $ilias->error_obj->FATAL);
402  }
403 
404  // assert that this is empty and does not contain old data
405  ilFileUtils::delDir($tst_dir, true);
406 
407  return $tst_dir;
408  }
409 
410  final public function isComplete(ilTestQuestionSetConfig $testQuestionSetConfig): bool
411  {
412  if (!count($this->mark_schema->mark_steps)) {
413  return false;
414  }
415 
416  if (!$testQuestionSetConfig->isQuestionSetConfigured()) {
417  return false;
418  }
419 
420  return true;
421  }
422 
423  public function saveCompleteStatus(ilTestQuestionSetConfig $testQuestionSetConfig): void
424  {
425  $complete = 0;
426  if ($this->isComplete($testQuestionSetConfig)) {
427  $complete = 1;
428  }
429  if ($this->getTestId() > 0) {
430  $this->db->manipulateF(
431  "UPDATE tst_tests SET complete = %s WHERE test_id = %s",
432  ['text', 'integer'],
433  [$complete, $this->test_id]
434  );
435  }
436  }
437 
438  public function saveToDb(bool $properties_only = false): void
439  {
440  if ($this->test_id === -1) {
441  // Create new dataset
442  $next_id = $this->db->nextId('tst_tests');
443 
444  $this->db->insert(
445  'tst_tests',
446  [
447  'test_id' => ['integer', $next_id],
448  'obj_fi' => ['integer', $this->getId()],
449  'author' => ['text', mb_substr($this->getAuthor(), 0, 50)],
450  'created' => ['integer', time()],
451  'tstamp' => ['integer', time()],
452  'template_id' => ['integer', $this->getTemplate()],
453  'broken' => ['integer', (int) $this->isTestFinalBroken()]
454  ]
455  );
456 
457  $this->test_id = $next_id;
458 
460  $this->logAction($this->lng->txtlng("assessment", "log_create_new_test", ilObjAssessmentFolder::_getLogLanguage()));
461  }
462  } else {
463  // Modify existing dataset
464  $oldrow = [];
466  $result = $this->db->queryF(
467  "SELECT * FROM tst_tests WHERE test_id = %s",
468  ['integer'],
469  [$this->test_id]
470  );
471  if ($result->numRows() == 1) {
472  $oldrow = $this->db->fetchAssoc($result);
473  }
474  }
475 
476  $this->db->update(
477  'tst_tests',
478  [
479  'author' => ['text', mb_substr($this->getAuthor(), 0, 50)],
480  'broken' => ['integer', (int) $this->isTestFinalBroken()]
481  ],
482  [
483  'test_id' => ['integer', $this->getTestId()]
484  ]
485  );
486 
488  $logresult = $this->db->queryF(
489  "SELECT * FROM tst_tests WHERE test_id = %s",
490  ['integer'],
491  [$this->getTestId()]
492  );
493  $newrow = [];
494  if ($logresult->numRows() == 1) {
495  $newrow = $this->db->fetchAssoc($logresult);
496  }
497  $changed_fields = [];
498  foreach ($oldrow as $key => $value) {
499  if ($oldrow[$key] !== $newrow[$key]) {
500  array_push($changed_fields, "$key: " . $oldrow[$key] . " => " . $newrow[$key]);
501  }
502  }
503  $changes = join(", ", $changed_fields);
504  if (count($changed_fields) > 0) {
505  $this->logAction($this->lng->txtlng("assessment", "log_modified_test", ilObjAssessmentFolder::_getLogLanguage()) . " [" . $changes . "]");
506  }
507  }
508  if ($this->evalTotalPersons() > 0) {
509  // reset the finished status of participants if the nr of test passes did change
510  if ($this->getNrOfTries() > 0) {
511  // set all unfinished tests with nr of passes >= allowed passes finished
512  $aresult = $this->db->queryF(
513  "SELECT active_id FROM tst_active WHERE test_fi = %s AND tries >= %s AND submitted = %s",
514  ['integer', 'integer', 'integer'],
515  [$this->getTestId(), $this->getNrOfTries(), 0]
516  );
517  while ($row = $this->db->fetchAssoc($aresult)) {
518  $this->db->manipulateF(
519  "UPDATE tst_active SET submitted = %s, submittimestamp = %s WHERE active_id = %s",
520  ['integer', 'timestamp', 'integer'],
521  [1, date('Y-m-d H:i:s'), $row["active_id"]]
522  );
523  }
524 
525  // set all finished tests with nr of passes < allowed passes not finished
526  $aresult = $this->db->queryF(
527  "SELECT active_id FROM tst_active WHERE test_fi = %s AND tries < %s AND submitted = %s",
528  ['integer', 'integer', 'integer'],
529  [$this->getTestId(), $this->getNrOfTries() - 1, 1]
530  );
531  while ($row = $this->db->fetchAssoc($aresult)) {
532  $this->db->manipulateF(
533  "UPDATE tst_active SET submitted = %s, submittimestamp = %s WHERE active_id = %s",
534  ['integer', 'timestamp', 'integer'],
535  [0, null, $row["active_id"]]
536  );
537  }
538  } else {
539  // set all finished tests with nr of passes >= allowed passes not finished
540  $aresult = $this->db->queryF(
541  "SELECT active_id FROM tst_active WHERE test_fi = %s AND submitted = %s",
542  ['integer', 'integer'],
543  [$this->getTestId(), 1]
544  );
545  while ($row = $this->db->fetchAssoc($aresult)) {
546  $this->db->manipulateF(
547  "UPDATE tst_active SET submitted = %s, submittimestamp = %s WHERE active_id = %s",
548  ['integer', 'timestamp', 'integer'],
549  [0, null, $row["active_id"]]
550  );
551  }
552  }
553  }
554  }
555 
557  $this->isActivationLimited(),
558  $this->getActivationStartingTime(),
559  $this->getActivationEndingTime(),
560  $this->getActivationVisibility(),
561  );
562 
563  if (!$properties_only) {
564  if ($this->getQuestionSetType() == self::QUESTION_SET_TYPE_FIXED) {
565  $this->saveQuestionsToDb();
566  }
567 
568  $this->mark_schema->saveToDb($this->test_id);
569  }
570  }
571 
572  public function saveQuestionsToDb(): void
573  {
574  $oldquestions = [];
576  $result = $this->db->queryF(
577  "SELECT question_fi FROM tst_test_question WHERE test_fi = %s ORDER BY sequence",
578  ['integer'],
579  [$this->getTestId()]
580  );
581  if ($result->numRows() > 0) {
582  while ($row = $this->db->fetchAssoc($result)) {
583  array_push($oldquestions, $row["question_fi"]);
584  }
585  }
586  }
587  // workaround for lost obligations
588  // this method is called if a question is removed
589  $currentQuestionsObligationsQuery = 'SELECT question_fi, obligatory FROM tst_test_question WHERE test_fi = %s';
590  $rset = $this->db->queryF($currentQuestionsObligationsQuery, ['integer'], [$this->getTestId()]);
591  while ($row = $this->db->fetchAssoc($rset)) {
592  $obligatoryQuestionState[$row['question_fi']] = $row['obligatory'];
593  }
594  // delete existing category relations
595  $this->db->manipulateF(
596  "DELETE FROM tst_test_question WHERE test_fi = %s",
597  ['integer'],
598  [$this->getTestId()]
599  );
600  // create new category relations
601  foreach ($this->questions as $key => $value) {
602  // workaround for import witout obligations information
603  if (!isset($obligatoryQuestionState[$value]) || is_null($obligatoryQuestionState[$value])) {
604  $obligatoryQuestionState[$value] = 0;
605  }
606 
607  // insert question
608  $next_id = $this->db->nextId('tst_test_question');
609  $this->db->insert('tst_test_question', [
610  'test_question_id' => ['integer', $next_id],
611  'test_fi' => ['integer', $this->getTestId()],
612  'question_fi' => ['integer', $value],
613  'sequence' => ['integer', $key],
614  'obligatory' => ['integer', $obligatoryQuestionState[$value]],
615  'tstamp' => ['integer', time()]
616  ]);
617  }
619  $result = $this->db->queryF(
620  "SELECT question_fi FROM tst_test_question WHERE test_fi = %s ORDER BY sequence",
621  ['integer'],
622  [$this->getTestId()]
623  );
624  $newquestions = [];
625  if ($result->numRows() > 0) {
626  while ($row = $this->db->fetchAssoc($result)) {
627  array_push($newquestions, $row["question_fi"]);
628  }
629  }
630  foreach ($oldquestions as $index => $question_id) {
631  if (!isset($newquestions[$index]) || $newquestions[$index] !== $question_id) {
632  $pos = array_search($question_id, $newquestions);
633  if ($pos === false) {
634  $this->logAction($this->lng->txtlng("assessment", "log_question_removed", ilObjAssessmentFolder::_getLogLanguage()), $question_id);
635  } else {
636  $this->logAction($this->lng->txtlng("assessment", "log_question_position_changed", ilObjAssessmentFolder::_getLogLanguage()) . ": " . ($index + 1) . " => " . ($pos + 1), $question_id);
637  }
638  }
639  }
640  foreach ($newquestions as $index => $question_id) {
641  if (array_search($question_id, $oldquestions) === false) {
642  $this->logAction($this->lng->txtlng("assessment", "log_question_added", ilObjAssessmentFolder::_getLogLanguage()) . ": " . ($index + 1), $question_id);
643  }
644  }
645  }
646  }
647 
651  public function getNrOfResultsForPass($active_id, $pass): int
652  {
653  $result = $this->db->queryF(
654  "SELECT test_result_id FROM tst_test_result WHERE active_fi = %s AND pass = %s",
655  ['integer','integer'],
656  [$active_id, $pass]
657  );
658  return $result->numRows();
659  }
660 
661  public function loadFromDb(): void
662  {
663  $result = $this->db->queryF(
664  "SELECT test_id FROM tst_tests WHERE obj_fi = %s",
665  ['integer'],
666  [$this->getId()]
667  );
668  if ($result->numRows() === 1) {
669  $data = $this->db->fetchObject($result);
670  $this->setTestId($data->test_id);
671 
672  $this->mark_schema->flush();
673  $this->mark_schema->loadFromDb($this->getTestId());
674 
675  $this->loadQuestions();
676  }
677 
678  // moved activation to ilObjectActivation
679  if (isset($this->ref_id)) {
680  $activation = ilObjectActivation::getItem($this->ref_id);
681  switch ($activation["timing_type"]) {
683  $this->setActivationLimited(true);
684  $this->setActivationStartingTime($activation["timing_start"]);
685  $this->setActivationEndingTime($activation["timing_end"]);
686  $this->setActivationVisibility($activation["visible"]);
687  break;
688 
689  default:
690  $this->setActivationLimited(false);
691  break;
692  }
693  }
694  }
695 
700  public function loadQuestions(int $active_id = 0, ?int $pass = null): void
701  {
702  $this->questions = [];
703  if ($this->isRandomTest()) {
704  if ($active_id === 0) {
705  $active_id = $this->getActiveIdOfUser($this->user->getId());
706  }
707  if (is_null($pass)) {
708  $pass = self::_getPass($active_id);
709  }
710  $result = $this->db->queryF(
711  "SELECT tst_test_rnd_qst.* FROM tst_test_rnd_qst, qpl_questions WHERE tst_test_rnd_qst.active_fi = %s AND qpl_questions.question_id = tst_test_rnd_qst.question_fi AND tst_test_rnd_qst.pass = %s ORDER BY sequence",
712  ['integer', 'integer'],
713  [$active_id, $pass]
714  );
715  // The following is a fix for random tests prior to ILIAS 3.8. If someone started a random test in ILIAS < 3.8, there
716  // is only one test pass (pass = 0) in tst_test_rnd_qst while with ILIAS 3.8 there are questions for every test pass.
717  // To prevent problems with tests started in an older version and continued in ILIAS 3.8, the first pass should be taken if
718  // no questions are present for a newer pass.
719  if ($result->numRows() == 0) {
720  $result = $this->db->queryF(
721  "SELECT tst_test_rnd_qst.* FROM tst_test_rnd_qst, qpl_questions WHERE tst_test_rnd_qst.active_fi = %s AND qpl_questions.question_id = tst_test_rnd_qst.question_fi AND tst_test_rnd_qst.pass = 0 ORDER BY sequence",
722  ['integer'],
723  [$active_id]
724  );
725  }
726  } else {
727  $result = $this->db->queryF(
728  "SELECT tst_test_question.* FROM tst_test_question, qpl_questions WHERE tst_test_question.test_fi = %s AND qpl_questions.question_id = tst_test_question.question_fi ORDER BY sequence",
729  ['integer'],
730  [$this->test_id]
731  );
732  }
733  $index = 1;
734  if ($this->test_id !== -1) {
735  //Omit loading of questions for non-id'ed test
736  while ($data = $this->db->fetchAssoc($result)) {
737  $this->questions[$index++] = $data["question_fi"];
738  }
739  }
740  }
741 
742  public function getIntroduction(): string
743  {
744  $page_id = $this->getMainSettings()->getIntroductionSettings()->getIntroductionPageId();
745  if ($page_id !== null) {
746  return (new ilTestPageGUI('tst', $page_id))->showPage();
747  }
748 
749  return $this->getMainSettings()->getIntroductionSettings()->getIntroductionText();
750  }
751 
752  private function cloneIntroduction(): ?int
753  {
754  $page_id = $this->getMainSettings()->getIntroductionSettings()->getIntroductionPageId();
755  if ($page_id === null) {
756  return null;
757  }
758  return $this->clonePage($page_id);
759  }
760 
761  public function getFinalStatement(): string
762  {
763  $page_id = $this->getMainSettings()->getFinishingSettings()->getConcludingRemarksPageId();
764  if ($page_id !== null) {
765  return (new ilTestPageGUI('tst', $page_id))->showPage();
766  }
767  return $this->getMainSettings()->getFinishingSettings()->getConcludingRemarksText();
768  }
769 
770  private function cloneConcludingRemarks(): ?int
771  {
772  $page_id = $this->getMainSettings()->getFinishingSettings()->getConcludingRemarksPageId();
773  if ($page_id === null) {
774  return null;
775  }
776  return $this->clonePage($page_id);
777  }
778 
779  private function clonePage(int $source_page_id): int
780  {
781  $page_object = new ilTestPage();
782  $page_object->setParentId($this->getId());
783  $new_page_id = $page_object->createPageWithNextId();
784  (new ilTestPage($source_page_id))->copy($new_page_id);
785  return $new_page_id;
786  }
787 
795  public function getTestId(): int
796  {
797  return $this->test_id;
798  }
799 
800  public function isPostponingEnabled(): bool
801  {
802  return $this->getMainSettings()->getParticipantFunctionalitySettings()->getPostponedQuestionsMoveToEnd();
803  }
804 
812  public function getScoreReporting(): int
813  {
814  return $this->getScoreSettings()->getResultSummarySettings()->getScoreReporting();
815  }
816 
817  public function isScoreReportingEnabled(): bool
818  {
819  switch ($this->getScoreSettings()->getResultSummarySettings()->getScoreReporting()) {
824 
825  return true;
826 
828  default:
829 
830  return false;
831  }
832  }
833 
834  public function getAnswerFeedbackPoints(): bool
835  {
836  return $this->getMainSettings()->getQuestionBehaviourSettings()->getInstantFeedbackPointsEnabled();
837  }
838 
839  public function getGenericAnswerFeedback(): bool
840  {
841  return $this->getMainSettings()->getQuestionBehaviourSettings()->getInstantFeedbackGenericEnabled();
842  }
843 
844  public function getInstantFeedbackSolution(): bool
845  {
846  return $this->getMainSettings()->getQuestionBehaviourSettings()->getInstantFeedbackSolutionEnabled();
847  }
848 
849  public function getCountSystem(): int
850  {
851  return $this->getScoreSettings()->getScoringSettings()->getCountSystem();
852  }
853 
854  public static function _getCountSystem($active_id)
855  {
856  global $DIC;
857  $ilDB = $DIC['ilDB'];
858  $result = $ilDB->queryF(
859  "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",
860  ['integer'],
861  [$active_id]
862  );
863  if ($result->numRows()) {
864  $row = $ilDB->fetchAssoc($result);
865  return $row["count_system"];
866  }
867  return false;
868  }
869 
877  public function getScoreCutting(): int
878  {
879  return $this->getScoreSettings()->getScoringSettings()->getScoreCutting();
880  }
881 
889  public function getPassScoring(): int
890  {
891  return $this->getScoreSettings()->getScoringSettings()->getPassScoring();
892  }
893 
901  public static function _getPassScoring($active_id): int
902  {
903  global $DIC;
904  $ilDB = $DIC['ilDB'];
905  $result = $ilDB->queryF(
906  "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",
907  ['integer'],
908  [$active_id]
909  );
910  if ($result->numRows()) {
911  $row = $ilDB->fetchAssoc($result);
912  return (int) $row["pass_scoring"];
913  }
914  return 0;
915  }
916 
924  public static function _getScoreCutting($active_id): bool
925  {
926  global $DIC;
927  $ilDB = $DIC['ilDB'];
928  $result = $ilDB->queryF(
929  "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",
930  ['integer'],
931  [$active_id]
932  );
933  if ($result->numRows()) {
934  $row = $ilDB->fetchAssoc($result);
935  return (bool) $row["score_cutting"];
936  }
937  return false;
938  }
939 
949  {
950  return $this->getScoreSettings()->getResultSummarySettings()->getReportingDate();
951  }
952 
953  public function getNrOfTries(): int
954  {
955  return $this->getMainSettings()->getTestBehaviourSettings()->getNumberOfTries();
956  }
957 
958  public function isBlockPassesAfterPassedEnabled(): bool
959  {
960  return $this->getMainSettings()->getTestBehaviourSettings()->getBlockAfterPassedEnabled();
961  }
962 
963  public function getKioskMode(): bool
964  {
965  return $this->getMainSettings()->getTestBehaviourSettings()->getKioskModeEnabled();
966  }
967 
968  public function getShowKioskModeTitle(): bool
969  {
970  return $this->getMainSettings()->getTestBehaviourSettings()->getShowTitleInKioskMode();
971  }
972  public function getShowKioskModeParticipant(): bool
973  {
974  return $this->getMainSettings()->getTestBehaviourSettings()->getShowParticipantNameInKioskMode();
975  }
976 
977  public function getUsePreviousAnswers(): bool
978  {
979  return $this->getMainSettings()->getParticipantFunctionalitySettings()->getUsePreviousAnswerAllowed();
980  }
981 
982  public function getTitleOutput(): int
983  {
984  return $this->getMainSettings()->getQuestionBehaviourSettings()->getQuestionTitleOutputMode();
985  }
986 
987  public function isPreviousSolutionReuseEnabled($active_id): bool
988  {
989  $result = $this->db->queryF(
990  "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",
991  ["integer"],
992  [$active_id]
993  );
994  if ($result->numRows()) {
995  $row = $this->db->fetchAssoc($result);
996  $test_allows_reuse = $row["use_previous_answers"];
997  }
998 
999  if ($test_allows_reuse === '1') {
1000  $res = $this->user->getPref("tst_use_previous_answers");
1001  if ($res === '1') {
1002  return true;
1003  }
1004  }
1005  return false;
1006  }
1007 
1012  public function getProcessingTime(): ?string
1013  {
1014  return $this->getMainSettings()->getTestBehaviourSettings()->getProcessingTime();
1015  }
1016 
1020  public function getProcessingTimeAsArray(): array
1021  {
1022  $processing_time = $this->getMainSettings()->getTestBehaviourSettings()->getProcessingTime();
1023  if ($processing_time && $processing_time !== '') {
1024  if (preg_match("/(\d{2}):(\d{2}):(\d{2})/is", (string) $processing_time, $matches)) {
1025  return [
1026  'hh' => $matches[1],
1027  'mm' => $matches[2],
1028  'ss' => $matches[3],
1029  ];
1030  }
1031  }
1032  }
1033 
1034  public function getProcessingTimeAsMinutes()
1035  {
1036  if ($this->processing_time !== null) {
1037  if (preg_match("/(\d{2}):(\d{2}):(\d{2})/is", (string) $this->processing_time, $matches)) {
1038  return ($matches[1] * 60) + $matches[2];
1039  }
1040  }
1041 
1042  return self::DEFAULT_PROCESSING_TIME_MINUTES;
1043  }
1044 
1052  public function getProcessingTimeInSeconds($active_id = ""): int
1053  {
1054  $processing_time = $this->getMainSettings()->getTestBehaviourSettings()->getProcessingTime() ?? '';
1055  if (preg_match("/(\d{2}):(\d{2}):(\d{2})/", (string) $processing_time, $matches)) {
1056  $extratime = $this->getExtraTime($active_id) * 60;
1057  return ($matches[1] * 3600) + ($matches[2] * 60) + $matches[3] + $extratime;
1058  } else {
1059  return 0;
1060  }
1061  }
1062 
1063  public function getEnableProcessingTime(): bool
1064  {
1065  return $this->getMainSettings()->getTestBehaviourSettings()->getProcessingTimeEnabled();
1066  }
1067 
1068  public function getResetProcessingTime(): bool
1069  {
1070  return $this->getMainSettings()->getTestBehaviourSettings()->getResetProcessingTime();
1071  }
1072 
1073  public function isStartingTimeEnabled(): bool
1074  {
1075  return $this->getMainSettings()->getAccessSettings()->getStartTimeEnabled();
1076  }
1077 
1078  public function getStartingTime(): int
1079  {
1080  $start_time = $this->getMainSettings()->getAccessSettings()->getStartTime();
1081  return $start_time !== null ? $start_time->getTimestamp() : 0;
1082  }
1083 
1084  public function isEndingTimeEnabled(): bool
1085  {
1086  return $this->getMainSettings()->getAccessSettings()->getEndTimeEnabled();
1087  }
1088 
1089  public function getEndingTime(): int
1090  {
1091  $end_time = $this->getMainSettings()->getAccessSettings()->getEndTime();
1092  return $end_time !== null ? $end_time->getTimestamp() : 0;
1093  }
1094 
1095  public function getRedirectionMode(): int
1096  {
1097  return $this->getMainSettings()->getFinishingSettings()->getRedirectionMode();
1098  }
1099 
1100  public function isRedirectModeKiosk(): bool
1101  {
1102  return $this->getMainSettings()->getFinishingSettings()->getRedirectionMode() === self::REDIRECT_KIOSK;
1103  }
1104 
1105  public function isRedirectModeNone(): bool
1106  {
1107  return $this->getMainSettings()->getFinishingSettings()->getRedirectionMode() === self::REDIRECT_NONE;
1108  }
1109 
1110  public function getRedirectionUrl(): string
1111  {
1112  return $this->getMainSettings()->getFinishingSettings()->getRedirectionUrl() ?? '';
1113  }
1114 
1115  public function isPasswordEnabled(): bool
1116  {
1117  return $this->getMainSettings()->getAccessSettings()->getPasswordEnabled();
1118  }
1119 
1120  public function getPassword(): ?string
1121  {
1122  return $this->getMainSettings()->getAccessSettings()->getPassword();
1123  }
1124 
1130  public function removeQuestionFromSequences($questionId, $activeIds, ilTestReindexedSequencePositionMap $reindexedSequencePositionMap): void
1131  {
1132  $test_sequence_factory = new ilTestSequenceFactory(
1133  $this,
1134  $this->db,
1135  $this->questioninfo
1136  );
1137 
1138  foreach ($activeIds as $activeId) {
1139  $passSelector = new ilTestPassesSelector($this->db, $this);
1140  $passSelector->setActiveId($activeId);
1141 
1142  foreach ($passSelector->getExistingPasses() as $pass) {
1143  $test_sequence = $test_sequence_factory->getSequenceByActiveIdAndPass($activeId, $pass);
1144  $test_sequence->loadFromDb();
1145 
1146  $test_sequence->removeQuestion($questionId, $reindexedSequencePositionMap);
1147  $test_sequence->saveToDb();
1148  }
1149  }
1150  }
1151 
1155  public function removeQuestions(array $removeQuestionIds): void
1156  {
1157  foreach ($removeQuestionIds as $value) {
1158  $this->removeQuestion((int) $value);
1159  }
1160 
1162  }
1163 
1164  public function removeQuestion(int $question_id): void
1165  {
1166  try {
1167  $question = self::_instanciateQuestion($question_id);
1169  $this->logAction(
1170  $this->lng->txtlng("assessment", "log_question_removed", ilObjAssessmentFolder::_getLogLanguage()),
1171  $question_id
1172  );
1173  }
1174  $question->delete($question_id);
1175  } catch (InvalidArgumentException $e) {
1176  $this->log->error($e->getMessage());
1177  $this->log->error($e->getTraceAsString());
1178  }
1179  }
1180 
1190  {
1191  $this->removeTestResultsByUserIds($userIds);
1192 
1193  $participantData = new ilTestParticipantData($this->db, $this->lng);
1194  $participantData->setUserIdsFilter($userIds);
1195  $participantData->load($this->getTestId());
1196 
1197  $this->removeTestActives($participantData->getActiveIds());
1198  }
1199 
1200  public function removeTestResults(ilTestParticipantData $participantData)
1201  {
1202  if (count($participantData->getAnonymousActiveIds())) {
1203  $this->removeTestResultsByActiveIds($participantData->getAnonymousActiveIds());
1204  }
1205 
1206  if (count($participantData->getUserIds())) {
1207  /* @var ilTestLP $testLP */
1208  $test_lp = ilObjectLP::getInstance($this->getId());
1209  if ($test_lp instanceof ilTestLP) {
1210  $test_lp->setTestObject($this);
1211  $test_lp->resetLPDataForUserIds($participantData->getUserIds(), false);
1212  }
1213  }
1214 
1215  if (count($participantData->getActiveIds())) {
1216  $this->removeTestActives($participantData->getActiveIds());
1217  }
1218  }
1219 
1220  public function removeTestResultsByUserIds($userIds)
1221  {
1222  $participantData = new ilTestParticipantData($this->db, $this->lng);
1223  $participantData->setUserIdsFilter($userIds);
1224  $participantData->load($this->getTestId());
1225 
1226  $IN_userIds = $this->db->in('usr_id', $participantData->getUserIds(), false, 'integer');
1227  $this->db->manipulateF(
1228  "DELETE FROM usr_pref WHERE $IN_userIds AND keyword = %s",
1229  ['text'],
1230  ["tst_password_" . $this->getTestId()]
1231  );
1232 
1233  if (count($participantData->getActiveIds())) {
1234  $this->removeTestResultsByActiveIds($participantData->getActiveIds());
1235  }
1236  }
1237 
1238  public function removeTestResultsByActiveIds($activeIds)
1239  {
1240  $IN_activeIds = $this->db->in('active_fi', $activeIds, false, 'integer');
1241 
1242  $this->db->manipulate("DELETE FROM tst_solutions WHERE $IN_activeIds");
1243  $this->db->manipulate("DELETE FROM tst_qst_solved WHERE $IN_activeIds");
1244  $this->db->manipulate("DELETE FROM tst_test_result WHERE $IN_activeIds");
1245  $this->db->manipulate("DELETE FROM tst_pass_result WHERE $IN_activeIds");
1246  $this->db->manipulate("DELETE FROM tst_result_cache WHERE $IN_activeIds");
1247  $this->db->manipulate("DELETE FROM tst_sequence WHERE $IN_activeIds");
1248  $this->db->manipulate("DELETE FROM tst_times WHERE $IN_activeIds");
1249  $this->db->manipulate('DELETE FROM ' . PassPresentedVariablesRepo::TABLE_NAME . ' WHERE ' . $this->db->in('active_id', $activeIds, false, 'integer'));
1250 
1251  if ($this->isRandomTest()) {
1252  $this->db->manipulate("DELETE FROM tst_test_rnd_qst WHERE $IN_activeIds");
1253  }
1254 
1255  foreach ($activeIds as $active_id) {
1256  // remove file uploads
1257  if (@is_dir(CLIENT_WEB_DIR . "/assessment/tst_" . $this->getTestId() . "/$active_id")) {
1258  ilFileUtils::delDir(CLIENT_WEB_DIR . "/assessment/tst_" . $this->getTestId() . "/$active_id");
1259  }
1260 
1262  $this->logAction(sprintf($this->lng->txtlng("assessment", "log_selected_user_data_removed", ilObjAssessmentFolder::_getLogLanguage()), $this->userLookupFullName($this->_getUserIdFromActiveId($active_id))));
1263  }
1264  }
1265 
1267  }
1268 
1269  public function removeTestActives($activeIds)
1270  {
1271  $IN_activeIds = $this->db->in('active_id', $activeIds, false, 'integer');
1272  $this->db->manipulate("DELETE FROM tst_active WHERE $IN_activeIds");
1273  }
1274 
1282  public function questionMoveUp($question_id)
1283  {
1284  // Move a question up in sequence
1285  $result = $this->db->queryF(
1286  "SELECT * FROM tst_test_question WHERE test_fi=%s AND question_fi=%s",
1287  ['integer', 'integer'],
1288  [$this->getTestId(), $question_id]
1289  );
1290  $data = $this->db->fetchObject($result);
1291  if ($data->sequence > 1) {
1292  // OK, it's not the top question, so move it up
1293  $result = $this->db->queryF(
1294  "SELECT * FROM tst_test_question WHERE test_fi=%s AND sequence=%s",
1295  ['integer','integer'],
1296  [$this->getTestId(), $data->sequence - 1]
1297  );
1298  $data_previous = $this->db->fetchObject($result);
1299  // change previous dataset
1300  $this->db->manipulateF(
1301  "UPDATE tst_test_question SET sequence=%s WHERE test_question_id=%s",
1302  ['integer','integer'],
1303  [$data->sequence, $data_previous->test_question_id]
1304  );
1305  // move actual dataset up
1306  $this->db->manipulateF(
1307  "UPDATE tst_test_question SET sequence=%s WHERE test_question_id=%s",
1308  ['integer','integer'],
1309  [$data->sequence - 1, $data->test_question_id]
1310  );
1312  $this->logAction($this->lng->txtlng("assessment", "log_question_position_changed", ilObjAssessmentFolder::_getLogLanguage()) . ": " . ($data->sequence) . " => " . ($data->sequence - 1), $question_id);
1313  }
1314  }
1315  $this->loadQuestions();
1316  }
1317 
1325  public function questionMoveDown($question_id)
1326  {
1327  // Move a question down in sequence
1328  $result = $this->db->queryF(
1329  "SELECT * FROM tst_test_question WHERE test_fi=%s AND question_fi=%s",
1330  ['integer','integer'],
1331  [$this->getTestId(), $question_id]
1332  );
1333  $data = $this->db->fetchObject($result);
1334  $result = $this->db->queryF(
1335  "SELECT * FROM tst_test_question WHERE test_fi=%s AND sequence=%s",
1336  ['integer','integer'],
1337  [$this->getTestId(), $data->sequence + 1]
1338  );
1339  if ($result->numRows() == 1) {
1340  // OK, it's not the last question, so move it down
1341  $data_next = $this->db->fetchObject($result);
1342  // change next dataset
1343  $this->db->manipulateF(
1344  "UPDATE tst_test_question SET sequence=%s WHERE test_question_id=%s",
1345  ['integer','integer'],
1346  [$data->sequence, $data_next->test_question_id]
1347  );
1348  // move actual dataset down
1349  $this->db->manipulateF(
1350  "UPDATE tst_test_question SET sequence=%s WHERE test_question_id=%s",
1351  ['integer','integer'],
1352  [$data->sequence + 1, $data->test_question_id]
1353  );
1355  $this->logAction($this->lng->txtlng("assessment", "log_question_position_changed", ilObjAssessmentFolder::_getLogLanguage()) . ": " . ($data->sequence) . " => " . ($data->sequence + 1), $question_id);
1356  }
1357  }
1358  $this->loadQuestions();
1359  }
1360 
1367  public function duplicateQuestionForTest($question_id): int
1368  {
1369  $question = ilObjTest::_instanciateQuestion($question_id);
1370  $duplicate_id = $question->duplicate(true, '', '', -1, $this->getId());
1371  return $duplicate_id;
1372  }
1373 
1382  public function insertQuestion(ilTestQuestionSetConfig $testQuestionSetConfig, $question_id, $linkOnly = false): int
1383  {
1384  if ($linkOnly) {
1385  $duplicate_id = $question_id;
1386  } else {
1387  $duplicate_id = $this->duplicateQuestionForTest($question_id);
1388  }
1389 
1390  // get maximum sequence index in test
1391  $result = $this->db->queryF(
1392  "SELECT MAX(sequence) seq FROM tst_test_question WHERE test_fi=%s",
1393  ['integer'],
1394  [$this->getTestId()]
1395  );
1396  $sequence = 1;
1397 
1398  if ($result->numRows() == 1) {
1399  $data = $this->db->fetchObject($result);
1400  $sequence = $data->seq + 1;
1401  }
1402 
1403  $next_id = $this->db->nextId('tst_test_question');
1404  $affectedRows = $this->db->manipulateF(
1405  "INSERT INTO tst_test_question (test_question_id, test_fi, question_fi, sequence, tstamp) VALUES (%s, %s, %s, %s, %s)",
1406  ['integer', 'integer','integer','integer','integer'],
1407  [$next_id, $this->getTestId(), $duplicate_id, $sequence, time()]
1408  );
1409  if ($affectedRows == 1) {
1411  $this->logAction($this->lng->txtlng("assessment", "log_question_added", ilObjAssessmentFolder::_getLogLanguage()) . ": " . $sequence, $duplicate_id);
1412  }
1413  }
1414  // remove test_active entries, because test has changed
1415  $affectedRows = $this->db->manipulateF(
1416  "DELETE FROM tst_active WHERE test_fi = %s",
1417  ['integer'],
1418  [$this->getTestId()]
1419  );
1420  $this->loadQuestions();
1421  $this->saveCompleteStatus($testQuestionSetConfig);
1422  return $duplicate_id;
1423  }
1424 
1432  public function &getQuestionTitles(): array
1433  {
1434  $titles = [];
1435  if ($this->getQuestionSetType() == self::QUESTION_SET_TYPE_FIXED) {
1436  $result = $this->db->queryF(
1437  "SELECT qpl_questions.title FROM tst_test_question, qpl_questions WHERE tst_test_question.test_fi = %s AND tst_test_question.question_fi = qpl_questions.question_id ORDER BY tst_test_question.sequence",
1438  ['integer'],
1439  [$this->getTestId()]
1440  );
1441  while ($row = $this->db->fetchAssoc($result)) {
1442  array_push($titles, $row["title"]);
1443  }
1444  }
1445  return $titles;
1446  }
1447 
1455  public function &getQuestionTitlesAndIndexes(): array
1456  {
1457  $titles = [];
1458  if ($this->getQuestionSetType() == self::QUESTION_SET_TYPE_FIXED) {
1459  $result = $this->db->queryF(
1460  "SELECT qpl_questions.title, qpl_questions.question_id FROM tst_test_question, qpl_questions WHERE tst_test_question.test_fi = %s AND tst_test_question.question_fi = qpl_questions.question_id ORDER BY tst_test_question.sequence",
1461  ['integer'],
1462  [$this->getTestId()]
1463  );
1464  while ($row = $this->db->fetchAssoc($result)) {
1465  $titles[$row['question_id']] = $row["title"];
1466  }
1467  }
1468  return $titles;
1469  }
1470 
1471  // fau: testNav - add number parameter (to show if title should not be shown)
1481  public function getQuestionTitle($title, $nr = null, $points = null): string
1482  {
1483  switch ($this->getTitleOutput()) {
1484  case '0':
1485  case '1':
1486  return $title;
1487  break;
1488  case '2':
1489  if (isset($nr)) {
1490  return $this->lng->txt("ass_question") . ' ' . $nr;
1491  }
1492  return $this->lng->txt("ass_question");
1493  break;
1494  case 3:
1495  if (isset($nr)) {
1496  $txt = $this->lng->txt("ass_question") . ' ' . $nr;
1497  } else {
1498  $txt = $this->lng->txt("ass_question");
1499  }
1500  if ($points != '') {
1501  $lngv = $this->lng->txt('points');
1502  if ($points == 1) {
1503  $lngv = $this->lng->txt('point');
1504  }
1505  $txt .= ' - ' . $points . ' ' . $lngv;
1506  }
1507  return $txt;
1508  break;
1509 
1510  }
1511  return $this->lng->txt("ass_question");
1512  }
1513  // fau.
1514 
1523  public function getQuestionDataset($question_id): object
1524  {
1525  $result = $this->db->queryF(
1526  "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",
1527  ['integer'],
1528  [$question_id]
1529  );
1530  $row = $this->db->fetchObject($result);
1531  return $row;
1532  }
1533 
1540  public function &getExistingQuestions($pass = null): array
1541  {
1542  $existing_questions = [];
1543  $active_id = $this->getActiveIdOfUser($this->user->getId());
1544  if ($this->isRandomTest()) {
1545  if (is_null($pass)) {
1546  $pass = 0;
1547  }
1548  $result = $this->db->queryF(
1549  "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",
1550  ['integer','integer'],
1551  [$active_id, $pass]
1552  );
1553  } else {
1554  $result = $this->db->queryF(
1555  "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",
1556  ['integer'],
1557  [$this->getTestId()]
1558  );
1559  }
1560  while ($data = $this->db->fetchObject($result)) {
1561  if ($data->original_id === null) {
1562  continue;
1563  }
1564 
1565  array_push($existing_questions, $data->original_id);
1566  }
1567  return $existing_questions;
1568  }
1569 
1577  public function getQuestionType($question_id)
1578  {
1579  if ($question_id < 1) {
1580  return -1;
1581  }
1582  $result = $this->db->queryF(
1583  "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",
1584  ['integer'],
1585  [$question_id]
1586  );
1587  if ($result->numRows() == 1) {
1588  $data = $this->db->fetchObject($result);
1589  return $data->type_tag;
1590  } else {
1591  return "";
1592  }
1593  }
1594 
1601  public function startWorkingTime($active_id, $pass)
1602  {
1603  $next_id = $this->db->nextId('tst_times');
1604  $affectedRows = $this->db->manipulateF(
1605  "INSERT INTO tst_times (times_id, active_fi, started, finished, pass, tstamp) VALUES (%s, %s, %s, %s, %s, %s)",
1606  ['integer', 'integer', 'timestamp', 'timestamp', 'integer', 'integer'],
1607  [$next_id, $active_id, date("Y-m-d H:i:s"), date("Y-m-d H:i:s"), $pass, time()]
1608  );
1609  return $next_id;
1610  }
1611 
1618  public function updateWorkingTime($times_id)
1619  {
1620  $affectedRows = $this->db->manipulateF(
1621  "UPDATE tst_times SET finished = %s, tstamp = %s WHERE times_id = %s",
1622  ['timestamp', 'integer', 'integer'],
1623  [date('Y-m-d H:i:s'), time(), $times_id]
1624  );
1625  }
1626 
1633  public function &getWorkedQuestions($active_id, $pass = null): array
1634  {
1635  if (is_null($pass)) {
1636  $result = $this->db->queryF(
1637  "SELECT question_fi FROM tst_solutions WHERE active_fi = %s AND pass = %s GROUP BY question_fi",
1638  ['integer','integer'],
1639  [$active_id, 0]
1640  );
1641  } else {
1642  $result = $this->db->queryF(
1643  "SELECT question_fi FROM tst_solutions WHERE active_fi = %s AND pass = %s GROUP BY question_fi",
1644  ['integer','integer'],
1645  [$active_id, $pass]
1646  );
1647  }
1648  $result_array = [];
1649  while ($row = $this->db->fetchAssoc($result)) {
1650  array_push($result_array, $row["question_fi"]);
1651  }
1652  return $result_array;
1653  }
1654 
1663  public function isTestFinishedToViewResults($active_id, $currentpass): bool
1664  {
1665  $num = ilObjTest::lookupPassResultsUpdateTimestamp($active_id, $currentpass);
1666  return ((($currentpass > 0) && ($num == 0)) || $this->isTestFinished($active_id)) ? true : false;
1667  }
1668 
1675  public function &getAllQuestions($pass = null): array
1676  {
1677  $result_array = [];
1678  if ($this->isRandomTest()) {
1679  $active_id = $this->getActiveIdOfUser($this->user->getId());
1680  $this->loadQuestions($active_id, $pass);
1681  if (count($this->questions) == 0) {
1682  return $result_array;
1683  }
1684  if (is_null($pass)) {
1685  $pass = self::_getPass($active_id);
1686  }
1687  $result = $this->db->queryF(
1688  "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'),
1689  ['integer','integer'],
1690  [$active_id, $pass]
1691  );
1692  } else {
1693  if (count($this->questions) == 0) {
1694  return $result_array;
1695  }
1696  $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'));
1697  }
1698  while ($row = $this->db->fetchAssoc($result)) {
1699  $result_array[$row["question_id"]] = $row;
1700  }
1701  return $result_array;
1702  }
1703 
1712  public function getActiveIdOfUser($user_id = "", $anonymous_id = ""): ?int
1713  {
1714  if (!$user_id) {
1715  $user_id = $this->user->getId();
1716  }
1717 
1718  $tst_access_code = ilSession::get('tst_access_code');
1719  if (is_array($tst_access_code) &&
1720  $this->user->getId() === ANONYMOUS_USER_ID &&
1721  isset($tst_access_code[$this->getTestId()]) &&
1722  $tst_access_code[$this->getTestId()] !== '') {
1723  $result = $this->db->queryF(
1724  'SELECT active_id FROM tst_active WHERE user_fi = %s AND test_fi = %s AND anonymous_id = %s',
1725  ['integer', 'integer', 'text'],
1726  [$user_id, $this->test_id, $tst_access_code[$this->getTestId()]]
1727  );
1728  } elseif ((string) $anonymous_id !== '') {
1729  $result = $this->db->queryF(
1730  'SELECT active_id FROM tst_active WHERE user_fi = %s AND test_fi = %s AND anonymous_id = %s',
1731  ['integer', 'integer', 'text'],
1732  [$user_id, $this->test_id, $anonymous_id]
1733  );
1734  } else {
1735  if ((int) $user_id === ANONYMOUS_USER_ID) {
1736  return null;
1737  }
1738  $result = $this->db->queryF(
1739  'SELECT active_id FROM tst_active WHERE user_fi = %s AND test_fi = %s',
1740  ['integer', 'integer'],
1741  [$user_id, $this->test_id]
1742  );
1743  }
1744 
1745  if ($result->numRows()) {
1746  $row = $this->db->fetchAssoc($result);
1747  return (int) $row['active_id'];
1748  }
1749 
1750  return null;
1751  }
1752 
1753  public static function _getActiveIdOfUser($user_id = "", $test_id = "")
1754  {
1755  global $DIC;
1756  $ilDB = $DIC['ilDB'];
1757  $ilUser = $DIC['ilUser'];
1758 
1759  if (!$user_id) {
1760  $user_id = $ilUser->id;
1761  }
1762  if (!$test_id) {
1763  return "";
1764  }
1765  $result = $ilDB->queryF(
1766  "SELECT tst_active.active_id FROM tst_active WHERE user_fi = %s AND test_fi = %s",
1767  ['integer', 'integer'],
1768  [$user_id, $test_id]
1769  );
1770  if ($result->numRows()) {
1771  $row = $ilDB->fetchAssoc($result);
1772  return $row["active_id"];
1773  } else {
1774  return "";
1775  }
1776  }
1777 
1784  public function pcArrayShuffle($array): array
1785  {
1786  $keys = array_keys($array);
1787  shuffle($keys);
1788  $result = [];
1789  foreach ($keys as $key) {
1790  $result[$key] = $array[$key];
1791  }
1792  return $result;
1793  }
1794 
1801  public function &getTestResult(
1802  int $active_id,
1803  ?int $pass = null,
1804  bool $ordered_sequence = false,
1805  bool $considerHiddenQuestions = true,
1806  bool $considerOptionalQuestions = true
1807  ): array {
1808  $results = $this->getResultsForActiveId($active_id);
1809 
1810  if ($pass === null) {
1811  $pass = (int) $results['pass'];
1812  }
1813 
1814  $test_sequence_factory = new ilTestSequenceFactory($this, $this->db, $this->questioninfo);
1815  $test_sequence = $test_sequence_factory->getSequenceByActiveIdAndPass($active_id, $pass);
1816 
1817  $test_sequence->setConsiderHiddenQuestionsEnabled($considerHiddenQuestions);
1818  $test_sequence->setConsiderOptionalQuestionsEnabled($considerOptionalQuestions);
1819 
1820  $test_sequence->loadFromDb();
1821  $test_sequence->loadQuestions();
1822 
1823  if ($ordered_sequence) {
1824  $sequence = $test_sequence->getOrderedSequenceQuestions();
1825  } else {
1826  $sequence = $test_sequence->getUserSequenceQuestions();
1827  }
1828 
1829  $arrResults = [];
1830 
1831  $query = "
1832  SELECT
1833  tst_test_result.question_fi,
1834  tst_test_result.points reached,
1835  tst_test_result.hint_count requested_hints,
1836  tst_test_result.hint_points hint_points,
1837  tst_test_result.answered answered,
1838  tst_manual_fb.finalized_evaluation finalized_evaluation
1839 
1840  FROM tst_test_result
1841 
1842  LEFT JOIN tst_solutions
1843  ON tst_solutions.active_fi = tst_test_result.active_fi
1844  AND tst_solutions.question_fi = tst_test_result.question_fi
1845 
1846  LEFT JOIN tst_manual_fb
1847  ON tst_test_result.active_fi = tst_manual_fb.active_fi
1848  AND tst_test_result.question_fi = tst_manual_fb.question_fi
1849 
1850  WHERE tst_test_result.active_fi = %s
1851  AND tst_test_result.pass = %s
1852  ";
1853 
1854  $solutionresult = $this->db->queryF(
1855  $query,
1856  ['integer', 'integer'],
1857  [$active_id, $pass]
1858  );
1859 
1860  while ($row = $this->db->fetchAssoc($solutionresult)) {
1861  $arrResults[ $row['question_fi'] ] = $row;
1862  }
1863 
1864  $numWorkedThrough = count($arrResults);
1865 
1866  $IN_question_ids = $this->db->in('qpl_questions.question_id', $sequence, false, 'integer');
1867 
1868  $query = "
1869  SELECT qpl_questions.*,
1870  qpl_qst_type.type_tag,
1871  qpl_sol_sug.question_fi has_sug_sol
1872 
1873  FROM qpl_qst_type,
1874  qpl_questions
1875 
1876  LEFT JOIN qpl_sol_sug
1877  ON qpl_sol_sug.question_fi = qpl_questions.question_id
1878 
1879  WHERE qpl_qst_type.question_type_id = qpl_questions.question_type_fi
1880  AND $IN_question_ids
1881  ";
1882 
1883  $result = $this->db->query($query);
1884 
1885  $unordered = [];
1886 
1887  $key = 1;
1888 
1889  $obligationsAnswered = true;
1890 
1891  while ($row = $this->db->fetchAssoc($result)) {
1892  if (!isset($arrResults[ $row['question_id'] ])) {
1893  $percentvalue = 0.0;
1894  } else {
1895  $percentvalue = (
1896  $row['points'] ? $arrResults[$row['question_id']]['reached'] / $row['points'] : 0
1897  );
1898  }
1899  if ($percentvalue < 0) {
1900  $percentvalue = 0.0;
1901  }
1902 
1903  $data = [
1904  "nr" => "$key",
1905  "title" => ilLegacyFormElementsUtil::prepareFormOutput($row['title']),
1906  "max" => round($row['points'], 2),
1907  "reached" => round($arrResults[$row['question_id']]['reached'] ?? 0, 2),
1908  'requested_hints' => $arrResults[$row['question_id']]['requested_hints'] ?? 0,
1909  'hint_points' => $arrResults[$row['question_id']]['hint_points'] ?? 0,
1910  "percent" => sprintf("%2.2f ", ($percentvalue) * 100) . "%",
1911  "solution" => ($row['has_sug_sol']) ? assQuestion::_getSuggestedSolutionOutput($row['question_id']) : '',
1912  "type" => $row["type_tag"],
1913  "qid" => $row['question_id'],
1914  "original_id" => $row["original_id"],
1915  "workedthrough" => isset($arrResults[$row['question_id']]) ? 1 : 0,
1916  'answered' => $arrResults[$row['question_id']]['answered'] ?? 0,
1917  'finalized_evaluation' => $arrResults[$row['question_id']]['finalized_evaluation'] ?? 0,
1918  ];
1919 
1920  if (!isset($arrResults[ $row['question_id'] ]['answered']) || !$arrResults[ $row['question_id'] ]['answered']) {
1921  $obligationsAnswered = false;
1922  }
1923 
1924  $unordered[ $row['question_id'] ] = $data;
1925 
1926  $key++;
1927  }
1928 
1929  $numQuestionsTotal = count($unordered);
1930 
1931  $pass_max = 0;
1932  $pass_reached = 0;
1933  $pass_requested_hints = 0;
1934  $pass_hint_points = 0;
1935  $key = 1;
1936 
1937  $found = [];
1938 
1939  foreach ($sequence as $qid) {
1940  // building pass point sums based on prepared data
1941  // for question that exists in users qst sequence
1942  $pass_max += round($unordered[$qid]['max'], 2);
1943  $pass_reached += round($unordered[$qid]['reached'], 2);
1944  $pass_requested_hints += $unordered[$qid]['requested_hints'];
1945  $pass_hint_points += $unordered[$qid]['hint_points'];
1946 
1947  // pickup prepared data for question
1948  // that exists in users qst sequence
1949  $unordered[$qid]['nr'] = $key;
1950  array_push($found, $unordered[$qid]);
1951 
1952  // increment key counter
1953  $key++;
1954  }
1955 
1956  $unordered = null;
1957 
1958  if ($this->getScoreCutting() == 1) {
1959  if ($results['reached_points'] < 0) {
1960  $results['reached_points'] = 0;
1961  }
1962 
1963  if ($pass_reached < 0) {
1964  $pass_reached = 0;
1965  }
1966  }
1967 
1968  $found['pass']['total_max_points'] = $pass_max;
1969  $found['pass']['total_reached_points'] = $pass_reached;
1970  $found['pass']['total_requested_hints'] = $pass_requested_hints;
1971  $found['pass']['total_hint_points'] = $pass_hint_points;
1972  $found['pass']['percent'] = ($pass_max > 0) ? $pass_reached / $pass_max : 0;
1973  $found['pass']['obligationsAnswered'] = $obligationsAnswered;
1974  $found['pass']['num_workedthrough'] = $numWorkedThrough;
1975  $found['pass']['num_questions_total'] = $numQuestionsTotal;
1976 
1977  $found["test"]["total_max_points"] = $results['max_points'];
1978  $found["test"]["total_reached_points"] = $results['reached_points'];
1979  $found["test"]["total_requested_hints"] = $results['hint_count'];
1980  $found["test"]["total_hint_points"] = $results['hint_points'];
1981  $found["test"]["result_pass"] = $results['pass'];
1982  $found['test']['result_tstamp'] = $results['tstamp'];
1983  $found['test']['obligations_answered'] = $results['obligations_answered'];
1984 
1985  if ((!$found['pass']['total_reached_points']) or (!$found['pass']['total_max_points'])) {
1986  $percentage = 0.0;
1987  } else {
1988  $percentage = ($found['pass']['total_reached_points'] / $found['pass']['total_max_points']) * 100.0;
1989 
1990  if ($percentage < 0) {
1991  $percentage = 0.0;
1992  }
1993  }
1994 
1995  $found["test"]["passed"] = $results['passed'];
1996 
1997  return $found;
1998  }
1999 
2006  public function evalTotalPersons(): int
2007  {
2008  $result = $this->db->queryF(
2009  "SELECT COUNT(active_id) total FROM tst_active WHERE test_fi = %s",
2010  ['integer'],
2011  [$this->getTestId()]
2012  );
2013  $row = $this->db->fetchAssoc($result);
2014  return $row["total"];
2015  }
2016 
2023  public function getCompleteWorkingTime($user_id): int
2024  {
2025  $result = $this->db->queryF(
2026  "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",
2027  ['integer','integer'],
2028  [$this->getTestId(), $user_id]
2029  );
2030  $time = 0;
2031  while ($row = $this->db->fetchAssoc($result)) {
2032  preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["started"], $matches);
2033  $epoch_1 = mktime(
2034  (int) $matches[4],
2035  (int) $matches[5],
2036  (int) $matches[6],
2037  (int) $matches[2],
2038  (int) $matches[3],
2039  (int) $matches[1]
2040  );
2041  preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["finished"], $matches);
2042  $epoch_2 = mktime(
2043  (int) $matches[4],
2044  (int) $matches[5],
2045  (int) $matches[6],
2046  (int) $matches[2],
2047  (int) $matches[3],
2048  (int) $matches[1]
2049  );
2050  $time += ($epoch_2 - $epoch_1);
2051  }
2052  return $time;
2053  }
2054 
2061  public function &getCompleteWorkingTimeOfParticipants(): array
2062  {
2063  return $this->_getCompleteWorkingTimeOfParticipants($this->getTestId());
2064  }
2065 
2073  public function &_getCompleteWorkingTimeOfParticipants($test_id): array
2074  {
2075  $result = $this->db->queryF(
2076  "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",
2077  ['integer'],
2078  [$test_id]
2079  );
2080  $time = 0;
2081  $times = [];
2082  while ($row = $this->db->fetchAssoc($result)) {
2083  if (!array_key_exists($row["active_fi"], $times)) {
2084  $times[$row["active_fi"]] = 0;
2085  }
2086  preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["started"], $matches);
2087  $epoch_1 = mktime(
2088  (int) $matches[4],
2089  (int) $matches[5],
2090  (int) $matches[6],
2091  (int) $matches[2],
2092  (int) $matches[3],
2093  (int) $matches[1]
2094  );
2095  preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["finished"], $matches);
2096  $epoch_2 = mktime(
2097  (int) $matches[4],
2098  (int) $matches[5],
2099  (int) $matches[6],
2100  (int) $matches[2],
2101  (int) $matches[3],
2102  (int) $matches[1]
2103  );
2104  $times[$row["active_fi"]] += ($epoch_2 - $epoch_1);
2105  }
2106  return $times;
2107  }
2108 
2115  public function getCompleteWorkingTimeOfParticipant($active_id): int
2116  {
2117  $result = $this->db->queryF(
2118  "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",
2119  ['integer','integer'],
2120  [$this->getTestId(), $active_id]
2121  );
2122  $time = 0;
2123  while ($row = $this->db->fetchAssoc($result)) {
2124  preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["started"], $matches);
2125  $epoch_1 = mktime(
2126  (int) $matches[4],
2127  (int) $matches[5],
2128  (int) $matches[6],
2129  (int) $matches[2],
2130  (int) $matches[3],
2131  (int) $matches[1]
2132  );
2133  preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["finished"], $matches);
2134  $epoch_2 = mktime(
2135  (int) $matches[4],
2136  (int) $matches[5],
2137  (int) $matches[6],
2138  (int) $matches[2],
2139  (int) $matches[3],
2140  (int) $matches[1]
2141  );
2142  $time += ($epoch_2 - $epoch_1);
2143  }
2144  return $time;
2145  }
2146 
2153  public static function _getWorkingTimeOfParticipantForPass($active_id, $pass): int
2154  {
2155  global $DIC;
2156  $ilDB = $DIC['ilDB'];
2157 
2158  $result = $ilDB->queryF(
2159  "SELECT * FROM tst_times WHERE active_fi = %s AND pass = %s ORDER BY started",
2160  ['integer','integer'],
2161  [$active_id, $pass]
2162  );
2163  $time = 0;
2164  while ($row = $ilDB->fetchAssoc($result)) {
2165  preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["started"], $matches);
2166  $epoch_1 = mktime(
2167  (int) $matches[4],
2168  (int) $matches[5],
2169  (int) $matches[6],
2170  (int) $matches[2],
2171  (int) $matches[3],
2172  (int) $matches[1]
2173  );
2174  preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["finished"], $matches);
2175  $epoch_2 = mktime(
2176  (int) $matches[4],
2177  (int) $matches[5],
2178  (int) $matches[6],
2179  (int) $matches[2],
2180  (int) $matches[3],
2181  (int) $matches[1]
2182  );
2183  $time += ($epoch_2 - $epoch_1);
2184  }
2185  return $time;
2186  }
2187 
2195  public function getVisitTimeOfParticipant($active_id): array
2196  {
2197  return ilObjTest::_getVisitTimeOfParticipant($this->getTestId(), $active_id);
2198  }
2199 
2208  public function _getVisitTimeOfParticipant($test_id, $active_id): array
2209  {
2210  $result = $this->db->queryF(
2211  "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.started",
2212  ['integer','integer'],
2213  [$test_id, $active_id]
2214  );
2215  $firstvisit = 0;
2216  $lastvisit = 0;
2217  while ($row = $this->db->fetchAssoc($result)) {
2218  preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["started"], $matches);
2219  $epoch_1 = mktime(
2220  (int) $matches[4],
2221  (int) $matches[5],
2222  (int) $matches[6],
2223  (int) $matches[2],
2224  (int) $matches[3],
2225  (int) $matches[1]
2226  );
2227  if ($firstvisit == 0 || $epoch_1 < $firstvisit) {
2228  $firstvisit = $epoch_1;
2229  }
2230  preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["finished"], $matches);
2231  $epoch_2 = mktime(
2232  (int) $matches[4],
2233  (int) $matches[5],
2234  (int) $matches[6],
2235  (int) $matches[2],
2236  (int) $matches[3],
2237  (int) $matches[1]
2238  );
2239  if ($epoch_2 > $lastvisit) {
2240  $lastvisit = $epoch_2;
2241  }
2242  }
2243  return ["firstvisit" => $firstvisit, "lastvisit" => $lastvisit];
2244  }
2245 
2249  public function evalStatistical($active_id): array
2250  {
2251  $pass = ilObjTest::_getResultPass($active_id);
2252  $test_result = &$this->getTestResult($active_id, $pass);
2253  $result = $this->db->queryF(
2254  "SELECT tst_times.* FROM tst_active, tst_times WHERE tst_active.active_id = %s AND tst_active.active_id = tst_times.active_fi",
2255  ['integer'],
2256  [$active_id]
2257  );
2258  $times = [];
2259  $first_visit = 0;
2260  $last_visit = 0;
2261  while ($row = $this->db->fetchObject($result)) {
2262  preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row->started, $matches);
2263  $epoch_1 = mktime(
2264  (int) $matches[4],
2265  (int) $matches[5],
2266  (int) $matches[6],
2267  (int) $matches[2],
2268  (int) $matches[3],
2269  (int) $matches[1]
2270  );
2271  if (!$first_visit) {
2272  $first_visit = $epoch_1;
2273  }
2274  if ($epoch_1 < $first_visit) {
2275  $first_visit = $epoch_1;
2276  }
2277  preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row->finished, $matches);
2278  $epoch_2 = mktime(
2279  (int) $matches[4],
2280  (int) $matches[5],
2281  (int) $matches[6],
2282  (int) $matches[2],
2283  (int) $matches[3],
2284  (int) $matches[1]
2285  );
2286  if (!$last_visit) {
2287  $last_visit = $epoch_2;
2288  }
2289  if ($epoch_2 > $last_visit) {
2290  $last_visit = $epoch_2;
2291  }
2292  $times[$row->active_fi] += ($epoch_2 - $epoch_1);
2293  }
2294  $max_time = 0;
2295  foreach ($times as $key => $value) {
2296  $max_time += $value;
2297  }
2298  if ((!$test_result["test"]["total_reached_points"]) or (!$test_result["test"]["total_max_points"])) {
2299  $percentage = 0.0;
2300  } else {
2301  $percentage = ($test_result["test"]["total_reached_points"] / $test_result["test"]["total_max_points"]) * 100.0;
2302  if ($percentage < 0) {
2303  $percentage = 0.0;
2304  }
2305  }
2306  $mark_obj = $this->mark_schema->getMatchingMark($percentage);
2307  $first_date = getdate($first_visit);
2308  $last_date = getdate($last_visit);
2309  $qworkedthrough = 0;
2310  foreach ($test_result as $key => $value) {
2311  if (preg_match("/\d+/", $key)) {
2312  $qworkedthrough += $value["workedthrough"];
2313  }
2314  }
2315  if (!$qworkedthrough) {
2316  $atimeofwork = 0;
2317  } else {
2318  $atimeofwork = $max_time / $qworkedthrough;
2319  }
2320 
2321  $obligationsAnswered = $test_result["test"]["obligations_answered"];
2322 
2323  $result_mark = "";
2324  $passed = "";
2325 
2326  if ($mark_obj) {
2327  $result_mark = $mark_obj->getShortName();
2328 
2329  if ($mark_obj->getPassed() && $obligationsAnswered) {
2330  $passed = 1;
2331  } else {
2332  $passed = 0;
2333  }
2334  }
2335  $percent_worked_through = 0;
2336  if (count($this->questions)) {
2337  $percent_worked_through = $qworkedthrough / count($this->questions);
2338  }
2339  $result_array = [
2340  "qworkedthrough" => $qworkedthrough,
2341  "qmax" => count($this->questions),
2342  "pworkedthrough" => $percent_worked_through,
2343  "timeofwork" => $max_time,
2344  "atimeofwork" => $atimeofwork,
2345  "firstvisit" => $first_date,
2346  "lastvisit" => $last_date,
2347  "resultspoints" => $test_result["test"]["total_reached_points"],
2348  "maxpoints" => $test_result["test"]["total_max_points"],
2349  "resultsmarks" => $result_mark,
2350  "passed" => $passed,
2351  "distancemedian" => "0"
2352  ];
2353  foreach ($test_result as $key => $value) {
2354  if (preg_match("/\d+/", $key)) {
2355  $result_array[$key] = $value;
2356  }
2357  }
2358  return $result_array;
2359  }
2360 
2368  public function &getTotalPointsPassedArray(): array
2369  {
2370  $totalpoints_array = [];
2371  $all_users = $this->evalTotalParticipantsArray();
2372  foreach ($all_users as $active_id => $user_name) {
2373  $test_result = &$this->getTestResult($active_id);
2374  $reached = $test_result["test"]["total_reached_points"];
2375  $total = $test_result["test"]["total_max_points"];
2376  $percentage = $total != 0 ? $reached / $total : 0;
2377  $mark = $this->mark_schema->getMatchingMark($percentage * 100.0);
2378 
2379  $obligationsAnswered = $test_result["test"]["obligations_answered"];
2380 
2381  if ($mark) {
2382  if ($mark->getPassed() && $obligationsAnswered) {
2383  array_push($totalpoints_array, $test_result["test"]["total_reached_points"]);
2384  }
2385  }
2386  }
2387  return $totalpoints_array;
2388  }
2389 
2395  public function &getParticipants(): array
2396  {
2397  $result = $this->db->queryF(
2398  "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",
2399  ['integer'],
2400  [$this->getTestId()]
2401  );
2402  $persons_array = [];
2403  while ($row = $this->db->fetchAssoc($result)) {
2404  $name = $this->lng->txt("anonymous");
2405  $fullname = $this->lng->txt("anonymous");
2406  $login = "";
2407  if (!$this->getAnonymity()) {
2408  if (strlen($row["firstname"] . $row["lastname"] . $row["title"]) == 0) {
2409  $name = $this->lng->txt("deleted_user");
2410  $fullname = $this->lng->txt("deleted_user");
2411  $login = $this->lng->txt("unknown");
2412  } else {
2413  $login = $row["login"];
2414  if ($row["usr_id"] == ANONYMOUS_USER_ID) {
2415  $name = $this->lng->txt("anonymous");
2416  $fullname = $this->lng->txt("anonymous");
2417  } else {
2418  $name = trim($row["lastname"] . ", " . $row["firstname"] . " " . $row["title"]);
2419  $fullname = trim($row["title"] . " " . $row["firstname"] . " " . $row["lastname"]);
2420  }
2421  }
2422  }
2423  $persons_array[$row["active_id"]] = [
2424  "name" => $name,
2425  "fullname" => $fullname,
2426  "login" => $login
2427  ];
2428  }
2429  return $persons_array;
2430  }
2431 
2438  public function evalTotalPersonsArray($name_sort_order = "asc"): array
2439  {
2440  $result = $this->db->queryF(
2441  "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),
2442  ['integer'],
2443  [$this->getTestId()]
2444  );
2445  $persons_array = [];
2446  while ($row = $this->db->fetchAssoc($result)) {
2447  if ($this->getAccessFilteredParticipantList() && !$this->getAccessFilteredParticipantList()->isActiveIdInList($row["active_id"])) {
2448  continue;
2449  }
2450 
2451  if ($this->getAnonymity()) {
2452  $persons_array[$row["active_id"]] = $this->lng->txt("anonymous");
2453  } else {
2454  if (strlen($row["firstname"] . $row["lastname"] . $row["title"]) == 0) {
2455  $persons_array[$row["active_id"]] = $this->lng->txt("deleted_user");
2456  } else {
2457  if ($row["user_fi"] == ANONYMOUS_USER_ID) {
2458  $persons_array[$row["active_id"]] = $row["lastname"];
2459  } else {
2460  $persons_array[$row["active_id"]] = trim($row["lastname"] . ", " . $row["firstname"] . " " . $row["title"]);
2461  }
2462  }
2463  }
2464  }
2465  return $persons_array;
2466  }
2467 
2473  public function evalTotalParticipantsArray($name_sort_order = "asc"): array
2474  {
2475  $result = $this->db->queryF(
2476  "SELECT tst_active.user_fi, tst_active.active_id, usr_data.login, 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),
2477  ['integer'],
2478  [$this->getTestId()]
2479  );
2480  $persons_array = [];
2481  while ($row = $this->db->fetchAssoc($result)) {
2482  if ($this->getAnonymity()) {
2483  $persons_array[$row["active_id"]] = ["name" => $this->lng->txt("anonymous")];
2484  } else {
2485  if (strlen($row["firstname"] . $row["lastname"] . $row["title"]) == 0) {
2486  $persons_array[$row["active_id"]] = ["name" => $this->lng->txt("deleted_user")];
2487  } else {
2488  if ($row["user_fi"] == ANONYMOUS_USER_ID) {
2489  $persons_array[$row["active_id"]] = ["name" => $row["lastname"]];
2490  } else {
2491  $persons_array[$row["active_id"]] = ["name" => trim($row["lastname"] . ", " . $row["firstname"] . " " . $row["title"]), "login" => $row["login"]];
2492  }
2493  }
2494  }
2495  }
2496  return $persons_array;
2497  }
2498 
2505  public function &getQuestionsOfTest($active_id): array
2506  {
2507  if ($this->isRandomTest()) {
2508  $this->db->setLimit($this->getQuestionCount(), 0);
2509  $result = $this->db->queryF(
2510  "SELECT tst_test_rnd_qst.sequence, tst_test_rnd_qst.question_fi, " .
2511  "tst_test_rnd_qst.pass, qpl_questions.points " .
2512  "FROM tst_test_rnd_qst, qpl_questions " .
2513  "WHERE tst_test_rnd_qst.question_fi = qpl_questions.question_id " .
2514  "AND tst_test_rnd_qst.active_fi = %s ORDER BY tst_test_rnd_qst.sequence",
2515  ['integer'],
2516  [$active_id]
2517  );
2518  } else {
2519  $result = $this->db->queryF(
2520  "SELECT tst_test_question.sequence, tst_test_question.question_fi, " .
2521  "qpl_questions.points " .
2522  "FROM tst_test_question, tst_active, qpl_questions " .
2523  "WHERE tst_test_question.question_fi = qpl_questions.question_id " .
2524  "AND tst_active.active_id = %s AND tst_active.test_fi = tst_test_question.test_fi",
2525  ['integer'],
2526  [$active_id]
2527  );
2528  }
2529  $qtest = [];
2530  if ($result->numRows()) {
2531  while ($row = $this->db->fetchAssoc($result)) {
2532  array_push($qtest, $row);
2533  }
2534  }
2535  return $qtest;
2536  }
2537 
2544  public function &getQuestionsOfPass($active_id, $pass): array
2545  {
2546  if ($this->isRandomTest()) {
2547  $this->db->setLimit($this->getQuestionCount(), 0);
2548  $result = $this->db->queryF(
2549  "SELECT tst_test_rnd_qst.sequence, tst_test_rnd_qst.question_fi, " .
2550  "qpl_questions.points " .
2551  "FROM tst_test_rnd_qst, qpl_questions " .
2552  "WHERE tst_test_rnd_qst.question_fi = qpl_questions.question_id " .
2553  "AND tst_test_rnd_qst.active_fi = %s AND tst_test_rnd_qst.pass = %s " .
2554  "ORDER BY tst_test_rnd_qst.sequence",
2555  ['integer', 'integer'],
2556  [$active_id, $pass]
2557  );
2558  } else {
2559  $result = $this->db->queryF(
2560  "SELECT tst_test_question.sequence, tst_test_question.question_fi, " .
2561  "qpl_questions.points " .
2562  "FROM tst_test_question, tst_active, qpl_questions " .
2563  "WHERE tst_test_question.question_fi = qpl_questions.question_id " .
2564  "AND tst_active.active_id = %s AND tst_active.test_fi = tst_test_question.test_fi",
2565  ['integer'],
2566  [$active_id]
2567  );
2568  }
2569  $qpass = [];
2570  if ($result->numRows()) {
2571  while ($row = $this->db->fetchAssoc($result)) {
2572  array_push($qpass, $row);
2573  }
2574  }
2575  return $qpass;
2576  }
2577 
2579  {
2581  }
2582 
2583  public function setAccessFilteredParticipantList(ilTestParticipantList $access_filtered_participant_list): void
2584  {
2585  $this->access_filtered_participant_list = $access_filtered_participant_list;
2586  }
2587 
2589  {
2590  $list = new ilTestParticipantList($this, $this->user, $this->lng, $this->db);
2591  $list->initializeFromDbRows($this->getTestParticipants());
2592 
2593  return $list->getAccessFilteredList(
2594  $this->participant_access_filter->getAccessStatisticsUserFilter($this->getRefId())
2595  );
2596  }
2597 
2599  {
2600  return (new ilTestEvaluationFactory($this->db, $this))
2601  ->getEvaluationData();
2602  }
2603 
2604  public static function _getQuestionCountAndPointsForPassOfParticipant($active_id, $pass): array
2605  {
2606  global $DIC;
2607  $ilDB = $DIC['ilDB'];
2608 
2609  $questionSetType = ilObjTest::lookupQuestionSetTypeByActiveId($active_id);
2610 
2611  switch ($questionSetType) {
2613 
2614  $res = $ilDB->queryF(
2615  "
2616  SELECT tst_test_rnd_qst.pass,
2617  COUNT(tst_test_rnd_qst.question_fi) qcount,
2618  SUM(qpl_questions.points) qsum
2619 
2620  FROM tst_test_rnd_qst,
2621  qpl_questions
2622 
2623  WHERE tst_test_rnd_qst.question_fi = qpl_questions.question_id
2624  AND tst_test_rnd_qst.active_fi = %s
2625  AND pass = %s
2626 
2627  GROUP BY tst_test_rnd_qst.active_fi,
2628  tst_test_rnd_qst.pass
2629  ",
2630  ['integer', 'integer'],
2631  [$active_id, $pass]
2632  );
2633 
2634  break;
2635 
2637 
2638  $res = $ilDB->queryF(
2639  "
2640  SELECT COUNT(tst_test_question.question_fi) qcount,
2641  SUM(qpl_questions.points) qsum
2642 
2643  FROM tst_test_question,
2644  qpl_questions,
2645  tst_active
2646 
2647  WHERE tst_test_question.question_fi = qpl_questions.question_id
2648  AND tst_test_question.test_fi = tst_active.test_fi
2649  AND tst_active.active_id = %s
2650 
2651  GROUP BY tst_test_question.test_fi
2652  ",
2653  ['integer'],
2654  [$active_id]
2655  );
2656 
2657  break;
2658 
2659  default:
2660 
2661  throw new ilTestException("not supported question set type: $questionSetType");
2662  }
2663 
2664  $row = $ilDB->fetchAssoc($res);
2665 
2666  if (is_array($row)) {
2667  return ["count" => $row["qcount"], "points" => $row["qsum"]];
2668  }
2669 
2670  return ["count" => 0, "points" => 0];
2671  }
2672 
2673  public function &getCompleteEvaluationData($withStatistics = true, $filterby = "", $filtertext = ""): ilTestEvaluationData
2674  {
2676  if ($withStatistics) {
2677  $data->calculateStatistics();
2678  }
2679  $data->setFilter($filterby, $filtertext);
2680  return $data;
2681  }
2682 
2689  public function &evalResultsOverview(): array
2690  {
2691  return $this->_evalResultsOverview($this->getTestId());
2692  }
2693 
2700  public function &_evalResultsOverview($test_id): array
2701  {
2702  $result = $this->db->queryF(
2703  "SELECT usr_data.usr_id, usr_data.firstname, usr_data.lastname, usr_data.title, usr_data.login, " .
2704  "tst_test_result.*, qpl_questions.original_id, qpl_questions.title questiontitle, " .
2705  "qpl_questions.points maxpoints " .
2706  "FROM tst_test_result, qpl_questions, tst_active " .
2707  "LEFT JOIN usr_data ON tst_active.user_fi = usr_data.usr_id " .
2708  "WHERE tst_active.active_id = tst_test_result.active_fi " .
2709  "AND qpl_questions.question_id = tst_test_result.question_fi " .
2710  "AND tst_active.test_fi = %s " .
2711  "ORDER BY tst_active.active_id, tst_test_result.pass, tst_test_result.tstamp",
2712  ['integer'],
2713  [$test_id]
2714  );
2715  $overview = [];
2716  while ($row = $this->db->fetchAssoc($result)) {
2717  if (!array_key_exists($row["active_fi"], $overview)) {
2718  $overview[$row["active_fi"]] = [];
2719  $overview[$row["active_fi"]]["firstname"] = $row["firstname"];
2720  $overview[$row["active_fi"]]["lastname"] = $row["lastname"];
2721  $overview[$row["active_fi"]]["title"] = $row["title"];
2722  $overview[$row["active_fi"]]["login"] = $row["login"];
2723  $overview[$row["active_fi"]]["usr_id"] = $row["usr_id"];
2724  $overview[$row["active_fi"]]["started"] = $row["started"];
2725  $overview[$row["active_fi"]]["finished"] = $row["finished"];
2726  }
2727  if (!array_key_exists($row["pass"], $overview[$row["active_fi"]])) {
2728  $overview[$row["active_fi"]][$row["pass"]] = [];
2729  $overview[$row["active_fi"]][$row["pass"]]["reached"] = 0;
2730  $overview[$row["active_fi"]][$row["pass"]]["maxpoints"] = $row["maxpoints"];
2731  }
2732  array_push($overview[$row["active_fi"]][$row["pass"]], $row);
2733  $overview[$row["active_fi"]][$row["pass"]]["reached"] += $row["points"];
2734  }
2735  return $overview;
2736  }
2737 
2745  public function &evalResultsOverviewOfParticipant($active_id): array
2746  {
2747  $result = $this->db->queryF(
2748  "SELECT usr_data.usr_id, usr_data.firstname, usr_data.lastname, usr_data.title, usr_data.login, " .
2749  "tst_test_result.*, qpl_questions.original_id, qpl_questions.title questiontitle, " .
2750  "qpl_questions.points maxpoints " .
2751  "FROM tst_test_result, qpl_questions, tst_active " .
2752  "LEFT JOIN usr_data ON tst_active.user_fi = usr_data.usr_id " .
2753  "WHERE tst_active.active_id = tst_test_result.active_fi " .
2754  "AND qpl_questions.question_id = tst_test_result.question_fi " .
2755  "AND tst_active.test_fi = %s AND tst_active.active_id = %s" .
2756  "ORDER BY tst_active.active_id, tst_test_result.pass, tst_test_result.tstamp",
2757  ['integer', 'integer'],
2758  [$this->getTestId(), $active_id]
2759  );
2760  $overview = [];
2761  while ($row = $this->db->fetchAssoc($result)) {
2762  if (!array_key_exists($row["active_fi"], $overview)) {
2763  $overview[$row["active_fi"]] = [];
2764  $overview[$row["active_fi"]]["firstname"] = $row["firstname"];
2765  $overview[$row["active_fi"]]["lastname"] = $row["lastname"];
2766  $overview[$row["active_fi"]]["title"] = $row["title"];
2767  $overview[$row["active_fi"]]["login"] = $row["login"];
2768  $overview[$row["active_fi"]]["usr_id"] = $row["usr_id"];
2769  $overview[$row["active_fi"]]["started"] = $row["started"];
2770  $overview[$row["active_fi"]]["finished"] = $row["finished"];
2771  }
2772  if (!array_key_exists($row["pass"], $overview[$row["active_fi"]])) {
2773  $overview[$row["active_fi"]][$row["pass"]] = [];
2774  $overview[$row["active_fi"]][$row["pass"]]["reached"] = 0;
2775  $overview[$row["active_fi"]][$row["pass"]]["maxpoints"] = $row["maxpoints"];
2776  }
2777  array_push($overview[$row["active_fi"]][$row["pass"]], $row);
2778  $overview[$row["active_fi"]][$row["pass"]]["reached"] += $row["points"];
2779  }
2780  return $overview;
2781  }
2782 
2794  public function buildName(
2795  ?int $user_id,
2796  ?string $firstname,
2797  ?string $lastname
2798  ): string {
2799  if ($user_id === null
2800  || $firstname . $lastname === '') {
2801  return $this->lng->txt('deleted_user');
2802  }
2803 
2804  if ($this->getAnonymity()) {
2805  return $this->lng->txt('anonymous');
2806  }
2807 
2808  if ($user_id == ANONYMOUS_USER_ID) {
2809  return $lastname;
2810  }
2811 
2812  return trim($lastname . ', ' . $firstname);
2813  }
2814 
2815  public function evalTotalStartedAverageTime(?array $active_ids_to_filter = null): float
2816  {
2817  $query = "SELECT tst_times.* FROM tst_active, tst_times WHERE tst_active.test_fi = %s AND tst_active.active_id = tst_times.active_fi";
2818 
2819  if ($active_ids_to_filter !== null && $active_ids_to_filter !== []) {
2820  $query .= " AND " . $this->db->in('active_id', $active_ids_to_filter, false, 'integer');
2821  }
2822 
2823  $result = $this->db->queryF($query, ['integer'], [$this->getTestId()]);
2824  $times = [];
2825  while ($row = $this->db->fetchObject($result)) {
2826  preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row->started, $matches);
2827  $epoch_1 = mktime(
2828  (int) $matches[4],
2829  (int) $matches[5],
2830  (int) $matches[6],
2831  (int) $matches[2],
2832  (int) $matches[3],
2833  (int) $matches[1]
2834  );
2835  preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row->finished, $matches);
2836  $epoch_2 = mktime(
2837  (int) $matches[4],
2838  (int) $matches[5],
2839  (int) $matches[6],
2840  (int) $matches[2],
2841  (int) $matches[3],
2842  (int) $matches[1]
2843  );
2844  if (isset($times[$row->active_fi])) {
2845  $times[$row->active_fi] += ($epoch_2 - $epoch_1);
2846  } else {
2847  $times[$row->active_fi] = ($epoch_2 - $epoch_1);
2848  }
2849  }
2850  $max_time = 0;
2851  $counter = 0;
2852  foreach ($times as $key => $value) {
2853  $max_time += $value;
2854  $counter++;
2855  }
2856  if ($counter) {
2857  $average_time = round($max_time / $counter);
2858  } else {
2859  $average_time = 0;
2860  }
2861  return $average_time;
2862  }
2863 
2870  public function getAvailableQuestionpools($use_object_id = false, $equal_points = false, $could_be_offline = false, $show_path = false, $with_questioncount = false, $permission = "read"): array
2871  {
2872  return ilObjQuestionPool::_getAvailableQuestionpools($use_object_id, $equal_points, $could_be_offline, $show_path, $with_questioncount, $permission);
2873  }
2874 
2881  public function getImagePath(): string
2882  {
2883  return CLIENT_WEB_DIR . "/assessment/" . $this->getId() . "/images/";
2884  }
2885 
2892  public function getImagePathWeb()
2893  {
2894  $webdir = ilFileUtils::removeTrailingPathSeparators(CLIENT_WEB_DIR) . "/assessment/" . $this->getId() . "/images/";
2895  return str_replace(
2896  ilFileUtils::removeTrailingPathSeparators(ILIAS_ABSOLUTE_PATH),
2898  $webdir
2899  );
2900  }
2901 
2910  public function createQuestionGUI($question_type, $question_id = -1): ?assQuestionGUI
2911  {
2912  if ((!$question_type) and ($question_id > 0)) {
2913  $question_type = $this->getQuestionType($question_id);
2914  }
2915 
2916  if (!strlen($question_type)) {
2917  return null;
2918  }
2919 
2920  $question_type_gui = $question_type . 'GUI';
2921  $question = new $question_type_gui();
2922 
2923  if ($question_id > 0) {
2924  $question->object->loadFromDb($question_id);
2925 
2926  $feedbackObjectClassname = assQuestion::getFeedbackClassNameByQuestionType($question_type);
2927  $question->object->feedbackOBJ = new $feedbackObjectClassname($question->object, $this->ctrl, $this->db, $this->lng);
2928 
2929  $assSettings = new ilSetting('assessment');
2930  $processLockerFactory = new ilAssQuestionProcessLockerFactory($assSettings, $this->db);
2931  $processLockerFactory->setQuestionId($question->object->getId());
2932  $processLockerFactory->setUserId($this->user->getId());
2933  $processLockerFactory->setAssessmentLogEnabled(ilObjAssessmentFolder::_enabledAssessmentLogging());
2934  $question->object->setProcessLocker($processLockerFactory->getLocker());
2935  }
2936 
2937  return $question;
2938  }
2939 
2946  public static function _instanciateQuestion($question_id): ?assQuestion
2947  {
2948  if (strcmp((string) $question_id, "") !== 0) {
2949  return assQuestion::instantiateQuestion((int) $question_id);
2950  }
2951 
2952  return null;
2953  }
2954 
2963  public function moveQuestions($move_questions, $target_index, $insert_mode)
2964  {
2965  $this->questions = array_values($this->questions);
2966  $array_pos = array_search($target_index, $this->questions);
2967  if ($insert_mode == 0) {
2968  $part1 = array_slice($this->questions, 0, $array_pos);
2969  $part2 = array_slice($this->questions, $array_pos);
2970  } elseif ($insert_mode == 1) {
2971  $part1 = array_slice($this->questions, 0, $array_pos + 1);
2972  $part2 = array_slice($this->questions, $array_pos + 1);
2973  }
2974  foreach ($move_questions as $question_id) {
2975  if (!(array_search($question_id, $part1) === false)) {
2976  unset($part1[array_search($question_id, $part1)]);
2977  }
2978  if (!(array_search($question_id, $part2) === false)) {
2979  unset($part2[array_search($question_id, $part2)]);
2980  }
2981  }
2982  $part1 = array_values($part1);
2983  $part2 = array_values($part2);
2984  $new_array = array_values(array_merge($part1, $move_questions, $part2));
2985  $this->questions = [];
2986  $counter = 1;
2987  foreach ($new_array as $question_id) {
2988  $this->questions[$counter] = $question_id;
2989  $counter++;
2990  }
2991  $this->saveQuestionsToDb();
2992  }
2993 
2994 
3002  public function startingTimeReached(): bool
3003  {
3004  if ($this->isStartingTimeEnabled() && $this->getStartingTime() != 0) {
3005  $now = time();
3006  if ($now < $this->getStartingTime()) {
3007  return false;
3008  }
3009  }
3010  return true;
3011  }
3012 
3020  public function endingTimeReached(): bool
3021  {
3022  if ($this->isEndingTimeEnabled() && $this->getEndingTime() != 0) {
3023  $now = time();
3024  if ($now > $this->getEndingTime()) {
3025  return true;
3026  }
3027  }
3028  return false;
3029  }
3030 
3036  public function getAvailableQuestions($arr_filter, $completeonly = 0): array
3037  {
3038  $available_pools = array_keys(ilObjQuestionPool::_getAvailableQuestionpools(true, false, false, false, false));
3039  $available = "";
3040  if (count($available_pools)) {
3041  $available = " AND " . $this->db->in('qpl_questions.obj_fi', $available_pools, false, 'integer');
3042  } else {
3043  return [];
3044  }
3045  if ($completeonly) {
3046  $available .= " AND qpl_questions.complete = " . $this->db->quote("1", 'text');
3047  }
3048 
3049  $where = "";
3050  if (is_array($arr_filter)) {
3051  if (array_key_exists('title', $arr_filter) && strlen($arr_filter['title'])) {
3052  $where .= " AND " . $this->db->like('qpl_questions.title', 'text', "%%" . $arr_filter['title'] . "%%");
3053  }
3054  if (array_key_exists('description', $arr_filter) && strlen($arr_filter['description'])) {
3055  $where .= " AND " . $this->db->like('qpl_questions.description', 'text', "%%" . $arr_filter['description'] . "%%");
3056  }
3057  if (array_key_exists('author', $arr_filter) && strlen($arr_filter['author'])) {
3058  $where .= " AND " . $this->db->like('qpl_questions.author', 'text', "%%" . $arr_filter['author'] . "%%");
3059  }
3060  if (array_key_exists('type', $arr_filter) && strlen($arr_filter['type'])) {
3061  $where .= " AND qpl_qst_type.type_tag = " . $this->db->quote($arr_filter['type'], 'text');
3062  }
3063  if (array_key_exists('qpl', $arr_filter) && strlen($arr_filter['qpl'])) {
3064  $where .= " AND " . $this->db->like('object_data.title', 'text', "%%" . $arr_filter['qpl'] . "%%");
3065  }
3066  }
3067 
3068  $original_ids = &$this->getExistingQuestions();
3069  $original_clause = " qpl_questions.original_id IS NULL";
3070  if (count($original_ids)) {
3071  $original_clause = " qpl_questions.original_id IS NULL AND " . $this->db->in('qpl_questions.question_id', $original_ids, true, 'integer');
3072  }
3073 
3074  $query_result = $this->db->query("
3075  SELECT qpl_questions.*, qpl_questions.tstamp,
3076  qpl_qst_type.type_tag, qpl_qst_type.plugin, qpl_qst_type.plugin_name,
3077  object_data.title parent_title
3078  FROM qpl_questions, qpl_qst_type, object_data
3079  WHERE $original_clause $available
3080  AND object_data.obj_id = qpl_questions.obj_fi
3081  AND qpl_questions.tstamp > 0
3082  AND qpl_questions.question_type_fi = qpl_qst_type.question_type_id
3083  $where
3084  ");
3085  $rows = [];
3086 
3087  if ($query_result->numRows()) {
3088  while ($row = $this->db->fetchAssoc($query_result)) {
3090 
3091  if (!$row['plugin']) {
3092  $row[ 'ttype' ] = $this->lng->txt($row[ "type_tag" ]);
3093 
3094  $rows[] = $row;
3095  continue;
3096  }
3097 
3098  $plugin = $this->component_repository->getPluginByName($row['plugin_name']);
3099  if (!$plugin->isActive()) {
3100  continue;
3101  }
3102 
3103  $pl = $this->component_factory->getPlugin($plugin->getId());
3104  $row[ 'ttype' ] = $pl->getQuestionTypeTranslation();
3105 
3106  $rows[] = $row;
3107  }
3108  }
3109  return $rows;
3110  }
3111 
3116  public function fromXML(ilQTIAssessment $assessment, array $mappings)
3117  {
3118  ilSession::clear('import_mob_xhtml');
3119 
3120  $this->saveToDb(true);
3121 
3122  $main_settings = $this->getMainSettings();
3123  $general_settings = $main_settings->getGeneralSettings();
3124  $introduction_settings = $main_settings->getIntroductionSettings();
3125  $access_settings = $main_settings->getAccessSettings();
3126  $test_behaviour_settings = $main_settings->getTestBehaviourSettings();
3127  $question_behaviour_settings = $main_settings->getQuestionBehaviourSettings();
3128  $participant_functionality_settings = $main_settings->getParticipantFunctionalitySettings();
3129  $finishing_settings = $main_settings->getFinishingSettings();
3130  $additional_settings = $main_settings->getAdditionalSettings();
3131 
3132  $introduction_settings = $introduction_settings->withIntroductionEnabled(false);
3133  foreach ($assessment->objectives as $objectives) {
3134  foreach ($objectives->materials as $material) {
3135  $introduction_settings = $this->addIntroductionToSettingsFromImport(
3136  $introduction_settings,
3137  $this->qtiMaterialToArray($material),
3138  $mappings
3139  );
3140  }
3141  }
3142 
3143  if ($assessment->getPresentationMaterial()
3144  && $assessment->getPresentationMaterial()->getFlowMat(0)
3145  && $assessment->getPresentationMaterial()->getFlowMat(0)->getMaterial(0)) {
3146  $finishing_settings = $this->addConcludingRemarksToSettingsFromImport(
3147  $finishing_settings,
3148  $this->qtiMaterialToArray(
3149  $assessment->getPresentationMaterial()->getFlowMat(0)->getMaterial(0)
3150  ),
3151  $mappings
3152  );
3153  }
3154 
3155  $score_settings = $this->getScoreSettings();
3156  $scoring_settings = $score_settings->getScoringSettings();
3157  $gamification_settings = $score_settings->getGamificationSettings();
3158  $result_summary_settings = $score_settings->getResultSummarySettings();
3159  $result_details_settings = $score_settings->getResultDetailsSettings();
3160  foreach ($assessment->qtimetadata as $metadata) {
3161  switch ($metadata["label"]) {
3162  case "solution_details":
3163  $result_details_settings = $result_details_settings->withShowPassDetails((bool) $metadata["entry"]);
3164  break;
3165  case "show_solution_list_comparison":
3166  $result_details_settings = $result_details_settings->withShowSolutionListComparison((bool) $metadata["entry"]);
3167  break;
3168  case "print_bs_with_res":
3169  $result_details_settings = $result_details_settings->withShowSolutionListComparison((bool) $metadata["entry"]);
3170  break;
3171  case "author":
3172  $this->saveAuthorToMetadata($metadata["entry"]);
3173  break;
3174  case "nr_of_tries":
3175  $test_behaviour_settings = $test_behaviour_settings->withNumberOfTries((int) $metadata["entry"]);
3176  break;
3177  case 'block_after_passed':
3178  $test_behaviour_settings = $test_behaviour_settings->withBlockAfterPassedEnabled((bool) $metadata['entry']);
3179  break;
3180  case "pass_waiting":
3181  $test_behaviour_settings = $test_behaviour_settings->withPassWaiting($metadata["entry"]);
3182  break;
3183  case "kiosk":
3184  $test_behaviour_settings = $test_behaviour_settings->withKioskMode((int) $metadata["entry"]);
3185  break;
3186  case 'show_introduction':
3187  $introduction_settings = $introduction_settings->withIntroductionEnabled((bool) $metadata['entry']);
3188  break;
3189  case "showfinalstatement":
3190  case 'show_concluding_remarks':
3191  $finishing_settings = $finishing_settings->withConcludingRemarksEnabled((bool) $metadata["entry"]);
3192  break;
3193  case 'exam_conditions':
3194  $introduction_settings = $introduction_settings->withExamConditionsCheckboxEnabled($metadata['entry'] === '1');
3195  break;
3196  case "highscore_enabled":
3197  $gamification_settings = $gamification_settings->withHighscoreEnabled((bool) $metadata["entry"]);
3198  break;
3199  case "highscore_anon":
3200  $gamification_settings = $gamification_settings->withHighscoreAnon((bool) $metadata["entry"]);
3201  break;
3202  case "highscore_achieved_ts":
3203  $gamification_settings = $gamification_settings->withHighscoreAchievedTS((bool) $metadata["entry"]);
3204  break;
3205  case "highscore_score":
3206  $gamification_settings = $gamification_settings->withHighscoreScore((bool) $metadata["entry"]);
3207  break;
3208  case "highscore_percentage":
3209  $gamification_settings = $gamification_settings->withHighscorePercentage((bool) $metadata["entry"]);
3210  break;
3211  case "highscore_hints":
3212  $gamification_settings = $gamification_settings->withHighscoreHints((bool) $metadata["entry"]);
3213  break;
3214  case "highscore_wtime":
3215  $gamification_settings = $gamification_settings->withHighscoreWTime((bool) $metadata["entry"]);
3216  break;
3217  case "highscore_own_table":
3218  $gamification_settings = $gamification_settings->withHighscoreOwnTable((bool) $metadata["entry"]);
3219  break;
3220  case "highscore_top_table":
3221  $gamification_settings = $gamification_settings->withHighscoreTopTable((bool) $metadata["entry"]);
3222  break;
3223  case "highscore_top_num":
3224  $gamification_settings = $gamification_settings->withHighscoreTopNum((int) $metadata["entry"]);
3225  break;
3226  case "use_previous_answers":
3227  $participant_functionality_settings = $participant_functionality_settings->withUsePreviousAnswerAllowed((bool) $metadata["entry"]);
3228  break;
3229  case 'question_list_enabled':
3230  $participant_functionality_settings = $participant_functionality_settings->withQuestionListEnabled((bool) $metadata['entry']);
3231  // no break
3232  case "title_output":
3233  $question_behaviour_settings = $question_behaviour_settings->withQuestionTitleOutputMode((int) $metadata["entry"]);
3234  break;
3235  case "question_set_type":
3236  $general_settings = $general_settings->withQuestionSetType($metadata["entry"]);
3237  break;
3238  case "anonymity":
3239  $general_settings = $general_settings->withAnonymity((bool) $metadata["entry"]);
3240  break;
3241  case "results_presentation":
3242  $result_details_settings = $result_details_settings->withResultsPresentation((int) $metadata["entry"]);
3243  break;
3244  case "reset_processing_time":
3245  $test_behaviour_settings = $test_behaviour_settings->withResetProcessingTime($metadata["entry"] === '1');
3246  break;
3247  case "answer_feedback_points":
3248  $question_behaviour_settings = $question_behaviour_settings->withInstantFeedbackPointsEnabled((bool) $metadata["entry"]);
3249  break;
3250  case "answer_feedback":
3251  $question_behaviour_settings = $question_behaviour_settings->withInstantFeedbackGenericEnabled((bool) $metadata["entry"]);
3252  break;
3253  case 'instant_feedback_specific':
3254  $question_behaviour_settings = $question_behaviour_settings->withInstantFeedbackSpecificEnabled((bool) $metadata['entry']);
3255  break;
3256  case "instant_verification":
3257  $question_behaviour_settings = $question_behaviour_settings->withInstantFeedbackSolutionEnabled((bool) $metadata["entry"]);
3258  break;
3259  case "force_instant_feedback":
3260  $question_behaviour_settings = $question_behaviour_settings->withForceInstantFeedbackOnNextQuestion((bool) $metadata["entry"]);
3261  break;
3262  case "follow_qst_answer_fixation":
3263  $question_behaviour_settings = $question_behaviour_settings->withLockAnswerOnNextQuestionEnabled((bool) $metadata["entry"]);
3264  break;
3265  case "instant_feedback_answer_fixation":
3266  $question_behaviour_settings = $question_behaviour_settings->withLockAnswerOnInstantFeedbackEnabled((bool) $metadata["entry"]);
3267  break;
3268  case "show_cancel":
3269  case "suspend_test_allowed":
3270  $participant_functionality_settings = $participant_functionality_settings->withSuspendTestAllowed((bool) $metadata["entry"]);
3271  break;
3272  case "sequence_settings":
3273  $participant_functionality_settings = $participant_functionality_settings->withPostponedQuestionsMoveToEnd((bool) $metadata["entry"]);
3274  break;
3275  case "show_marker":
3276  $participant_functionality_settings = $participant_functionality_settings->withQuestionMarkingEnabled((bool) $metadata["entry"]);
3277  break;
3278  case "fixed_participants":
3279  $access_settings = $access_settings->withFixedParticipants((bool) $metadata["entry"]);
3280  break;
3281  case "score_reporting":
3282  $result_summary_settings = $result_summary_settings->withScoreReporting((int) $metadata["entry"]);
3283  break;
3284  case "shuffle_questions":
3285  $question_behaviour_settings = $question_behaviour_settings->withShuffleQuestions((bool) $metadata["entry"]);
3286  break;
3287  case "count_system":
3288  $scoring_settings = $scoring_settings->withCountSystem((int) $metadata["entry"]);
3289  break;
3290  case "mailnotification":
3291  $finishing_settings = $finishing_settings->withMailNotificationContentType((int) $metadata["entry"]);
3292  break;
3293  case "mailnottype":
3294  $finishing_settings = $finishing_settings->withAlwaysSendMailNotification((bool) $metadata["entry"]);
3295  break;
3296  case "exportsettings":
3297  $result_details_settings = $result_details_settings->withExportSettings((int) $metadata["entry"]);
3298  break;
3299  case "score_cutting":
3300  $scoring_settings = $scoring_settings->withScoreCutting((int) $metadata["entry"]);
3301  break;
3302  case "password":
3303  $access_settings = $access_settings->withPasswordEnabled(
3304  $metadata["entry"] !== null && $metadata["entry"] !== ''
3305  )->withPassword($metadata["entry"]);
3306  break;
3307  case "pass_scoring":
3308  $scoring_settings = $scoring_settings->withPassScoring((int) $metadata["entry"]);
3309  break;
3310  case 'pass_deletion_allowed':
3311  $result_summary_settings = $result_summary_settings->withPassDeletionAllowed((bool) $metadata["entry"]);
3312  break;
3313  case "usr_pass_overview_mode":
3314  $participant_functionality_settings = $participant_functionality_settings->withUsrPassOverviewMode((int) $metadata["entry"]);
3315  break;
3316  case "question_list":
3317  $participant_functionality_settings = $participant_functionality_settings->withQuestionListEnabled((bool) $metadata["entry"]);
3318  break;
3319 
3320  case "reporting_date":
3321  $reporting_date = $this->buildDateTimeImmutableFromPeriod($metadata['entry']);
3322  if ($reporting_date !== null) {
3323  $result_summary_settings = $result_summary_settings->withReportingDate($reporting_date);
3324  }
3325  break;
3326  case 'enable_processing_time':
3327  $test_behaviour_settings = $test_behaviour_settings->withProcessingTimeEnabled((bool) $metadata['entry']);
3328  break;
3329  case "processing_time":
3330  $test_behaviour_settings = $test_behaviour_settings->withProcessingTime($metadata['entry']);
3331  break;
3332  case "starting_time":
3333  $starting_time = $this->buildDateTimeImmutableFromPeriod($metadata['entry']);
3334  if ($starting_time !== null) {
3335  $access_settings = $access_settings->withStartTime($starting_time)
3336  ->withStartTimeEnabled(true);
3337  }
3338  break;
3339  case "ending_time":
3340  $ending_time = $this->buildDateTimeImmutableFromPeriod($metadata['entry']);
3341  if ($ending_time !== null) {
3342  $access_settings = $access_settings->withEndTime($ending_time)
3343  ->withStartTimeEnabled(true);
3344  }
3345  break;
3346  case "enable_examview":
3347  $finishing_settings = $finishing_settings->withShowAnswerOverview((bool) $metadata["entry"]);
3348  break;
3349  case 'redirection_mode':
3350  $finishing_settings = $finishing_settings->withRedirectionMode((int) $metadata['entry']);
3351  break;
3352  case 'redirection_url':
3353  $finishing_settings = $finishing_settings->withRedirectionUrl($metadata['entry']);
3354  break;
3355  case 'examid_in_test_pass':
3356  $test_behaviour_settings = $test_behaviour_settings->withExamIdInTestPassEnabled((bool) $metadata['entry']);
3357  break;
3358  case 'examid_in_test_res':
3359  $result_details_settings = $result_details_settings->withShowExamIdInTestResults((bool) $metadata["entry"]);
3360  break;
3361  case 'skill_service':
3362  $additional_settings = $additional_settings->withSkillsServiceEnabled((bool) $metadata['entry']);
3363  break;
3364  case 'show_grading_status':
3365  $result_summary_settings = $result_summary_settings->withShowGradingStatusEnabled((bool) $metadata["entry"]);
3366  break;
3367  case 'show_grading_mark':
3368  $result_summary_settings = $result_summary_settings->withShowGradingMarkEnabled((bool) $metadata["entry"]);
3369  break;
3370  case 'activation_limited':
3371  $this->setActivationLimited((bool) $metadata['entry']);
3372  break;
3373  case 'activation_start_time':
3374  $this->setActivationStartingTime($metadata['entry'] !== 'null' ? (int) $metadata['entry'] : null);
3375  break;
3376  case 'activation_end_time':
3377  $this->setActivationEndingTime($metadata['entry'] !== 'null' ? (int) $metadata['entry'] : null);
3378  break;
3379  case 'activation_visibility':
3380  $this->setActivationVisibility($metadata['entry']);
3381  break;
3382  case 'autosave':
3383  $question_behaviour_settings = $question_behaviour_settings->withAutosaveEnabled((bool) $metadata['entry']);
3384  break;
3385  case 'autosave_ival':
3386  $question_behaviour_settings = $question_behaviour_settings->withAutosaveInterval((int) $metadata['entry']);
3387  break;
3388  case 'offer_question_hints':
3389  $question_behaviour_settings = $question_behaviour_settings->withQuestionHintsEnabled((bool) $metadata['entry']);
3390  break;
3391  case 'obligations_enabled':
3392  $question_behaviour_settings = $question_behaviour_settings->withCompulsoryQuestionsEnabled((bool) $metadata['entry']);
3393  break;
3394  case 'show_summary':
3395  $participant_functionality_settings = $participant_functionality_settings->withQuestionListEnabled(($metadata['entry'] & 1) > 0)
3396  ->withUsrPassOverviewMode((int) $metadata['entry']);
3397 
3398  // no break
3399  case 'hide_info_tab':
3400  $additional_settings = $additional_settings->withHideInfoTab($metadata['entry'] === '1');
3401  }
3402  if (preg_match("/mark_step_\d+/", $metadata["label"])) {
3403  $xmlmark = $metadata["entry"];
3404  preg_match("/<short>(.*?)<\/short>/", $xmlmark, $matches);
3405  $mark_short = $matches[1];
3406  preg_match("/<official>(.*?)<\/official>/", $xmlmark, $matches);
3407  $mark_official = $matches[1];
3408  preg_match("/<percentage>(.*?)<\/percentage>/", $xmlmark, $matches);
3409  $mark_percentage = (float) $matches[1];
3410  preg_match("/<passed>(.*?)<\/passed>/", $xmlmark, $matches);
3411  $mark_passed = (int) $matches[1];
3412  $this->mark_schema->addMarkStep($mark_short, $mark_official, $mark_percentage, $mark_passed);
3413  }
3414  }
3415 
3416  $this->saveToDb();
3417  $this->getObjectProperties()->storePropertyTitleAndDescription(
3418  $this->getObjectProperties()->getPropertyTitleAndDescription()
3419  ->withTitle($assessment->getTitle())
3420  ->withDescription($assessment->getComment())
3421  );
3422  $this->addToNewsOnOnline(false, $this->getObjectProperties()->getPropertyIsOnline()->getIsOnline());
3423  $main_settings = $main_settings
3424  ->withGeneralSettings($general_settings)
3425  ->withIntroductionSettings($introduction_settings)
3426  ->withAccessSettings($access_settings)
3427  ->withParticipantFunctionalitySettings($participant_functionality_settings)
3428  ->withTestBehaviourSettings($test_behaviour_settings)
3429  ->withQuestionBehaviourSettings($question_behaviour_settings)
3430  ->withFinishingSettings($finishing_settings)
3431  ->withAdditionalSettings($additional_settings);
3432  $this->getMainSettingsRepository()->store($main_settings);
3433  $this->main_settings = $main_settings;
3434 
3435  $score_settings = $score_settings
3436  ->withGamificationSettings($gamification_settings)
3437  ->withScoringSettings($scoring_settings)
3438  ->withResultDetailsSettings($result_details_settings)
3439  ->withResultSummarySettings($result_summary_settings);
3440  $this->getScoreSettingsRepository()->store($score_settings);
3441  $this->score_settings = $score_settings;
3442  $this->loadFromDb();
3443  }
3444 
3447  array $material,
3448  array $mappings
3450  $text = $material['text'];
3451  $mobs = $material['mobs'];
3452  if (str_starts_with($text, '<PageObject>')) {
3453  $text = $this->replaceMobsInPageImports($text, $mappings['Services/MediaObjects']['mob'] ?? []);
3454  $text = $this->replaceFilesInPageImports($text, $mappings['Modules/File']['file'] ?? []);
3455  $page_object = new ilTestPage();
3456  $page_object->setParentId($this->getId());
3457  $page_object->setXMLContent($text);
3458  $new_page_id = $page_object->createPageWithNextId();
3459  return $settings->withIntroductionPageId($new_page_id);
3460  }
3461 
3462  $text = $this->retrieveMobsFromLegacyImports($text, $mobs);
3463 
3464  return new ilObjTestSettingsIntroduction(
3465  $settings->getTestId(),
3466  strlen($text) > 0,
3467  $text
3468  );
3469  }
3470 
3472  ilObjTestSettingsFinishing $settings,
3473  array $material,
3474  array $mappings
3476  $text = $material['text'];
3477  $mobs = $material['mobs'];
3478  if (str_starts_with($text, '<PageObject>')) {
3479  $text = $this->replaceMobsInPageImports($text, $mappings['Services/MediaObjects']['mob'] ?? []);
3480  $text = $this->replaceFilesInPageImports($text, $mappings['Modules/File']['file'] ?? []);
3481  $page_object = new ilTestPage();
3482  $page_object->setParentId($this->getId());
3483  $page_object->setXMLContent($text);
3484  $new_page_id = $page_object->createPageWithNextId();
3485  return $settings->withConcludingRemarksPageId($new_page_id);
3486  }
3487 
3488  $text = $this->retrieveMobsFromLegacyImports($text, $mobs);
3489 
3490  return new ilObjTestSettingsFinishing(
3491  $settings->getTestId(),
3492  $settings->getShowAnswerOverview(),
3493  strlen($text) > 0,
3494  $text,
3495  null,
3496  $settings->getRedirectionMode(),
3497  $settings->getRedirectionUrl(),
3498  $settings->getMailNotificationContentType(),
3499  $settings->getAlwaysSendMailNotification()
3500  );
3501  }
3502 
3503  private function replaceMobsInPageImports(string $text, array $mappings): string
3504  {
3505  preg_match_all('/il_(\d+)_mob_(\d+)/', $text, $matches);
3506  foreach ($matches[0] as $index => $match) {
3507  if (empty($mappings[$matches[2][$index]])) {
3508  continue;
3509  }
3510  $text = str_replace($match, "il__mob_{$mappings[$matches[2][$index]]}", $text);
3511  ilObjMediaObject::_saveUsage((int) $mappings[$matches[2][$index]], 'tst', $this->getId());
3512  }
3513  return $text;
3514  }
3515 
3516  private function replaceFilesInPageImports(string $text, $mappings): string
3517  {
3518  preg_match_all('/il_(\d+)_file_(\d+)/', $text, $matches);
3519  foreach ($matches[0] as $index => $match) {
3520  if (empty($mappings[$matches[2][$index]])) {
3521  continue;
3522  }
3523  $text = str_replace($match, "il__file_{$mappings[$matches[2][$index]]}", $text);
3524  }
3525  return $text;
3526  }
3527 
3528  private function retrieveMobsFromLegacyImports(string $text, array $mobs): string
3529  {
3530  foreach ($mobs as $mob) {
3531  $importfile = ilObjTest::_getImportDirectory() . '/' . ilSession::get('tst_import_subdir') . '/' . $mob['uri'];
3532  if (file_exists($importfile)) {
3533  $media_object = ilObjMediaObject::_saveTempFileAsMediaObject(basename($importfile), $importfile, false);
3534  ilObjMediaObject::_saveUsage($media_object->getId(), 'tst:html', $this->getId());
3536  str_replace(
3537  'src="' . $mob['mob'] . '"',
3538  'src="' . 'il_' . IL_INST_ID . '_mob_' . $media_object->getId() . '"',
3539  $text
3540  ),
3541  1
3542  );
3543  }
3544  }
3545  return $text;
3546  }
3547 
3553  public function toXML(): string
3554  {
3555  $main_settings = $this->getMainSettings();
3556  $a_xml_writer = new ilXmlWriter();
3557  // set xml header
3558  $a_xml_writer->xmlHeader();
3559  $a_xml_writer->xmlSetDtdDef("<!DOCTYPE questestinterop SYSTEM \"ims_qtiasiv1p2p1.dtd\">");
3560  $a_xml_writer->xmlStartTag("questestinterop");
3561 
3562  $attrs = [
3563  "ident" => "il_" . IL_INST_ID . "_tst_" . $this->getTestId(),
3564  "title" => $this->getTitle()
3565  ];
3566  $a_xml_writer->xmlStartTag("assessment", $attrs);
3567  // add qti comment
3568  $a_xml_writer->xmlElement("qticomment", null, $this->getDescription());
3569 
3570  // add qti duration
3571  if ($main_settings->getTestBehaviourSettings()->getProcessingTimeEnabled()) {
3572  $processing_time_array = $this->getProcessingTimeAsArray();
3573  $a_xml_writer->xmlElement(
3574  "duration",
3575  null,
3576  sprintf(
3577  "P0Y0M0DT%dH%dM%dS",
3578  $processing_time_array['hh'],
3579  $processing_time_array['mm'],
3580  $processing_time_array['ss']
3581  )
3582  );
3583  }
3584 
3585  // add the rest of the preferences in qtimetadata tags, because there is no correspondent definition in QTI
3586  $a_xml_writer->xmlStartTag("qtimetadata");
3587  $a_xml_writer->xmlStartTag("qtimetadatafield");
3588  $a_xml_writer->xmlElement("fieldlabel", null, "ILIAS_VERSION");
3589  $a_xml_writer->xmlElement("fieldentry", null, ILIAS_VERSION);
3590  $a_xml_writer->xmlEndTag("qtimetadatafield");
3591 
3592  // anonymity
3593  $a_xml_writer->xmlStartTag("qtimetadatafield");
3594  $a_xml_writer->xmlElement("fieldlabel", null, "anonymity");
3595  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $main_settings->getGeneralSettings()->getAnonymity()));
3596  $a_xml_writer->xmlEndTag("qtimetadatafield");
3597 
3598  // question set type (fixed, random, ...)
3599  $a_xml_writer->xmlStartTag("qtimetadatafield");
3600  $a_xml_writer->xmlElement("fieldlabel", null, "question_set_type");
3601  $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getGeneralSettings()->getQuestionSetType());
3602  $a_xml_writer->xmlEndTag("qtimetadatafield");
3603 
3604  // sequence settings
3605  $a_xml_writer->xmlStartTag("qtimetadatafield");
3606  $a_xml_writer->xmlElement("fieldlabel", null, "sequence_settings");
3607  $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getParticipantFunctionalitySettings()->getPostponedQuestionsMoveToEnd());
3608  $a_xml_writer->xmlEndTag("qtimetadatafield");
3609 
3610  // author
3611  $a_xml_writer->xmlStartTag("qtimetadatafield");
3612  $a_xml_writer->xmlElement("fieldlabel", null, "author");
3613  $a_xml_writer->xmlElement("fieldentry", null, $this->getAuthor());
3614  $a_xml_writer->xmlEndTag("qtimetadatafield");
3615 
3616  // reset processing time
3617  $a_xml_writer->xmlStartTag("qtimetadatafield");
3618  $a_xml_writer->xmlElement("fieldlabel", null, "reset_processing_time");
3619  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getTestBehaviourSettings()->getResetProcessingTime());
3620  $a_xml_writer->xmlEndTag("qtimetadatafield");
3621 
3622  // count system
3623  $a_xml_writer->xmlStartTag("qtimetadatafield");
3624  $a_xml_writer->xmlElement("fieldlabel", null, "count_system");
3625  $a_xml_writer->xmlElement("fieldentry", null, $this->getCountSystem());
3626  $a_xml_writer->xmlEndTag("qtimetadatafield");
3627 
3628  // multiple choice scoring
3629  $a_xml_writer->xmlStartTag("qtimetadatafield");
3630  $a_xml_writer->xmlElement("fieldlabel", null, "score_cutting");
3631  $a_xml_writer->xmlElement("fieldentry", null, $this->getScoreCutting());
3632  $a_xml_writer->xmlEndTag("qtimetadatafield");
3633 
3634  // multiple choice scoring
3635  $a_xml_writer->xmlStartTag("qtimetadatafield");
3636  $a_xml_writer->xmlElement("fieldlabel", null, "password");
3637  $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getAccessSettings()->getPassword() ?? '');
3638  $a_xml_writer->xmlEndTag("qtimetadatafield");
3639 
3640  // pass scoring
3641  $a_xml_writer->xmlStartTag("qtimetadatafield");
3642  $a_xml_writer->xmlElement("fieldlabel", null, "pass_scoring");
3643  $a_xml_writer->xmlElement("fieldentry", null, $this->getPassScoring());
3644  $a_xml_writer->xmlEndTag("qtimetadatafield");
3645 
3646  $a_xml_writer->xmlStartTag('qtimetadatafield');
3647  $a_xml_writer->xmlElement('fieldlabel', null, 'pass_deletion_allowed');
3648  $a_xml_writer->xmlElement('fieldentry', null, (int) $this->isPassDeletionAllowed());
3649  $a_xml_writer->xmlEndTag('qtimetadatafield');
3650 
3651  // score reporting date
3652  if ($this->getScoreSettings()->getResultSummarySettings()->getReportingDate() !== null) {
3653  $a_xml_writer->xmlStartTag("qtimetadatafield");
3654  $a_xml_writer->xmlElement("fieldlabel", null, "reporting_date");
3655  $a_xml_writer->xmlElement(
3656  "fieldentry",
3657  null,
3659  $this->getScoreSettings()->getResultSummarySettings()->getReportingDate(),
3660  ),
3661  );
3662  $a_xml_writer->xmlEndTag("qtimetadatafield");
3663  }
3664  // number of tries
3665  $a_xml_writer->xmlStartTag("qtimetadatafield");
3666  $a_xml_writer->xmlElement("fieldlabel", null, "nr_of_tries");
3667  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $main_settings->getTestBehaviourSettings()->getNumberOfTries()));
3668  $a_xml_writer->xmlEndTag("qtimetadatafield");
3669 
3670  // number of tries
3671  $a_xml_writer->xmlStartTag('qtimetadatafield');
3672  $a_xml_writer->xmlElement('fieldlabel', null, 'block_after_passed');
3673  $a_xml_writer->xmlElement('fieldentry', null, (int) $main_settings->getTestBehaviourSettings()->getBlockAfterPassedEnabled());
3674  $a_xml_writer->xmlEndTag('qtimetadatafield');
3675 
3676  // pass_waiting
3677  $a_xml_writer->xmlStartTag("qtimetadatafield");
3678  $a_xml_writer->xmlElement("fieldlabel", null, "pass_waiting");
3679  $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getTestBehaviourSettings()->getPassWaiting());
3680  $a_xml_writer->xmlEndTag("qtimetadatafield");
3681 
3682  // kiosk
3683  $a_xml_writer->xmlStartTag("qtimetadatafield");
3684  $a_xml_writer->xmlElement("fieldlabel", null, "kiosk");
3685  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $main_settings->getTestBehaviourSettings()->getKioskMode()));
3686  $a_xml_writer->xmlEndTag("qtimetadatafield");
3687 
3688 
3689  //redirection_mode
3690  $a_xml_writer->xmlStartTag('qtimetadatafield');
3691  $a_xml_writer->xmlElement("fieldlabel", null, "redirection_mode");
3692  $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getFinishingSettings()->getRedirectionMode());
3693  $a_xml_writer->xmlEndTag("qtimetadatafield");
3694 
3695  //redirection_url
3696  $a_xml_writer->xmlStartTag('qtimetadatafield');
3697  $a_xml_writer->xmlElement("fieldlabel", null, "redirection_url");
3698  $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getFinishingSettings()->getRedirectionUrl());
3699  $a_xml_writer->xmlEndTag("qtimetadatafield");
3700 
3701  // use previous answers
3702  $a_xml_writer->xmlStartTag("qtimetadatafield");
3703  $a_xml_writer->xmlElement("fieldlabel", null, "use_previous_answers");
3704  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getParticipantFunctionalitySettings()->getUsePreviousAnswerAllowed());
3705  $a_xml_writer->xmlEndTag("qtimetadatafield");
3706 
3707  $a_xml_writer->xmlStartTag('qtimetadatafield');
3708  $a_xml_writer->xmlElement('fieldlabel', null, 'question_list_enabled');
3709  $a_xml_writer->xmlElement('fieldentry', null, (int) $main_settings->getParticipantFunctionalitySettings()->getQuestionListEnabled());
3710  $a_xml_writer->xmlEndTag('qtimetadatafield');
3711 
3712  $a_xml_writer->xmlStartTag("qtimetadatafield");
3713  $a_xml_writer->xmlElement("fieldlabel", null, "title_output");
3714  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $main_settings->getQuestionBehaviourSettings()->getQuestionTitleOutputMode()));
3715  $a_xml_writer->xmlEndTag("qtimetadatafield");
3716 
3717  // results presentation
3718  $a_xml_writer->xmlStartTag("qtimetadatafield");
3719  $a_xml_writer->xmlElement("fieldlabel", null, "results_presentation");
3720  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $this->getScoreSettings()->getResultDetailsSettings()->getResultsPresentation()));
3721  $a_xml_writer->xmlEndTag("qtimetadatafield");
3722 
3723  // examid in test pass
3724  $a_xml_writer->xmlStartTag("qtimetadatafield");
3725  $a_xml_writer->xmlElement("fieldlabel", null, "examid_in_test_pass");
3726  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $main_settings->getTestBehaviourSettings()->getExamIdInTestPassEnabled()));
3727  $a_xml_writer->xmlEndTag("qtimetadatafield");
3728 
3729  // examid in kiosk
3730  $a_xml_writer->xmlStartTag("qtimetadatafield");
3731  $a_xml_writer->xmlElement("fieldlabel", null, "examid_in_test_res");
3732  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $this->getScoreSettings()->getResultDetailsSettings()->getShowExamIdInTestResults()));
3733  $a_xml_writer->xmlEndTag("qtimetadatafield");
3734 
3735  // solution details
3736  $a_xml_writer->xmlStartTag("qtimetadatafield");
3737  $a_xml_writer->xmlElement("fieldlabel", null, "usr_pass_overview_mode");
3738  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $main_settings->getParticipantFunctionalitySettings()->getUsrPassOverviewMode()));
3739  $a_xml_writer->xmlEndTag("qtimetadatafield");
3740 
3741  // solution details
3742  $a_xml_writer->xmlStartTag("qtimetadatafield");
3743  $a_xml_writer->xmlElement("fieldlabel", null, "score_reporting");
3744  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $this->getScoreSettings()->getResultSummarySettings()->getScoreReporting()));
3745  $a_xml_writer->xmlEndTag("qtimetadatafield");
3746 
3747  $a_xml_writer->xmlStartTag("qtimetadatafield");
3748  $a_xml_writer->xmlElement("fieldlabel", null, "show_solution_list_comparison");
3749  $a_xml_writer->xmlElement("fieldentry", null, (int) $this->score_settings->getResultDetailsSettings()->getShowSolutionListComparison());
3750  $a_xml_writer->xmlEndTag("qtimetadatafield");
3751 
3752  // solution details
3753  $a_xml_writer->xmlStartTag("qtimetadatafield");
3754  $a_xml_writer->xmlElement("fieldlabel", null, "instant_verification");
3755  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackSolutionEnabled()));
3756  $a_xml_writer->xmlEndTag("qtimetadatafield");
3757 
3758  // generic feedback
3759  $a_xml_writer->xmlStartTag("qtimetadatafield");
3760  $a_xml_writer->xmlElement("fieldlabel", null, "answer_feedback");
3761  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackGenericEnabled()));
3762  $a_xml_writer->xmlEndTag("qtimetadatafield");
3763 
3764  // answer specific feedback
3765  $a_xml_writer->xmlStartTag("qtimetadatafield");
3766  $a_xml_writer->xmlElement("fieldlabel", null, "instant_feedback_specific");
3767  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackSpecificEnabled()));
3768  $a_xml_writer->xmlEndTag("qtimetadatafield");
3769 
3770  // answer specific feedback of reached points
3771  $a_xml_writer->xmlStartTag("qtimetadatafield");
3772  $a_xml_writer->xmlElement("fieldlabel", null, "answer_feedback_points");
3773  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackPointsEnabled()));
3774  $a_xml_writer->xmlEndTag("qtimetadatafield");
3775 
3776  // followup question previous answer freezing
3777  $a_xml_writer->xmlStartTag("qtimetadatafield");
3778  $a_xml_writer->xmlElement("fieldlabel", null, "follow_qst_answer_fixation");
3779  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getQuestionBehaviourSettings()->getLockAnswerOnNextQuestionEnabled());
3780  $a_xml_writer->xmlEndTag("qtimetadatafield");
3781 
3782  // instant response answer freezing
3783  $a_xml_writer->xmlStartTag("qtimetadatafield");
3784  $a_xml_writer->xmlElement("fieldlabel", null, "instant_feedback_answer_fixation");
3785  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getQuestionBehaviourSettings()->getLockAnswerOnInstantFeedbackEnabled());
3786  $a_xml_writer->xmlEndTag("qtimetadatafield");
3787 
3788  // instant response forced
3789  $a_xml_writer->xmlStartTag("qtimetadatafield");
3790  $a_xml_writer->xmlElement("fieldlabel", null, "force_instant_feedback");
3791  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getQuestionBehaviourSettings()->getForceInstantFeedbackOnNextQuestion());
3792  $a_xml_writer->xmlEndTag("qtimetadatafield");
3793 
3794 
3795  // highscore
3796  $highscore_metadata = [
3797  'highscore_enabled' => ['value' => $this->getHighscoreEnabled()],
3798  'highscore_anon' => ['value' => $this->getHighscoreAnon()],
3799  'highscore_achieved_ts' => ['value' => $this->getHighscoreAchievedTS()],
3800  'highscore_score' => ['value' => $this->getHighscoreScore()],
3801  'highscore_percentage' => ['value' => $this->getHighscorePercentage()],
3802  'highscore_hints' => ['value' => $this->getHighscoreHints()],
3803  'highscore_wtime' => ['value' => $this->getHighscoreWTime()],
3804  'highscore_own_table' => ['value' => $this->getHighscoreOwnTable()],
3805  'highscore_top_table' => ['value' => $this->getHighscoreTopTable()],
3806  'highscore_top_num' => ['value' => $this->getHighscoreTopNum()],
3807  ];
3808  foreach ($highscore_metadata as $label => $data) {
3809  $a_xml_writer->xmlStartTag("qtimetadatafield");
3810  $a_xml_writer->xmlElement("fieldlabel", null, $label);
3811  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", $data['value']));
3812  $a_xml_writer->xmlEndTag("qtimetadatafield");
3813  }
3814 
3815  // show cancel
3816  $a_xml_writer->xmlStartTag("qtimetadatafield");
3817  $a_xml_writer->xmlElement("fieldlabel", null, "suspend_test_allowed");
3818  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getParticipantFunctionalitySettings()->getSuspendTestAllowed()));
3819  $a_xml_writer->xmlEndTag("qtimetadatafield");
3820 
3821  // show marker
3822  $a_xml_writer->xmlStartTag("qtimetadatafield");
3823  $a_xml_writer->xmlElement("fieldlabel", null, "show_marker");
3824  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getParticipantFunctionalitySettings()->getQuestionMarkingEnabled()));
3825  $a_xml_writer->xmlEndTag("qtimetadatafield");
3826 
3827  // fixed participants
3828  $a_xml_writer->xmlStartTag("qtimetadatafield");
3829  $a_xml_writer->xmlElement("fieldlabel", null, "fixed_participants");
3830  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getAccessSettings()->getFixedParticipants()));
3831  $a_xml_writer->xmlEndTag("qtimetadatafield");
3832 
3833  // show final statement
3834  $a_xml_writer->xmlStartTag("qtimetadatafield");
3835  $a_xml_writer->xmlElement("fieldlabel", null, "show_introduction");
3836  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getIntroductionSettings()->getIntroductionEnabled()));
3837  $a_xml_writer->xmlEndTag("qtimetadatafield");
3838 
3839  // show final statement
3840  $a_xml_writer->xmlStartTag("qtimetadatafield");
3841  $a_xml_writer->xmlElement("fieldlabel", null, 'exam_conditions');
3842  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getIntroductionSettings()->getExamConditionsCheckboxEnabled()));
3843  $a_xml_writer->xmlEndTag("qtimetadatafield");
3844 
3845  $a_xml_writer->xmlStartTag("qtimetadatafield");
3846  $a_xml_writer->xmlElement("fieldlabel", null, "show_concluding_remarks");
3847  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getFinishingSettings()->getConcludingRemarksEnabled()));
3848  $a_xml_writer->xmlEndTag("qtimetadatafield");
3849 
3850  // mail notification
3851  $a_xml_writer->xmlStartTag("qtimetadatafield");
3852  $a_xml_writer->xmlElement("fieldlabel", null, "mailnotification");
3853  $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getFinishingSettings()->getMailNotificationContentType());
3854  $a_xml_writer->xmlEndTag("qtimetadatafield");
3855 
3856  // mail notification type
3857  $a_xml_writer->xmlStartTag("qtimetadatafield");
3858  $a_xml_writer->xmlElement("fieldlabel", null, "mailnottype");
3859  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getFinishingSettings()->getAlwaysSendMailNotification());
3860  $a_xml_writer->xmlEndTag("qtimetadatafield");
3861 
3862  // export settings
3863  $a_xml_writer->xmlStartTag("qtimetadatafield");
3864  $a_xml_writer->xmlElement("fieldlabel", null, "exportsettings");
3865  $a_xml_writer->xmlElement("fieldentry", null, $this->getExportSettings());
3866  $a_xml_writer->xmlEndTag("qtimetadatafield");
3867 
3868  // shuffle questions
3869  $a_xml_writer->xmlStartTag("qtimetadatafield");
3870  $a_xml_writer->xmlElement("fieldlabel", null, "shuffle_questions");
3871  $a_xml_writer->xmlElement("fieldentry", null, sprintf("%d", (int) $main_settings->getQuestionBehaviourSettings()->getShuffleQuestions()));
3872  $a_xml_writer->xmlEndTag("qtimetadatafield");
3873 
3874  // processing time
3875  $a_xml_writer->xmlStartTag("qtimetadatafield");
3876  $a_xml_writer->xmlElement("fieldlabel", null, "processing_time");
3877  $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getTestBehaviourSettings()->getProcessingTime());
3878  $a_xml_writer->xmlEndTag("qtimetadatafield");
3879 
3880  // enable_examview
3881  $a_xml_writer->xmlStartTag("qtimetadatafield");
3882  $a_xml_writer->xmlElement("fieldlabel", null, "enable_examview");
3883  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getFinishingSettings()->getShowAnswerOverview());
3884  $a_xml_writer->xmlEndTag("qtimetadatafield");
3885 
3886 
3887  // skill_service
3888  $a_xml_writer->xmlStartTag("qtimetadatafield");
3889  $a_xml_writer->xmlElement("fieldlabel", null, "skill_service");
3890  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getAdditionalSettings()->getSkillsServiceEnabled());
3891  $a_xml_writer->xmlEndTag("qtimetadatafield");
3892 
3893  // add qti assessmentcontrol
3894  if ($this->getInstantFeedbackSolution() == 1) {
3895  $attrs = [
3896  "solutionswitch" => "Yes"
3897  ];
3898  } else {
3899  $attrs = null;
3900  }
3901  $a_xml_writer->xmlElement("assessmentcontrol", $attrs, null);
3902 
3903  // show_grading_status
3904  $a_xml_writer->xmlStartTag("qtimetadatafield");
3905  $a_xml_writer->xmlElement("fieldlabel", null, "show_grading_status");
3906  $a_xml_writer->xmlElement("fieldentry", null, (int) $this->isShowGradingStatusEnabled());
3907  $a_xml_writer->xmlEndTag("qtimetadatafield");
3908 
3909  // show_grading_mark
3910  $a_xml_writer->xmlStartTag("qtimetadatafield");
3911  $a_xml_writer->xmlElement("fieldlabel", null, "show_grading_mark");
3912  $a_xml_writer->xmlElement("fieldentry", null, (int) $this->isShowGradingMarkEnabled());
3913  $a_xml_writer->xmlEndTag("qtimetadatafield");
3914 
3915  $a_xml_writer->xmlStartTag('qtimetadatafield');
3916  $a_xml_writer->xmlElement('fieldlabel', null, 'hide_info_tab');
3917  $a_xml_writer->xmlElement('fieldentry', null, (int) $this->getMainSettings()->getAdditionalSettings()->getHideInfoTab());
3918  $a_xml_writer->xmlEndTag("qtimetadatafield");
3919 
3920  if ($this->getStartingTime() > 0) {
3921  $a_xml_writer->xmlStartTag("qtimetadatafield");
3922  $a_xml_writer->xmlElement("fieldlabel", null, "starting_time");
3923  $a_xml_writer->xmlElement(
3924  "fieldentry",
3925  null,
3927  (new DateTimeImmutable())->setTimestamp($this->getStartingTime()),
3928  ),
3929  );
3930  $a_xml_writer->xmlEndTag("qtimetadatafield");
3931  }
3932  // ending time
3933  if ($this->getEndingTime() > 0) {
3934  $a_xml_writer->xmlStartTag("qtimetadatafield");
3935  $a_xml_writer->xmlElement("fieldlabel", null, "ending_time");
3936  $a_xml_writer->xmlElement(
3937  "fieldentry",
3938  null,
3940  (new DateTimeImmutable())->setTimestamp($this->getEndingTime()),
3941  ),
3942  );
3943  $a_xml_writer->xmlEndTag("qtimetadatafield");
3944  }
3945 
3946 
3947  //activation_limited
3948  $a_xml_writer->xmlStartTag("qtimetadatafield");
3949  $a_xml_writer->xmlElement("fieldlabel", null, "activation_limited");
3950  $a_xml_writer->xmlElement("fieldentry", null, (int) $this->isActivationLimited());
3951  $a_xml_writer->xmlEndTag("qtimetadatafield");
3952 
3953  //activation_start_time
3954  $a_xml_writer->xmlStartTag("qtimetadatafield");
3955  $a_xml_writer->xmlElement("fieldlabel", null, "activation_start_time");
3956  $a_xml_writer->xmlElement("fieldentry", null, (int) $this->getActivationStartingTime());
3957  $a_xml_writer->xmlEndTag("qtimetadatafield");
3958 
3959  //activation_end_time
3960  $a_xml_writer->xmlStartTag("qtimetadatafield");
3961  $a_xml_writer->xmlElement("fieldlabel", null, "activation_end_time");
3962  $a_xml_writer->xmlElement("fieldentry", null, (int) $this->getActivationEndingTime());
3963  $a_xml_writer->xmlEndTag("qtimetadatafield");
3964 
3965  //activation_visibility
3966  $a_xml_writer->xmlStartTag("qtimetadatafield");
3967  $a_xml_writer->xmlElement("fieldlabel", null, "activation_visibility");
3968  $a_xml_writer->xmlElement("fieldentry", null, (int) $this->getActivationVisibility());
3969  $a_xml_writer->xmlEndTag("qtimetadatafield");
3970 
3971  // autosave
3972  $a_xml_writer->xmlStartTag("qtimetadatafield");
3973  $a_xml_writer->xmlElement("fieldlabel", null, "autosave");
3974  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getQuestionBehaviourSettings()->getAutosaveEnabled());
3975  $a_xml_writer->xmlEndTag("qtimetadatafield");
3976 
3977  // autosave_ival
3978  $a_xml_writer->xmlStartTag("qtimetadatafield");
3979  $a_xml_writer->xmlElement("fieldlabel", null, "autosave_ival");
3980  $a_xml_writer->xmlElement("fieldentry", null, $main_settings->getQuestionBehaviourSettings()->getAutosaveInterval());
3981  $a_xml_writer->xmlEndTag("qtimetadatafield");
3982 
3983  //offer_question_hints
3984  $a_xml_writer->xmlStartTag("qtimetadatafield");
3985  $a_xml_writer->xmlElement("fieldlabel", null, "offer_question_hints");
3986  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getQuestionBehaviourSettings()->getQuestionHintsEnabled());
3987  $a_xml_writer->xmlEndTag("qtimetadatafield");
3988 
3989  //instant_feedback_specific
3990  $a_xml_writer->xmlStartTag("qtimetadatafield");
3991  $a_xml_writer->xmlElement("fieldlabel", null, "instant_feedback_specific");
3992  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackSpecificEnabled());
3993  $a_xml_writer->xmlEndTag("qtimetadatafield");
3994 
3995  //instant_feedback_answer_fixation
3996  $a_xml_writer->xmlStartTag("qtimetadatafield");
3997  $a_xml_writer->xmlElement("fieldlabel", null, "instant_feedback_answer_fixation");
3998  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getQuestionBehaviourSettings()->getLockAnswerOnInstantFeedbackEnabled());
3999  $a_xml_writer->xmlEndTag("qtimetadatafield");
4000 
4001  //obligations_enabled
4002  $a_xml_writer->xmlStartTag("qtimetadatafield");
4003  $a_xml_writer->xmlElement("fieldlabel", null, "obligations_enabled");
4004  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getQuestionBehaviourSettings()->getCompulsoryQuestionsEnabled());
4005  $a_xml_writer->xmlEndTag("qtimetadatafield");
4006 
4007  //enable_processing_time
4008  $a_xml_writer->xmlStartTag("qtimetadatafield");
4009  $a_xml_writer->xmlElement("fieldlabel", null, "enable_processing_time");
4010  $a_xml_writer->xmlElement("fieldentry", null, (int) $main_settings->getTestBehaviourSettings()->getProcessingTimeEnabled());
4011  $a_xml_writer->xmlEndTag("qtimetadatafield");
4012 
4013  foreach ($this->mark_schema->mark_steps as $index => $mark) {
4014  // mark steps
4015  $a_xml_writer->xmlStartTag("qtimetadatafield");
4016  $a_xml_writer->xmlElement("fieldlabel", null, "mark_step_$index");
4017  $a_xml_writer->xmlElement("fieldentry", null, sprintf(
4018  "<short>%s</short><official>%s</official><percentage>%.2f</percentage><passed>%d</passed>",
4019  $mark->getShortName(),
4020  $mark->getOfficialName(),
4021  $mark->getMinimumLevel(),
4022  $mark->getPassed()
4023  ));
4024  $a_xml_writer->xmlEndTag("qtimetadatafield");
4025  }
4026  $a_xml_writer->xmlEndTag("qtimetadata");
4027 
4028  $page_id = $main_settings->getIntroductionSettings()->getIntroductionPageId();
4029  $introduction = $page_id !== null
4030  ? (new ilTestPage($page_id))->getXMLContent()
4032 
4033  // add qti objectives
4034  $a_xml_writer->xmlStartTag("objectives");
4035  $this->addQTIMaterial($a_xml_writer, $page_id, $introduction);
4036  $a_xml_writer->xmlEndTag("objectives");
4037 
4038  // add qti assessmentcontrol
4039  if ($this->getInstantFeedbackSolution() == 1) {
4040  $attrs = [
4041  "solutionswitch" => "Yes"
4042  ];
4043  } else {
4044  $attrs = null;
4045  }
4046  $a_xml_writer->xmlElement("assessmentcontrol", $attrs, null);
4047 
4048  if (strlen($this->getFinalStatement())) {
4049  $page_id = $main_settings->getFinishingSettings()->getConcludingRemarksPageId();
4050  $concluding_remarks = $page_id !== null
4051  ? (new ilTestPage($page_id))->getXMLContent()
4053  // add qti presentation_material
4054  $a_xml_writer->xmlStartTag("presentation_material");
4055  $a_xml_writer->xmlStartTag("flow_mat");
4056  $this->addQTIMaterial($a_xml_writer, $page_id, $concluding_remarks);
4057  $a_xml_writer->xmlEndTag("flow_mat");
4058  $a_xml_writer->xmlEndTag("presentation_material");
4059  }
4060 
4061  $attrs = [
4062  "ident" => "1"
4063  ];
4064  $a_xml_writer->xmlElement("section", $attrs, null);
4065  $a_xml_writer->xmlEndTag("assessment");
4066  $a_xml_writer->xmlEndTag("questestinterop");
4067 
4068  $xml = $a_xml_writer->xmlDumpMem(false);
4069  return $xml;
4070  }
4071 
4072  protected function buildIso8601PeriodForExportCompatibility(DateTimeImmutable $date_time): string
4073  {
4074  return $date_time->setTimezone(new DateTimeZone('UTC'))->format('\PY\Yn\Mj\D\TG\Hi\Ms\S');
4075  }
4076 
4077  protected function buildDateTimeImmutableFromPeriod(?string $period): ?DateTimeImmutable
4078  {
4079  if ($period === null) {
4080  return null;
4081  }
4082  if (preg_match("/P(\d+)Y(\d+)M(\d+)DT(\d+)H(\d+)M(\d+)S/", $period, $matches)) {
4083  return new DateTimeImmutable(
4084  sprintf(
4085  "%02d-%02d-%02d %02d:%02d:%02d",
4086  $matches[1],
4087  $matches[2],
4088  $matches[3],
4089  $matches[4],
4090  $matches[5],
4091  $matches[6]
4092  ),
4093  new \DateTimeZone('UTC')
4094  );
4095  }
4096  return null;
4097  }
4098 
4105  public function exportPagesXML(&$a_xml_writer, $a_inst, $a_target_dir, &$expLog): void
4106  {
4107  $this->mob_ids = [];
4108 
4109  // MetaData
4110  $this->exportXMLMetaData($a_xml_writer);
4111 
4112  // PageObjects
4113  $expLog->write(date("[y-m-d H:i:s] ") . "Start Export Page Objects");
4114  $this->bench->start("ContentObjectExport", "exportPageObjects");
4115  $this->exportXMLPageObjects($a_xml_writer, $a_inst, $expLog);
4116  $this->bench->stop("ContentObjectExport", "exportPageObjects");
4117  $expLog->write(date("[y-m-d H:i:s] ") . "Finished Export Page Objects");
4118 
4119  // MediaObjects
4120  $expLog->write(date("[y-m-d H:i:s] ") . "Start Export Media Objects");
4121  $this->bench->start("ContentObjectExport", "exportMediaObjects");
4122  $this->exportXMLMediaObjects($a_xml_writer, $a_inst, $a_target_dir, $expLog);
4123  $this->bench->stop("ContentObjectExport", "exportMediaObjects");
4124  $expLog->write(date("[y-m-d H:i:s] ") . "Finished Export Media Objects");
4125 
4126  // FileItems
4127  $expLog->write(date("[y-m-d H:i:s] ") . "Start Export File Items");
4128  $this->bench->start("ContentObjectExport", "exportFileItems");
4129  $this->exportFileItems($a_target_dir, $expLog);
4130  $this->bench->stop("ContentObjectExport", "exportFileItems");
4131  $expLog->write(date("[y-m-d H:i:s] ") . "Finished Export File Items");
4132  }
4133 
4140  public function exportXMLMetaData(&$a_xml_writer)
4141  {
4142  $md2xml = new ilMD2XML($this->getId(), 0, $this->getType());
4143  $md2xml->setExportMode(true);
4144  $md2xml->startExport();
4145  $a_xml_writer->appendXML($md2xml->getXML());
4146  }
4147 
4153  public function modifyExportIdentifier($a_tag, $a_param, $a_value)
4154  {
4155  if ($a_tag == "Identifier" && $a_param == "Entry") {
4156  $a_value = ilUtil::insertInstIntoID($a_value);
4157  }
4158 
4159  return $a_value;
4160  }
4161 
4162 
4169  public function exportXMLPageObjects(&$a_xml_writer, $inst, &$expLog)
4170  {
4171  foreach ($this->questions as $question_id) {
4172  $this->bench->start("ContentObjectExport", "exportPageObject");
4173  $expLog->write(date("[y-m-d H:i:s] ") . "Page Object " . $question_id);
4174 
4175  $attrs = [];
4176  $a_xml_writer->xmlStartTag("PageObject", $attrs);
4177 
4178 
4179  // export xml to writer object
4180  $this->bench->start("ContentObjectExport", "exportPageObject_XML");
4181  $page_object = new ilAssQuestionPage($question_id);
4182  $page_object->buildDom();
4183  $page_object->insertInstIntoIDs((string) $inst);
4184  $mob_ids = $page_object->collectMediaObjects(false);
4185  $file_ids = ilPCFileList::collectFileItems($page_object, $page_object->getDomDoc());
4186  $xml = $page_object->getXMLFromDom(false, false, false, "", true);
4187  $xml = str_replace("&", "&amp;", $xml);
4188  $a_xml_writer->appendXML($xml);
4189  $page_object->freeDom();
4190  unset($page_object);
4191 
4192  $this->bench->stop("ContentObjectExport", "exportPageObject_XML");
4193 
4194  // collect media objects
4195  $this->bench->start("ContentObjectExport", "exportPageObject_CollectMedia");
4196  //$mob_ids = $page_obj->getMediaObjectIDs();
4197  foreach ($mob_ids as $mob_id) {
4198  $this->mob_ids[$mob_id] = $mob_id;
4199  }
4200  $this->bench->stop("ContentObjectExport", "exportPageObject_CollectMedia");
4201 
4202  // collect all file items
4203  $this->bench->start("ContentObjectExport", "exportPageObject_CollectFileItems");
4204  //$file_ids = $page_obj->getFileItemIds();
4205  foreach ($file_ids as $file_id) {
4206  $this->file_ids[$file_id] = $file_id;
4207  }
4208  $this->bench->stop("ContentObjectExport", "exportPageObject_CollectFileItems");
4209 
4210  $a_xml_writer->xmlEndTag("PageObject");
4211  //unset($page_obj);
4212 
4213  $this->bench->stop("ContentObjectExport", "exportPageObject");
4214  }
4215  }
4216 
4220  public function exportXMLMediaObjects(&$a_xml_writer, $a_inst, $a_target_dir, &$expLog)
4221  {
4222  foreach ($this->mob_ids as $mob_id) {
4223  $expLog->write(date("[y-m-d H:i:s] ") . "Media Object " . $mob_id);
4224  if (ilObjMediaObject::_exists((int) $mob_id)) {
4225  $media_obj = new ilObjMediaObject((int) $mob_id);
4226  $media_obj->exportXML($a_xml_writer, (int) $a_inst);
4227  $media_obj->exportFiles($a_target_dir);
4228  unset($media_obj);
4229  }
4230  }
4231  }
4232 
4237  public function exportFileItems($target_dir, &$expLog)
4238  {
4239  foreach ($this->file_ids as $file_id) {
4240  $expLog->write(date("[y-m-d H:i:s] ") . "File Item " . $file_id);
4241  $file_dir = $target_dir . '/objects/il_' . IL_INST_ID . '_file_' . $file_id;
4242  ilFileUtils::makeDir($file_dir);
4243  $file_obj = new ilObjFile((int) $file_id, false);
4244  $source_file = $file_obj->getFile($file_obj->getVersion());
4245  if (!is_file($source_file)) {
4246  $source_file = $file_obj->getFile();
4247  }
4248  if (is_file($source_file)) {
4249  copy($source_file, $file_dir . '/' . $file_obj->getFileName());
4250  }
4251  unset($file_obj);
4252  }
4253  }
4254 
4259  public function getImportMapping(): array
4260  {
4261  return [];
4262  }
4263 
4267  public function checkMarks()
4268  {
4269  return $this->mark_schema->checkMarks();
4270  }
4271 
4275  public function getMarkSchema(): ASS_MarkSchema
4276  {
4277  return $this->mark_schema;
4278  }
4279 
4283  public function getMarkSchemaForeignId(): int
4284  {
4285  return $this->getTestId();
4286  }
4287 
4290  public function onMarkSchemaSaved()
4291  {
4292  $this->saveCompleteStatus($this->question_set_config_factory->getQuestionSetConfig());
4293 
4294  if ($this->participantDataExist()) {
4295  $this->recalculateScores(true);
4296  }
4297  }
4298 
4302  public function canEditMarks(): bool
4303  {
4304  $total = $this->evalTotalPersons();
4305  $results_summary_settings = $this->getScoreSettings()->getResultSummarySettings();
4306  if ($total === 0
4307  || $results_summary_settings->getScoreReportingEnabled() === false) {
4308  return true;
4309  }
4310 
4311  if ($results_summary_settings->getScoreReporting() === ilObjTestSettingsResultSummary::SCORE_REPORTING_DATE) {
4312  return $results_summary_settings->getReportingDate()
4313  >= new DateTimeImmutable('now', new DateTimeZone('UTC'));
4314  }
4315 
4316  return false;
4317  }
4318 
4328  public function saveAuthorToMetadata($author = "")
4329  {
4330  $md = new ilMD($this->getId(), 0, $this->getType());
4331  $md_life = $md->getLifecycle();
4332  if (!$md_life) {
4333  if (strlen($author) == 0) {
4334  $author = $this->user->getFullname();
4335  }
4336 
4337  $md_life = $md->addLifecycle();
4338  $md_life->save();
4339  $con = $md_life->addContribute();
4340  $con->setRole("Author");
4341  $con->save();
4342  $ent = $con->addEntity();
4343  $ent->setEntity($author);
4344  $ent->save();
4345  }
4346  }
4347 
4351  protected function doCreateMetaData(): void
4352  {
4353  $this->saveAuthorToMetadata();
4354  }
4355 
4363  public function getAuthor(): string
4364  {
4365  $author = [];
4366  $md = new ilMD($this->getId(), 0, $this->getType());
4367  $md_life = $md->getLifecycle();
4368  if ($md_life) {
4369  $ids = $md_life->getContributeIds();
4370  foreach ($ids as $id) {
4371  $md_cont = $md_life->getContribute($id);
4372  if (strcmp($md_cont->getRole(), "Author") == 0) {
4373  $entids = $md_cont->getEntityIds();
4374  foreach ($entids as $entid) {
4375  $md_ent = $md_cont->getEntity($entid);
4376  array_push($author, $md_ent->getEntity());
4377  }
4378  }
4379  }
4380  }
4381  return join(", ", $author);
4382  }
4383 
4391  public static function _lookupAuthor($obj_id): string
4392  {
4393  $author = [];
4394  $md = new ilMD($obj_id, 0, "tst");
4395  $md_life = $md->getLifecycle();
4396  if ($md_life) {
4397  $ids = $md_life->getContributeIds();
4398  foreach ($ids as $id) {
4399  $md_cont = $md_life->getContribute($id);
4400  if (strcmp($md_cont->getRole(), "Author") == 0) {
4401  $entids = $md_cont->getEntityIds();
4402  foreach ($entids as $entid) {
4403  $md_ent = $md_cont->getEntity($entid);
4404  array_push($author, $md_ent->getEntity());
4405  }
4406  }
4407  }
4408  }
4409  return join(",", $author);
4410  }
4411 
4418  public static function _getAvailableTests($use_object_id = false): array
4419  {
4420  global $DIC;
4421  $ilUser = $DIC['ilUser'];
4422 
4423  $result_array = [];
4424  $tests = array_slice(
4425  array_reverse(
4426  ilUtil::_getObjectsByOperations("tst", "write", $ilUser->getId(), PHP_INT_MAX)
4427  ),
4428  0,
4429  10000
4430  );
4431 
4432  if (count($tests)) {
4433  $titles = ilObject::_prepareCloneSelection($tests, "tst");
4434  foreach ($tests as $ref_id) {
4435  if ($use_object_id) {
4436  $obj_id = ilObject::_lookupObjId($ref_id);
4437  $result_array[$obj_id] = $titles[$ref_id];
4438  } else {
4439  $result_array[$ref_id] = $titles[$ref_id];
4440  }
4441  }
4442  }
4443  return $result_array;
4444  }
4445 
4454  public function cloneObject(int $target_id, int $copy_id = 0, bool $omit_tree = false): ?ilObject
4455  {
4456  $this->loadFromDb();
4457 
4458  $new_obj = parent::cloneObject($target_id, $copy_id, $omit_tree);
4459  $new_obj->setTmpCopyWizardCopyId($copy_id);
4460  $this->cloneMetaData($new_obj);
4461 
4462  $new_obj->mark_schema = clone $this->mark_schema;
4463  $new_obj->setTemplate($this->getTemplate());
4464  $new_obj->saveToDb();
4465  $new_obj->addToNewsOnOnline(false, $new_obj->getObjectProperties()->getPropertyIsOnline()->getIsOnline());
4466 
4467  $this->getMainSettingsRepository()->store(
4468  $this->getMainSettings()->withTestId($new_obj->getTestId())
4469  ->withIntroductionSettings(
4470  $this->getMainSettings()->getIntroductionSettings()->withIntroductionPageId(
4471  $this->cloneIntroduction()
4472  )->withTestId($new_obj->getTestId())
4473  )->withFinishingSettings(
4474  $this->getMainSettings()->getFinishingSettings()->withConcludingRemarksPageId(
4475  $this->cloneConcludingRemarks()
4476  )->withTestId($new_obj->getTestId())
4477  )
4478  );
4479  $this->getScoreSettingsRepository()->store(
4480  $this->getScoreSettings()->withTestId($new_obj->getTestId())
4481  );
4482 
4483  // clone certificate
4484  $pathFactory = new ilCertificatePathFactory();
4485  $templateRepository = new ilCertificateTemplateDatabaseRepository($this->db);
4486 
4487  $cloneAction = new ilCertificateCloneAction(
4488  $this->db,
4489  $pathFactory,
4490  $templateRepository,
4492  $this->filesystem_web,
4494  );
4495 
4496  $cloneAction->cloneCertificate($this, $new_obj);
4497 
4498  $this->question_set_config_factory->getQuestionSetConfig()->cloneQuestionSetRelatedData($new_obj);
4499  $new_obj->saveQuestionsToDb();
4500 
4501  $skillLevelThresholdList = new ilTestSkillLevelThresholdList($this->db);
4502  $skillLevelThresholdList->setTestId($this->getTestId());
4503  $skillLevelThresholdList->loadFromDb();
4504  $skillLevelThresholdList->cloneListForTest($new_obj->getTestId());
4505 
4506  $obj_settings = new ilLPObjSettings($this->getId());
4507  $obj_settings->cloneSettings($new_obj->getId());
4508 
4509  return $new_obj;
4510  }
4511 
4512  public function getQuestionCount(): int
4513  {
4514  $num = 0;
4515 
4516  if ($this->isRandomTest()) {
4517  $questionSetConfig = new ilTestRandomQuestionSetConfig(
4518  $this->tree,
4519  $this->db,
4520  $this->lng,
4521  $this->log,
4522  $this->component_repository,
4523  $this,
4524  $this->questioninfo
4525  );
4526 
4527  $questionSetConfig->loadFromDb();
4528 
4529  if ($questionSetConfig->isQuestionAmountConfigurationModePerPool()) {
4530  $sourcePoolDefinitionList = new ilTestRandomQuestionSetSourcePoolDefinitionList(
4531  $this->db,
4532  $this,
4534  );
4535 
4536  $sourcePoolDefinitionList->loadDefinitions();
4537 
4538  if (is_int($sourcePoolDefinitionList->getQuestionAmount())) {
4539  $num = $sourcePoolDefinitionList->getQuestionAmount();
4540  }
4541  } elseif (is_int($questionSetConfig->getQuestionAmountPerTest())) {
4542  $num = $questionSetConfig->getQuestionAmountPerTest();
4543  }
4544  } else {
4545  $this->loadQuestions();
4546  $num = count($this->questions);
4547  }
4548 
4549  return $num;
4550  }
4551 
4553  {
4554  if ($this->isRandomTest()) {
4555  return $this->getQuestionCount();
4556  }
4557  return count($this->questions);
4558  }
4559 
4567  public function logAction($logtext = "", $question_id = 0)
4568  {
4569  $original_id = 0;
4570  if ($question_id !== 0) {
4571  $original_id = $this->questioninfo->getOriginalId($question_id);
4572  }
4573  ilObjAssessmentFolder::_addLog($this->user->getId(), $this->getId(), $logtext, $question_id, $original_id, true, $this->getRefId());
4574  }
4575 
4583  public static function _getObjectIDFromTestID($test_id)
4584  {
4585  global $DIC;
4586  $ilDB = $DIC['ilDB'];
4587  $object_id = false;
4588  $result = $ilDB->queryF(
4589  "SELECT obj_fi FROM tst_tests WHERE test_id = %s",
4590  ['integer'],
4591  [$test_id]
4592  );
4593  if ($result->numRows()) {
4594  $row = $ilDB->fetchAssoc($result);
4595  $object_id = $row["obj_fi"];
4596  }
4597  return $object_id;
4598  }
4599 
4607  public static function _getObjectIDFromActiveID($active_id)
4608  {
4609  global $DIC;
4610  $ilDB = $DIC['ilDB'];
4611  $object_id = false;
4612  $result = $ilDB->queryF(
4613  "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",
4614  ['integer'],
4615  [$active_id]
4616  );
4617  if ($result->numRows()) {
4618  $row = $ilDB->fetchAssoc($result);
4619  $object_id = $row["obj_fi"];
4620  }
4621  return $object_id;
4622  }
4623 
4631  public static function _getTestIDFromObjectID($object_id)
4632  {
4633  global $DIC;
4634  $ilDB = $DIC['ilDB'];
4635  $test_id = false;
4636  $result = $ilDB->queryF(
4637  "SELECT test_id FROM tst_tests WHERE obj_fi = %s",
4638  ['integer'],
4639  [$object_id]
4640  );
4641  if ($result->numRows()) {
4642  $row = $ilDB->fetchAssoc($result);
4643  $test_id = $row["test_id"];
4644  }
4645  return $test_id;
4646  }
4647 
4656  public function getTextAnswer($active_id, $question_id, $pass = null): string
4657  {
4658  if (($active_id) && ($question_id)) {
4659  if ($pass === null) {
4660  $pass = assQuestion::_getSolutionMaxPass($question_id, $active_id);
4661  }
4662  if ($pass === null) {
4663  return '';
4664  }
4665  $query = $this->db->queryF(
4666  "SELECT value1 FROM tst_solutions WHERE active_fi = %s AND question_fi = %s AND pass = %s",
4667  ['integer', 'integer', 'integer'],
4668  [$active_id, $question_id, $pass]
4669  );
4670  $result = $this->db->fetchAll($query);
4671  if (count($result) == 1) {
4672  return $result[0]["value1"];
4673  }
4674  }
4675  return '';
4676  }
4677 
4685  public function getQuestiontext($question_id): string
4686  {
4687  $res = "";
4688  if ($question_id) {
4689  $result = $this->db->queryF(
4690  "SELECT question_text FROM qpl_questions WHERE question_id = %s",
4691  ['integer'],
4692  [$question_id]
4693  );
4694  if ($result->numRows() == 1) {
4695  $row = $this->db->fetchAssoc($result);
4696  $res = $row["question_text"];
4697  }
4698  }
4699  return $res;
4700  }
4701 
4703  {
4704  $participant_list = new ilTestParticipantList($this, $this->user, $this->lng, $this->db);
4705  $participant_list->initializeFromDbRows($this->getInvitedUsers());
4706  return $participant_list;
4707  }
4708 
4710  {
4711  $participant_list = new ilTestParticipantList($this, $this->user, $this->lng, $this->db);
4712  $participant_list->initializeFromDbRows($this->getTestParticipants());
4713 
4714  return $participant_list;
4715  }
4716 
4723  public function &getInvitedUsers(int $user_id = 0, $order = "login, lastname, firstname"): array
4724  {
4725  $result_array = [];
4726 
4727  if ($this->getAnonymity()) {
4728  if ($user_id !== 0) {
4729  $result = $this->db->queryF(
4730  "SELECT tst_active.active_id, tst_active.tries, usr_id, %s login, %s lastname, %s firstname, tst_invited_user.clientip, " .
4731  "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 " .
4732  "LEFT JOIN tst_active ON tst_active.user_fi = tst_invited_user.user_fi AND tst_active.test_fi = tst_invited_user.test_fi " .
4733  "WHERE tst_invited_user.test_fi = %s and tst_invited_user.user_fi=usr_data.usr_id AND usr_data.usr_id=%s " .
4734  "ORDER BY $order",
4735  ['text', 'text', 'text', 'integer', 'integer'],
4736  ['', $this->lng->txt('anonymous'), '', $this->getTestId(), $user_id]
4737  );
4738  } else {
4739  $result = $this->db->queryF(
4740  "SELECT tst_active.active_id, tst_active.tries, usr_id, %s login, %s lastname, %s firstname, tst_invited_user.clientip, " .
4741  "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 " .
4742  "LEFT JOIN tst_active ON tst_active.user_fi = tst_invited_user.user_fi AND tst_active.test_fi = tst_invited_user.test_fi " .
4743  "WHERE tst_invited_user.test_fi = %s and tst_invited_user.user_fi=usr_data.usr_id " .
4744  "ORDER BY $order",
4745  ['text', 'text', 'text', 'integer'],
4746  ['', $this->lng->txt('anonymous'), '', $this->getTestId()]
4747  );
4748  }
4749  } else {
4750  if ($user_id !== 0) {
4751  $result = $this->db->queryF(
4752  "SELECT tst_active.active_id, tst_active.tries, usr_id, login, lastname, firstname, tst_invited_user.clientip, " .
4753  "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 " .
4754  "LEFT JOIN tst_active ON tst_active.user_fi = tst_invited_user.user_fi AND tst_active.test_fi = tst_invited_user.test_fi " .
4755  "WHERE tst_invited_user.test_fi = %s and tst_invited_user.user_fi=usr_data.usr_id AND usr_data.usr_id=%s " .
4756  "ORDER BY $order",
4757  ['integer', 'integer'],
4758  [$this->getTestId(), $user_id]
4759  );
4760  } else {
4761  $result = $this->db->queryF(
4762  "SELECT tst_active.active_id, tst_active.tries, usr_id, login, lastname, firstname, tst_invited_user.clientip, " .
4763  "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 " .
4764  "LEFT JOIN tst_active ON tst_active.user_fi = tst_invited_user.user_fi AND tst_active.test_fi = tst_invited_user.test_fi " .
4765  "WHERE tst_invited_user.test_fi = %s and tst_invited_user.user_fi=usr_data.usr_id " .
4766  "ORDER BY $order",
4767  ['integer'],
4768  [$this->getTestId()]
4769  );
4770  }
4771  }
4772  $result_array = [];
4773  while ($row = $this->db->fetchAssoc($result)) {
4774  $result_array[$row['usr_id']] = $row;
4775  }
4776  return $result_array;
4777  }
4778 
4779  public function &getTestParticipants(): array
4780  {
4781  if ($this->getMainSettings()->getGeneralSettings()->getAnonymity()) {
4782  $query = "
4783  SELECT tst_active.active_id,
4784  tst_active.tries,
4785  tst_active.user_fi usr_id,
4786  %s login,
4787  %s lastname,
4788  %s firstname,
4789  tst_active.submitted test_finished,
4790  usr_data.matriculation,
4791  usr_data.active,
4792  tst_active.lastindex,
4793  COALESCE(tst_active.last_finished_pass, -1) <> tst_active.last_started_pass unfinished_passes
4794  FROM tst_active
4795  LEFT JOIN usr_data
4796  ON tst_active.user_fi = usr_data.usr_id
4797  WHERE tst_active.test_fi = %s
4798  ORDER BY usr_data.lastname
4799  ";
4800  $result = $this->db->queryF(
4801  $query,
4802  ['text', 'text', 'text', 'integer'],
4803  ['', $this->lng->txt("anonymous"), "", $this->getTestId()]
4804  );
4805  } else {
4806  $query = "
4807  SELECT tst_active.active_id,
4808  tst_active.tries,
4809  tst_active.user_fi usr_id,
4810  usr_data.login,
4811  usr_data.lastname,
4812  usr_data.firstname,
4813  tst_active.submitted test_finished,
4814  usr_data.matriculation,
4815  usr_data.active,
4816  tst_active.lastindex,
4817  COALESCE(tst_active.last_finished_pass, -1) <> tst_active.last_started_pass unfinished_passes
4818  FROM tst_active
4819  LEFT JOIN usr_data
4820  ON tst_active.user_fi = usr_data.usr_id
4821  WHERE tst_active.test_fi = %s
4822  ORDER BY usr_data.lastname
4823  ";
4824  $result = $this->db->queryF(
4825  $query,
4826  ['integer'],
4827  [$this->getTestId()]
4828  );
4829  }
4830  $data = [];
4831  while ($row = $this->db->fetchAssoc($result)) {
4832  $data[$row['active_id']] = $row;
4833  }
4834  foreach ($data as $index => $participant) {
4835  if (strlen(trim($participant["firstname"] . $participant["lastname"])) == 0) {
4836  $data[$index]["lastname"] = $this->lng->txt("deleted_user");
4837  }
4838  }
4839  return $data;
4840  }
4841 
4842  public function getTestParticipantsForManualScoring($filter = null): array
4843  {
4845  if (count($scoring) == 0) {
4846  return [];
4847  }
4848 
4849  $participants = &$this->getTestParticipants();
4850  $filtered_participants = [];
4851  foreach ($participants as $active_id => $participant) {
4852  if ($participant['tries'] > 0) {
4853  switch ($filter) {
4854  case 4:
4855  if ($this->testManScoringDoneHelper->isDone((int) $active_id)) {
4856  $filtered_participants[$active_id] = $participant;
4857  }
4858  break;
4859  case 5:
4860  if (!$this->testManScoringDoneHelper->isDone((int) $active_id)) {
4861  $filtered_participants[$active_id] = $participant;
4862  }
4863  break;
4864  default:
4865  $filtered_participants[$active_id] = $participant;
4866  }
4867  }
4868  }
4869  return $filtered_participants;
4870  }
4871 
4878  public function getUserData($ids): array
4879  {
4880  if (!is_array($ids) || count($ids) == 0) {
4881  return [];
4882  }
4883 
4884  if ($this->getAnonymity()) {
4885  $result = $this->db->queryF(
4886  "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",
4887  ['text', 'text', 'text'],
4888  ["", $this->lng->txt("anonymous"), ""]
4889  );
4890  } else {
4891  $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");
4892  }
4893 
4894  $result_array = [];
4895  while ($row = $this->db->fetchAssoc($result)) {
4896  $result_array[$row["usr_id"]] = $row;
4897  }
4898  return $result_array;
4899  }
4900 
4901  public function getGroupData($ids): array
4902  {
4903  if (!is_array($ids) || count($ids) == 0) {
4904  return [];
4905  }
4906  $result = [];
4907  foreach ($ids as $ref_id) {
4908  $obj_id = ilObject::_lookupObjId($ref_id);
4909  $result[$ref_id] = ["ref_id" => $ref_id, "title" => ilObject::_lookupTitle($obj_id), "description" => ilObject::_lookupDescription($obj_id)];
4910  }
4911  return $result;
4912  }
4913 
4914  public function getRoleData($ids): array
4915  {
4916  if (!is_array($ids) || count($ids) == 0) {
4917  return [];
4918  }
4919  $result = [];
4920  foreach ($ids as $obj_id) {
4921  $result[$obj_id] = ["obj_id" => $obj_id, "title" => ilObject::_lookupTitle($obj_id), "description" => ilObject::_lookupDescription($obj_id)];
4922  }
4923  return $result;
4924  }
4925 
4926 
4933  public function inviteGroup($group_id)
4934  {
4935  $group = new ilObjGroup($group_id);
4936  $members = $group->getGroupMemberIds();
4937  foreach ($members as $user_id) {
4938  $this->inviteUser($user_id, ilObjUser::_lookupClientIP($user_id));
4939  }
4940  }
4941 
4948  public function inviteRole($role_id)
4949  {
4950  $members = $this->rbac_review->assignedUsers($role_id);
4951  foreach ($members as $user_id) {
4952  $this->inviteUser($user_id, ilObjUser::_lookupClientIP($user_id));
4953  }
4954  }
4955 
4956 
4957 
4964  public function disinviteUser($user_id)
4965  {
4966  $affectedRows = $this->db->manipulateF(
4967  "DELETE FROM tst_invited_user WHERE test_fi = %s AND user_fi = %s",
4968  ['integer', 'integer'],
4969  [$this->getTestId(), $user_id]
4970  );
4971  }
4972 
4979  public function inviteUser($user_id, $client_ip = "")
4980  {
4981  $this->db->manipulateF(
4982  "DELETE FROM tst_invited_user WHERE test_fi = %s AND user_fi = %s",
4983  ['integer', 'integer'],
4984  [$this->getTestId(), $user_id]
4985  );
4986  $this->db->manipulateF(
4987  "INSERT INTO tst_invited_user (test_fi, user_fi, clientip, tstamp) VALUES (%s, %s, %s, %s)",
4988  ['integer', 'integer', 'text', 'integer'],
4989  [$this->getTestId(), $user_id, (strlen($client_ip)) ? $client_ip : null, time()]
4990  );
4991  }
4992 
4993 
4994  public function setClientIP($user_id, $client_ip)
4995  {
4996  $this->db->manipulateF(
4997  "UPDATE tst_invited_user SET clientip = %s, tstamp = %s WHERE test_fi=%s and user_fi=%s",
4998  ['text', 'integer', 'integer', 'integer'],
4999  [(strlen($client_ip)) ? $client_ip : null, time(), $this->getTestId(), $user_id]
5000  );
5001  }
5002 
5008  public static function _getSolvedQuestions($active_id, $question_fi = null): array
5009  {
5010  global $DIC;
5011  $ilDB = $DIC['ilDB'];
5012  if (is_numeric($question_fi)) {
5013  $result = $ilDB->queryF(
5014  "SELECT question_fi, solved FROM tst_qst_solved WHERE active_fi = %s AND question_fi=%s",
5015  ['integer', 'integer'],
5016  [$active_id, $question_fi]
5017  );
5018  } else {
5019  $result = $ilDB->queryF(
5020  "SELECT question_fi, solved FROM tst_qst_solved WHERE active_fi = %s",
5021  ['integer'],
5022  [$active_id]
5023  );
5024  }
5025  $result_array = [];
5026  while ($row = $ilDB->fetchAssoc($result)) {
5027  $result_array[$row["question_fi"]] = $row;
5028  }
5029  return $result_array;
5030  }
5031 
5032 
5036  public function setQuestionSetSolved($value, $question_id, $user_id)
5037  {
5038  $active_id = $this->getActiveIdOfUser($user_id);
5039  $this->db->manipulateF(
5040  "DELETE FROM tst_qst_solved WHERE active_fi = %s AND question_fi = %s",
5041  ['integer', 'integer'],
5042  [$active_id, $question_id]
5043  );
5044  $this->db->manipulateF(
5045  "INSERT INTO tst_qst_solved (solved, question_fi, active_fi) VALUES (%s, %s, %s)",
5046  ['integer', 'integer', 'integer'],
5047  [$value, $question_id, $active_id]
5048  );
5049  }
5050 
5054  public function isTestFinished($active_id): bool
5055  {
5056  $result = $this->db->queryF(
5057  "SELECT submitted FROM tst_active WHERE active_id=%s AND submitted=%s",
5058  ['integer', 'integer'],
5059  [$active_id, 1]
5060  );
5061  return $result->numRows() == 1;
5062  }
5063 
5067  public function isActiveTestSubmitted($user_id = null): bool
5068  {
5069  if (!is_numeric($user_id)) {
5070  $user_id = $this->user->getId();
5071  }
5072 
5073  $result = $this->db->queryF(
5074  "SELECT submitted FROM tst_active WHERE test_fi=%s AND user_fi=%s AND submitted=%s",
5075  ['integer', 'integer', 'integer'],
5076  [$this->getTestId(), $user_id, 1]
5077  );
5078  return $result->numRows() == 1;
5079  }
5080 
5084  public function hasNrOfTriesRestriction(): bool
5085  {
5086  return $this->getNrOfTries() != 0;
5087  }
5088 
5089 
5095  public function isNrOfTriesReached($tries): bool
5096  {
5097  return $tries >= $this->getNrOfTries();
5098  }
5099 
5100 
5109  public function getAllTestResults($participants, $prepareForCSV = true): array
5110  {
5111  $results = [];
5112  $row = [
5113  "user_id" => $this->lng->txt("user_id"),
5114  "matriculation" => $this->lng->txt("matriculation"),
5115  "lastname" => $this->lng->txt("lastname"),
5116  "firstname" => $this->lng->txt("firstname"),
5117  "login" => $this->lng->txt("login"),
5118  "reached_points" => $this->lng->txt("tst_reached_points"),
5119  "max_points" => $this->lng->txt("tst_maximum_points"),
5120  "percent_value" => $this->lng->txt("tst_percent_solved"),
5121  "mark" => $this->lng->txt("tst_mark"),
5122  "passed" => $this->lng->txt("tst_mark_passed"),
5123  ];
5124  $results[] = $row;
5125  if (count($participants)) {
5126  foreach ($participants as $active_id => $user_rec) {
5127  $mark = '';
5128  $row = [];
5129  $reached_points = 0;
5130  $max_points = 0;
5131  $pass = ilObjTest::_getResultPass($active_id);
5132  // abort if no valid pass can be found
5133  if (!is_int($pass)) {
5134  continue;
5135  }
5136  foreach ($this->questions as $value) {
5137  $question = ilObjTest::_instanciateQuestion($value);
5138  if (is_object($question)) {
5139  $max_points += $question->getMaximumPoints();
5140  $reached_points += $question->getReachedPoints($active_id, $pass);
5141  }
5142  }
5143  if ($max_points > 0) {
5144  $percentvalue = $reached_points / $max_points;
5145  if ($percentvalue < 0) {
5146  $percentvalue = 0.0;
5147  }
5148  } else {
5149  $percentvalue = 0;
5150  }
5151  $mark_obj = $this->mark_schema->getMatchingMark($percentvalue * 100);
5152  $passed = "";
5153  if ($mark_obj) {
5154  $mark = $mark_obj->getOfficialName();
5155  }
5156  if ($this->getAnonymity()) {
5157  $user_rec['firstname'] = "";
5158  $user_rec['lastname'] = $this->lng->txt("anonymous");
5159  }
5160  $row = [
5161  "user_id" => $user_rec['usr_id'],
5162  "matriculation" => $user_rec['matriculation'],
5163  "lastname" => $user_rec['lastname'],
5164  "firstname" => $user_rec['firstname'],
5165  "login" => $user_rec['login'],
5166  "reached_points" => $reached_points,
5167  "max_points" => $max_points,
5168  "percent_value" => $percentvalue,
5169  "mark" => $mark,
5170  "passed" => $user_rec['passed'] ? '1' : '0',
5171  ];
5172  $results[] = $prepareForCSV ? $this->processCSVRow($row, true) : $row;
5173  }
5174  }
5175  return $results;
5176  }
5177 
5188  public function processCSVRow(
5189  mixed $row,
5190  bool $quote_all = false,
5191  string $separator = ";"
5192  ): array {
5193  $resultarray = [];
5194  foreach ($row as $rowindex => $entry) {
5195  $surround = false;
5196  if ($quote_all) {
5197  $surround = true;
5198  }
5199  if (is_string($entry) && strpos($entry, "\"") !== false) {
5200  $entry = str_replace("\"", "\"\"", $entry);
5201  $surround = true;
5202  }
5203  if (is_string($entry) && strpos($entry, $separator) !== false) {
5204  $surround = true;
5205  }
5206 
5207  if (is_string($entry)) {
5208  // replace all CR LF with LF (for Excel for Windows compatibility
5209  $entry = str_replace(chr(13) . chr(10), chr(10), $entry);
5210  }
5211 
5212  if ($surround) {
5213  $entry = "\"" . $entry . "\"";
5214  }
5215 
5216  $resultarray[$rowindex] = $entry;
5217  }
5218  return $resultarray;
5219  }
5220 
5229  public static function _getPass($active_id): int
5230  {
5231  global $DIC;
5232  $ilDB = $DIC['ilDB'];
5233  $result = $ilDB->queryF(
5234  "SELECT tries FROM tst_active WHERE active_id = %s",
5235  ['integer'],
5236  [$active_id]
5237  );
5238  if ($result->numRows()) {
5239  $row = $ilDB->fetchAssoc($result);
5240  return $row["tries"];
5241  } else {
5242  return 0;
5243  }
5244  }
5245 
5255  public static function _getMaxPass($active_id): ?int
5256  {
5257  global $DIC;
5258  $ilDB = $DIC['ilDB'];
5259  $result = $ilDB->queryF(
5260  "SELECT MAX(pass) maxpass FROM tst_pass_result WHERE active_fi = %s",
5261  ['integer'],
5262  [$active_id]
5263  );
5264 
5265  if ($result->numRows()) {
5266  $row = $ilDB->fetchAssoc($result);
5267  return $row["maxpass"];
5268  }
5269 
5270  return null;
5271  }
5272 
5278  public static function _getBestPass($active_id): ?int
5279  {
5280  global $DIC;
5281  $ilDB = $DIC['ilDB'];
5282 
5283  $result = $ilDB->queryF(
5284  "SELECT * FROM tst_pass_result WHERE active_fi = %s",
5285  ['integer'],
5286  [$active_id]
5287  );
5288 
5289  if (!$result->numRows()) {
5290  return null;
5291  }
5292 
5293  $bestrow = null;
5294  $bestfactor = 0.0;
5295  while ($row = $ilDB->fetchAssoc($result)) {
5296  if ($row["maxpoints"] > 0.0) {
5297  $factor = (float) ($row["points"] / $row["maxpoints"]);
5298  } else {
5299  $factor = 0.0;
5300  }
5301  if ($factor === 0.0 && $bestfactor === 0.0
5302  || $factor > $bestfactor) {
5303  $bestrow = $row;
5304  $bestfactor = $factor;
5305  }
5306  }
5307 
5308  if (is_array($bestrow)) {
5309  return $bestrow["pass"];
5310  }
5311 
5312  return null;
5313  }
5314 
5323  public static function _getResultPass($active_id): ?int
5324  {
5325  $counted_pass = null;
5326  if (ilObjTest::_getPassScoring($active_id) == SCORE_BEST_PASS) {
5327  $counted_pass = ilObjTest::_getBestPass($active_id);
5328  } else {
5329  $counted_pass = ilObjTest::_getMaxPass($active_id);
5330  }
5331  return $counted_pass;
5332  }
5333 
5343  public function getAnsweredQuestionCount($active_id, $pass = null): int
5344  {
5345  if ($this->isRandomTest()) {
5346  $this->loadQuestions($active_id, $pass);
5347  }
5348  $workedthrough = 0;
5349  foreach ($this->questions as $value) {
5350  if ($this->questioninfo->lookupResultRecordExist($active_id, $value, $pass)) {
5351  $workedthrough += 1;
5352  }
5353  }
5354  return $workedthrough;
5355  }
5356 
5363  public static function lookupPassResultsUpdateTimestamp($active_id, $pass): int
5364  {
5365  global $DIC;
5366  $ilDB = $DIC['ilDB'];
5367 
5368  if (is_null($pass)) {
5369  $pass = 0;
5370  }
5371 
5372  $query = "
5373  SELECT tst_pass_result.tstamp pass_res_tstamp,
5374  tst_test_result.tstamp quest_res_tstamp
5375 
5376  FROM tst_pass_result
5377 
5378  LEFT JOIN tst_test_result
5379  ON tst_test_result.active_fi = tst_pass_result.active_fi
5380  AND tst_test_result.pass = tst_pass_result.pass
5381 
5382  WHERE tst_pass_result.active_fi = %s
5383  AND tst_pass_result.pass = %s
5384 
5385  ORDER BY tst_test_result.tstamp DESC
5386  ";
5387 
5388  $result = $ilDB->queryF(
5389  $query,
5390  ['integer', 'integer'],
5391  [$active_id, $pass]
5392  );
5393 
5394  while ($row = $ilDB->fetchAssoc($result)) {
5395  if ($row['quest_res_tstamp']) {
5396  return $row['quest_res_tstamp'];
5397  }
5398 
5399  return $row['pass_res_tstamp'];
5400  }
5401 
5402  return 0;
5403  }
5404 
5413  public function isExecutable($test_session, $user_id, $allow_pass_increase = false): array
5414  {
5415  $result = [
5416  "executable" => true,
5417  "errormessage" => ""
5418  ];
5419 
5420  if (!$this->getObjectProperties()->getPropertyIsOnline()->getIsOnline()) {
5421  $result["executable"] = false;
5422  $result["errormessage"] = $this->lng->txt('autosave_failed') . ': ' . $this->lng->txt('offline');
5423  return $result;
5424  }
5425 
5426  if (!$this->startingTimeReached()) {
5427  $result["executable"] = false;
5428  $result["errormessage"] = sprintf($this->lng->txt("detail_starting_time_not_reached"), ilDatePresentation::formatDate(new ilDateTime($this->getStartingTime(), IL_CAL_UNIX)));
5429  return $result;
5430  }
5431  if ($this->endingTimeReached()) {
5432  $result["executable"] = false;
5433  $result["errormessage"] = sprintf($this->lng->txt("detail_ending_time_reached"), ilDatePresentation::formatDate(new ilDateTime($this->getEndingTime(), IL_CAL_UNIX)));
5434  return $result;
5435  }
5436 
5437  $active_id = $this->getActiveIdOfUser($user_id);
5438 
5439  if ($this->getEnableProcessingTime()
5440  && $active_id > 0
5441  && ($starting_time = $this->getStartingTimeOfUser($active_id)) !== false
5442  && $this->isMaxProcessingTimeReached($starting_time, $active_id)) {
5443  $result["executable"] = false;
5444  $result["errormessage"] = $this->lng->txt("detail_max_processing_time_reached");
5445  return $result;
5446  }
5447 
5448  $testPassesSelector = new ilTestPassesSelector($this->db, $this);
5449  $testPassesSelector->setActiveId($active_id);
5450  $testPassesSelector->setLastFinishedPass($test_session->getLastFinishedPass());
5451 
5452  if ($this->hasNrOfTriesRestriction() && ($active_id > 0)) {
5453  $closedPasses = $testPassesSelector->getClosedPasses();
5454 
5455  if (count($closedPasses) >= $this->getNrOfTries()) {
5456  $result["executable"] = false;
5457  $result["errormessage"] = $this->lng->txt("maximum_nr_of_tries_reached");
5458  return $result;
5459  }
5460 
5461  if ($this->isBlockPassesAfterPassedEnabled() && !$testPassesSelector->openPassExists()) {
5462  if (ilObjTestAccess::_isPassed($user_id, $this->getId())) {
5463  $result['executable'] = false;
5464  $result['errormessage'] = $this->lng->txt("tst_addit_passes_blocked_after_passed_msg");
5465  return $result;
5466  }
5467  }
5468  }
5469 
5470  $next_pass_allowed_timestamp = 0;
5471  if (!$this->isNextPassAllowed($testPassesSelector, $next_pass_allowed_timestamp)) {
5472  $date = ilDatePresentation::formatDate(new ilDateTime($next_pass_allowed_timestamp, IL_CAL_UNIX));
5473 
5474  $result['executable'] = false;
5475  $result['errormessage'] = sprintf($this->lng->txt('wait_for_next_pass_hint_msg'), $date);
5476  return $result;
5477  }
5478  return $result;
5479  }
5480 
5481  public function isNextPassAllowed(ilTestPassesSelector $testPassesSelector, int &$next_pass_allowed_timestamp): bool
5482  {
5483  $waiting_between_passes = $this->getMainSettings()->getTestBehaviourSettings()->getPassWaiting();
5484  $last_finished_pass_timestamp = $testPassesSelector->getLastFinishedPassTimestamp();
5485 
5486  if (
5487  $this->getMainSettings()->getTestBehaviourSettings()->getPassWaitingEnabled()
5488  && ($waiting_between_passes !== '')
5489  && ($testPassesSelector->getLastFinishedPass() !== null)
5490  && ($last_finished_pass_timestamp !== null)
5491  ) {
5492  $time_values = explode(':', $waiting_between_passes);
5493  $next_pass_allowed_timestamp = strtotime('+ ' . $time_values[0] . ' Days + ' . $time_values[1] . ' Hours' . $time_values[2] . ' Minutes', $last_finished_pass_timestamp);
5494  return (time() > $next_pass_allowed_timestamp);
5495  }
5496 
5497  return true;
5498  }
5499 
5500  public function canShowTestResults(ilTestSession $test_session): bool
5501  {
5502  $passSelector = new ilTestPassesSelector($this->db, $this);
5503 
5504  $passSelector->setActiveId($test_session->getActiveId());
5505  $passSelector->setLastFinishedPass($test_session->getLastFinishedPass());
5506 
5507  return $passSelector->hasReportablePasses();
5508  }
5509 
5510  public function hasAnyTestResult(ilTestSession $test_session): bool
5511  {
5512  $passSelector = new ilTestPassesSelector($this->db, $this);
5513 
5514  $passSelector->setActiveId($test_session->getActiveId());
5515  $passSelector->setLastFinishedPass($test_session->getLastFinishedPass());
5516 
5517  return $passSelector->hasExistingPasses();
5518  }
5519 
5527  public function getStartingTimeOfUser($active_id, $pass = null)
5528  {
5529  if ($active_id < 1) {
5530  return false;
5531  }
5532  if ($pass === null) {
5533  $pass = ($this->getResetProcessingTime()) ? self::_getPass($active_id) : 0;
5534  }
5535  $result = $this->db->queryF(
5536  "SELECT tst_times.started FROM tst_times WHERE tst_times.active_fi = %s AND tst_times.pass = %s ORDER BY tst_times.started",
5537  ['integer', 'integer'],
5538  [$active_id, $pass]
5539  );
5540  if ($result->numRows()) {
5541  $row = $this->db->fetchAssoc($result);
5542  if (preg_match("/(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/", $row["started"], $matches)) {
5543  return mktime(
5544  (int) $matches[4],
5545  (int) $matches[5],
5546  (int) $matches[6],
5547  (int) $matches[2],
5548  (int) $matches[3],
5549  (int) $matches[1]
5550  );
5551  } else {
5552  return time();
5553  }
5554  } else {
5555  return time();
5556  }
5557  }
5558 
5565  public function isMaxProcessingTimeReached(int $starting_time, int $active_id): bool
5566  {
5567  if (!$this->getEnableProcessingTime()) {
5568  return false;
5569  }
5570 
5571  $processing_time = $this->getProcessingTimeInSeconds($active_id);
5572  $now = time();
5573  if ($now > ($starting_time + $processing_time)) {
5574  return true;
5575  }
5576 
5577  return false;
5578  }
5579 
5580  public function &getTestQuestions(): array
5581  {
5582  $tags_trafo = $this->refinery->string()->stripTags();
5583 
5584  $query = "
5585  SELECT questions.*,
5586  questtypes.type_tag,
5587  tstquest.sequence,
5588  tstquest.obligatory,
5589  origquest.obj_fi orig_obj_fi
5590 
5591  FROM qpl_questions questions
5592 
5593  INNER JOIN qpl_qst_type questtypes
5594  ON questtypes.question_type_id = questions.question_type_fi
5595 
5596  INNER JOIN tst_test_question tstquest
5597  ON tstquest.question_fi = questions.question_id
5598 
5599  LEFT JOIN qpl_questions origquest
5600  ON origquest.question_id = questions.original_id
5601 
5602  WHERE tstquest.test_fi = %s
5603 
5604  ORDER BY tstquest.sequence
5605  ";
5606 
5607  $query_result = $this->db->queryF(
5608  $query,
5609  ['integer'],
5610  [$this->getTestId()]
5611  );
5612 
5613  $questions = [];
5614 
5615  while ($row = $this->db->fetchAssoc($query_result)) {
5616  $row['title'] = $tags_trafo->transform($row['title']);
5617  $row['description'] = $tags_trafo->transform($row['description'] !== '' && $row['description'] !== null ? $row['description'] : '&nbsp;');
5618  $row['author'] = $tags_trafo->transform($row['author']);
5619  $row['obligationPossible'] = self::isQuestionObligationPossible($row['question_id']);
5620 
5621  $questions[] = $row;
5622  }
5623 
5624  return $questions;
5625  }
5626 
5631  public function isTestQuestion($questionId): bool
5632  {
5633  foreach ($this->getTestQuestions() as $questionData) {
5634  if ($questionData['question_id'] != $questionId) {
5635  continue;
5636  }
5637 
5638  return true;
5639  }
5640 
5641  return false;
5642  }
5643 
5644  public function checkQuestionParent($questionId): bool
5645  {
5646  $row = $this->db->fetchAssoc($this->db->queryF(
5647  "SELECT COUNT(question_id) cnt FROM qpl_questions WHERE question_id = %s AND obj_fi = %s",
5648  ['integer', 'integer'],
5649  [$questionId, $this->getId()]
5650  ));
5651 
5652  return (bool) $row['cnt'];
5653  }
5654 
5655  public function getFixedQuestionSetTotalPoints(): float
5656  {
5657  $points = 0;
5658 
5659  foreach ($this->getTestQuestions() as $question_data) {
5660  $points += $question_data['points'];
5661  }
5662 
5663  return $points;
5664  }
5665 
5669  public function getPotentialRandomTestQuestions(): array
5670  {
5671  $query = "
5672  SELECT questions.*,
5673  questtypes.type_tag,
5674  origquest.obj_fi orig_obj_fi
5675 
5676  FROM qpl_questions questions
5677 
5678  INNER JOIN qpl_qst_type questtypes
5679  ON questtypes.question_type_id = questions.question_type_fi
5680 
5681  INNER JOIN tst_rnd_cpy tstquest
5682  ON tstquest.qst_fi = questions.question_id
5683 
5684  LEFT JOIN qpl_questions origquest
5685  ON origquest.question_id = questions.original_id
5686 
5687  WHERE tstquest.tst_fi = %s
5688  ";
5689 
5690  $query_result = $this->db->queryF(
5691  $query,
5692  ['integer'],
5693  [$this->getTestId()]
5694  );
5695 
5696  $questions = [];
5697 
5698  while ($row = $this->db->fetchAssoc($query_result)) {
5699  $question = $row;
5700 
5701  $question['obligationPossible'] = self::isQuestionObligationPossible($row['question_id']);
5702 
5703  $questions[] = $question;
5704  }
5705 
5706  return $questions;
5707  }
5708 
5709  public function getShuffleQuestions(): bool
5710  {
5711  return $this->getMainSettings()->getQuestionBehaviourSettings()->getShuffleQuestions();
5712  }
5713 
5725  public function getListOfQuestionsSettings()
5726  {
5727  return $this->getMainSettings()->getParticipantFunctionalitySettings()->getUsrPassOverviewMode();
5728  }
5729 
5730  public function getListOfQuestions(): bool
5731  {
5732  return $this->getMainSettings()->getParticipantFunctionalitySettings()->getQuestionListEnabled();
5733  }
5734 
5735  public function getUsrPassOverviewEnabled(): bool
5736  {
5737  return $this->getMainSettings()->getParticipantFunctionalitySettings()->getUsrPassOverviewEnabled();
5738  }
5739 
5740  public function getListOfQuestionsStart(): bool
5741  {
5742  return $this->getMainSettings()->getParticipantFunctionalitySettings()->getShownQuestionListAtBeginning();
5743  }
5744 
5745  public function getListOfQuestionsEnd(): bool
5746  {
5747  return $this->getMainSettings()->getParticipantFunctionalitySettings()->getShownQuestionListAtEnd();
5748  }
5749 
5750  public function getListOfQuestionsDescription(): bool
5751  {
5752  return $this->getMainSettings()->getParticipantFunctionalitySettings()->getShowDescriptionInQuestionList();
5753  }
5754 
5758  public function getShowPassDetails(): bool
5759  {
5760  return $this->getScoreSettings()->getResultDetailsSettings()->getShowPassDetails();
5761  }
5762 
5766  public function getShowSolutionPrintview(): bool
5767  {
5768  return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionPrintview();
5769  }
5773  public function canShowSolutionPrintview($user_id = null): bool
5774  {
5775  return $this->getShowSolutionPrintview();
5776  }
5777 
5781  public function getShowSolutionFeedback(): bool
5782  {
5783  return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionFeedback();
5784  }
5785 
5789  public function getShowSolutionAnswersOnly(): bool
5790  {
5791  return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionAnswersOnly();
5792  }
5793 
5797  public function getShowSolutionSignature(): bool
5798  {
5799  return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionSignature();
5800  }
5801 
5805  public function getShowSolutionSuggested(): bool
5806  {
5807  return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionSuggested();
5808  }
5809 
5814  public function getShowSolutionListComparison(): bool
5815  {
5816  return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionListComparison();
5817  }
5818 
5819  public function getShowSolutionListOwnAnswers(): bool
5820  {
5821  return $this->getScoreSettings()->getResultDetailsSettings()->getShowSolutionListOwnAnswers();
5822  }
5823 
5827  public static function _getUserIdFromActiveId(int $active_id): int
5828  {
5829  global $DIC;
5830  $ilDB = $DIC['ilDB'];
5831  $result = $ilDB->queryF(
5832  "SELECT user_fi FROM tst_active WHERE active_id = %s",
5833  ['integer'],
5834  [$active_id]
5835  );
5836  if ($result->numRows()) {
5837  $row = $ilDB->fetchAssoc($result);
5838  return $row["user_fi"];
5839  } else {
5840  return -1;
5841  }
5842  }
5843 
5844  public function _getLastAccess(int $active_id): string
5845  {
5846  $result = $this->db->queryF(
5847  "SELECT finished FROM tst_times WHERE active_fi = %s ORDER BY finished DESC",
5848  ['integer'],
5849  [$active_id]
5850  );
5851  if ($result->numRows()) {
5852  $row = $this->db->fetchAssoc($result);
5853  return $row["finished"];
5854  }
5855  return "";
5856  }
5857 
5858  public static function lookupLastTestPassAccess(int $active_id, int $pass_index): ?int
5859  {
5861  global $DIC;
5862  $ilDB = $DIC['ilDB'];
5863 
5864  $query = "
5865  SELECT MAX(tst_times.tstamp) as last_pass_access
5866  FROM tst_times
5867  WHERE active_fi = %s
5868  AND pass = %s
5869  ";
5870 
5871  $res = $ilDB->queryF(
5872  $query,
5873  ['integer', 'integer'],
5874  [$active_id, $pass_index]
5875  );
5876 
5877  while ($row = $ilDB->fetchAssoc($res)) {
5878  return $row['last_pass_access'];
5879  }
5880 
5881  return null;
5882  }
5883 
5891  public function isHTML($a_text): bool
5892  {
5893  if (preg_match("/<[^>]*?>/", $a_text)) {
5894  return true;
5895  } else {
5896  return false;
5897  }
5898  }
5899 
5906  public function qtiMaterialToArray($a_material): array
5907  {
5908  $result = '';
5909  $mobs = [];
5910  for ($i = 0; $i < $a_material->getMaterialCount(); $i++) {
5911  $material = $a_material->getMaterial($i);
5912  if ($material['type'] === 'mattext') {
5913  $result .= $material['material']->getContent();
5914  }
5915  if ($material['type'] === 'matimage') {
5916  $matimage = $material['material'];
5917  if (preg_match('/(il_([0-9]+)_mob_([0-9]+))/', $matimage->getLabel(), $matches)) {
5918  $mobs[] = [
5919  'mob' => $matimage->getLabel(),
5920  'uri' => $matimage->getUri()
5921  ];
5922  }
5923  }
5924  }
5925 
5926  $decoded_result = base64_decode($result);
5927  if (str_starts_with($decoded_result, '<PageObject>')) {
5928  $result = $decoded_result;
5929  }
5930 
5931  $this->log->write(print_r(ilSession::get('import_mob_xhtml'), true));
5932  return [
5933  'text' => $result,
5934  'mobs' => $mobs
5935  ];
5936  }
5937 
5938  public function addQTIMaterial(ilXmlWriter &$xml_writer, ?int $page_id, string $material = ''): void
5939  {
5940  $xml_writer->xmlStartTag('material');
5941  $attrs = [
5942  'texttype' => 'text/plain'
5943  ];
5944  $file_ids = [];
5945  $mobs = [];
5946  if ($page_id !== null) {
5947  $attrs['texttype'] = 'text/xml';
5948  $mobs = ilObjMediaObject::_getMobsOfObject('tst:pg', $page_id);
5949  $page_object = new ilTestPage($page_id);
5950  $page_object->buildDom();
5951  $page_object->insertInstIntoIDs((string) IL_INST_ID);
5952  $material = base64_encode($page_object->getXMLFromDom());
5953  $file_ids = ilPCFileList::collectFileItems($page_object, $page_object->getDomDoc());
5954  foreach ($file_ids as $file_id) {
5955  $this->file_ids[] = (int) $file_id;
5956  };
5957  $mob_string = 'il_' . IL_INST_ID . '_mob_';
5958  } elseif ($this->isHTML($material)) {
5959  $attrs['texttype'] = 'text/xhtml';
5960  $mobs = ilObjMediaObject::_getMobsOfObject('tst:html', $this->getId());
5961  $mob_string = 'mm_';
5962  }
5963 
5964  $xml_writer->xmlElement('mattext', $attrs, $material);
5965  foreach ($mobs as $mob) {
5966  $mob_id_string = (string) $mob;
5967  $moblabel = 'il_' . IL_INST_ID . '_mob_' . $mob_id_string;
5968  if (strpos($material, $mob_string . $mob_id_string) !== false) {
5969  if (ilObjMediaObject::_exists($mob)) {
5970  $mob_obj = new ilObjMediaObject($mob);
5971  $imgattrs = [
5972  'label' => $moblabel,
5973  'uri' => 'objects/' . 'il_' . IL_INST_ID . '_mob_' . $mob_id_string . '/' . $mob_obj->getTitle()
5974  ];
5975  }
5976  $xml_writer->xmlElement('matimage', $imgattrs, null);
5977  }
5978  }
5979  $xml_writer->xmlEndTag('material');
5980  }
5981 
5988  public function prepareTextareaOutput($txt_output, $prepare_for_latex_output = false, $omitNl2BrWhenTextArea = false)
5989  {
5990  if ($txt_output == null) {
5991  $txt_output = '';
5992  }
5994  $txt_output,
5995  $prepare_for_latex_output,
5996  $omitNl2BrWhenTextArea
5997  );
5998  }
5999 
6000  public function getAnonymity(): bool
6001  {
6002  return $this->getMainSettings()->getGeneralSettings()->getAnonymity();
6003  }
6004 
6005 
6006  public static function _lookupAnonymity($a_obj_id): int
6007  {
6008  global $DIC;
6009  $ilDB = $DIC['ilDB'];
6010 
6011  $result = $ilDB->queryF(
6012  "SELECT anonymity FROM tst_tests WHERE obj_fi = %s",
6013  ['integer'],
6014  [$a_obj_id]
6015  );
6016  while ($row = $ilDB->fetchAssoc($result)) {
6017  return (int) $row['anonymity'];
6018  }
6019  return 0;
6020  }
6021 
6022  public function getShowCancel(): bool
6023  {
6024  return $this->getMainSettings()->getParticipantFunctionalitySettings()->getSuspendTestAllowed();
6025  }
6026 
6027  public function getShowMarker(): bool
6028  {
6029  return $this->getMainSettings()->getParticipantFunctionalitySettings()->getQuestionMarkingEnabled();
6030  }
6031 
6032  public function getFixedParticipants(): bool
6033  {
6034  return $this->getMainSettings()->getAccessSettings()->getFixedParticipants();
6035  }
6036 
6043  public static function lookupQuestionSetTypeByActiveId($active_id): ?string
6044  {
6045  global $DIC;
6046  $ilDB = $DIC['ilDB'];
6047 
6048  $query = "
6049  SELECT tst_tests.question_set_type
6050  FROM tst_active
6051  INNER JOIN tst_tests
6052  ON tst_active.test_fi = tst_tests.test_id
6053  WHERE tst_active.active_id = %s
6054  ";
6055 
6056  $res = $ilDB->queryF($query, ['integer'], [$active_id]);
6057 
6058  while ($row = $ilDB->fetchAssoc($res)) {
6059  return $row['question_set_type'];
6060  }
6061 
6062  return null;
6063  }
6064 
6075  public function userLookupFullName($user_id, $overwrite_anonymity = false, $sorted_order = false, $suffix = ""): string
6076  {
6077  if ($this->getAnonymity() && !$overwrite_anonymity) {
6078  return $this->lng->txt("anonymous") . $suffix;
6079  } else {
6080  $uname = ilObjUser::_lookupName($user_id);
6081  if (strlen($uname["firstname"] . $uname["lastname"]) == 0) {
6082  $uname["firstname"] = $this->lng->txt("deleted_user");
6083  }
6084  if ($sorted_order) {
6085  return trim($uname["lastname"] . ", " . $uname["firstname"]) . $suffix;
6086  } else {
6087  return trim($uname["firstname"] . " " . $uname["lastname"]) . $suffix;
6088  }
6089  }
6090  }
6091 
6097  public function getAvailableDefaults(): array
6098  {
6099  $result = $this->db->queryF(
6100  "SELECT * FROM tst_test_defaults WHERE user_fi = %s ORDER BY name ASC",
6101  ['integer'],
6102  [$this->user->getId()]
6103  );
6104  $defaults = [];
6105  while ($row = $this->db->fetchAssoc($result)) {
6106  $defaults[$row["test_defaults_id"]] = $row;
6107  }
6108  return $defaults;
6109  }
6110 
6111  public function getTestDefaults($test_defaults_id): ?array
6112  {
6113  $result = $this->db->queryF(
6114  "SELECT * FROM tst_test_defaults WHERE test_defaults_id = %s",
6115  ['integer'],
6116  [$test_defaults_id]
6117  );
6118  if ($result->numRows() == 1) {
6119  $row = $this->db->fetchAssoc($result);
6120  return $row;
6121  } else {
6122  return null;
6123  }
6124  }
6125 
6126  public function deleteDefaults($test_default_id)
6127  {
6128  $this->db->manipulateF(
6129  "DELETE FROM tst_test_defaults WHERE test_defaults_id = %s",
6130  ['integer'],
6131  [$test_default_id]
6132  );
6133  }
6134 
6141  public function addDefaults($a_name)
6142  {
6143  $main_settings = $this->getMainSettings();
6144  $score_settings = $this->getScoreSettings();
6145  $testsettings = [
6146  'questionSetType' => $main_settings->getGeneralSettings()->getQuestionSetType(),
6147  'Anonymity' => (int) $main_settings->getGeneralSettings()->getAnonymity(),
6148 
6149  'activation_limited' => $this->isActivationLimited(),
6150  'activation_start_time' => $this->getActivationStartingTime(),
6151  'activation_end_time' => $this->getActivationEndingTime(),
6152  'activation_visibility' => $this->getActivationVisibility(),
6153 
6154  'IntroEnabled' => (int) $main_settings->getIntroductionSettings()->getIntroductionEnabled(),
6155  'ExamConditionsCheckboxEnabled' => (int) $main_settings->getIntroductionSettings()->getExamConditionsCheckboxEnabled(),
6156 
6157  'StartingTimeEnabled' => (int) $main_settings->getAccessSettings()->getStartTimeEnabled(),
6158  'StartingTime' => $main_settings->getAccessSettings()->getStartTime(),
6159  'EndingTimeEnabled' => (int) $main_settings->getAccessSettings()->getEndTimeEnabled(),
6160  'EndingTime' => $main_settings->getAccessSettings()->getEndTime(),
6161  'password_enabled' => (int) $main_settings->getAccessSettings()->getPasswordEnabled(),
6162  'password' => $main_settings->getAccessSettings()->getPassword(),
6163  'fixed_participants' => (int) $main_settings->getAccessSettings()->getFixedParticipants(),
6164 
6165  'NrOfTries' => $main_settings->getTestBehaviourSettings()->getNumberOfTries(),
6166  'BlockAfterPassed' => (int) $main_settings->getTestBehaviourSettings()->getBlockAfterPassedEnabled(),
6167  'pass_waiting' => $main_settings->getTestBehaviourSettings()->getPassWaiting(),
6168  'EnableProcessingTime' => (int) $main_settings->getTestBehaviourSettings()->getProcessingTimeEnabled(),
6169  'ProcessingTime' => $main_settings->getTestBehaviourSettings()->getProcessingTime(),
6170  'ResetProcessingTime' => $main_settings->getTestBehaviourSettings()->getResetProcessingTime(),
6171  'Kiosk' => $main_settings->getTestBehaviourSettings()->getKioskMode(),
6172  'examid_in_test_pass' => (int) $main_settings->getTestBehaviourSettings()->getExamIdInTestPassEnabled(),
6173 
6174  'TitleOutput' => $main_settings->getQuestionBehaviourSettings()->getQuestionTitleOutputMode(),
6175  'autosave' => (int) $main_settings->getQuestionBehaviourSettings()->getAutosaveEnabled(),
6176  'autosave_ival' => $main_settings->getQuestionBehaviourSettings()->getAutosaveInterval(),
6177  'Shuffle' => (int) $main_settings->getQuestionBehaviourSettings()->getShuffleQuestions(),
6178  'offer_question_hints' => (int) $main_settings->getQuestionBehaviourSettings()->getQuestionHintsEnabled(),
6179  'AnswerFeedbackPoints' => (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackPointsEnabled(),
6180  'AnswerFeedback' => (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackGenericEnabled(),
6181  'SpecificAnswerFeedback' => (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackSpecificEnabled(),
6182  'InstantFeedbackSolution' => (int) $main_settings->getQuestionBehaviourSettings()->getInstantFeedbackSolutionEnabled(),
6183  'force_inst_fb' => (int) $main_settings->getQuestionBehaviourSettings()->getForceInstantFeedbackOnNextQuestion(),
6184  'follow_qst_answer_fixation' => (int) $main_settings->getQuestionBehaviourSettings()->getLockAnswerOnNextQuestionEnabled(),
6185  'inst_fb_answer_fixation' => (int) $main_settings->getQuestionBehaviourSettings()->getLockAnswerOnInstantFeedbackEnabled(),
6186  'obligations_enabled' => (int) $main_settings->getQuestionBehaviourSettings()->getCompulsoryQuestionsEnabled(),
6187 
6188  'use_previous_answers' => (int) $main_settings->getParticipantFunctionalitySettings()->getUsePreviousAnswerAllowed(),
6189  'ShowCancel' => (int) $main_settings->getParticipantFunctionalitySettings()->getSuspendTestAllowed(),
6190  'SequenceSettings' => (int) $main_settings->getParticipantFunctionalitySettings()->getPostponedQuestionsMoveToEnd(),
6191  'ListOfQuestionsSettings' => $main_settings->getParticipantFunctionalitySettings()->getUsrPassOverviewMode(),
6192  'ShowMarker' => (int) $main_settings->getParticipantFunctionalitySettings()->getQuestionMarkingEnabled(),
6193 
6194  'enable_examview' => $main_settings->getFinishingSettings()->getShowAnswerOverview(),
6195  'ShowFinalStatement' => (int) $main_settings->getFinishingSettings()->getConcludingRemarksEnabled(),
6196  'redirection_mode' => $main_settings->getFinishingSettings()->getRedirectionMode(),
6197  'redirection_url' => $main_settings->getFinishingSettings()->getRedirectionUrl(),
6198  'mailnotification' => $main_settings->getFinishingSettings()->getMailNotificationContentType(),
6199  'mailnottype' => (int) $main_settings->getFinishingSettings()->getAlwaysSendMailNotification(),
6200 
6201  'skill_service' => (int) $main_settings->getAdditionalSettings()->getSkillsServiceEnabled(),
6202 
6203  'PassScoring' => $score_settings->getScoringSettings()->getPassScoring(),
6204  'ScoreCutting' => $score_settings->getScoringSettings()->getScoreCutting(),
6205  'CountSystem' => $score_settings->getScoringSettings()->getCountSystem(),
6206 
6207  'ScoreReporting' => $score_settings->getResultSummarySettings()->getScoreReporting(),
6208  'ReportingDate' => $score_settings->getResultSummarySettings()->getReportingDate(),
6209  'pass_deletion_allowed' => (int) $score_settings->getResultSummarySettings()->getPassDeletionAllowed(),
6210  'show_grading_status' => (int) $score_settings->getResultSummarySettings()->getShowGradingStatusEnabled(),
6211  'show_grading_mark' => (int) $score_settings->getResultSummarySettings()->getShowGradingMarkEnabled(),
6212 
6213  'ResultsPresentation' => $score_settings->getResultDetailsSettings()->getResultsPresentation(),
6214  'show_solution_list_comparison' => (int) $score_settings->getResultDetailsSettings()->getShowSolutionListComparison(),
6215  'examid_in_test_res' => (int) $score_settings->getResultDetailsSettings()->getShowExamIdInTestResults(),
6216 
6217  'highscore_enabled' => (int) $score_settings->getGamificationSettings()->getHighscoreEnabled(),
6218  'highscore_anon' => (int) $score_settings->getGamificationSettings()->getHighscoreAnon(),
6219  'highscore_achieved_ts' => $score_settings->getGamificationSettings()->getHighscoreAchievedTS(),
6220  'highscore_score' => $score_settings->getGamificationSettings()->getHighscoreScore(),
6221  'highscore_percentage' => $score_settings->getGamificationSettings()->getHighscorePercentage(),
6222  'highscore_hints' => $score_settings->getGamificationSettings()->getHighscoreHints(),
6223  'highscore_wtime' => $score_settings->getGamificationSettings()->getHighscoreWTime(),
6224  'highscore_own_table' => $score_settings->getGamificationSettings()->getHighscoreOwnTable(),
6225  'highscore_top_table' => $score_settings->getGamificationSettings()->getHighscoreTopTable(),
6226  'highscore_top_num' => $score_settings->getGamificationSettings()->getHighscoreTopNum(),
6227 
6228  'HideInfoTab' => (int) $main_settings->getAdditionalSettings()->getHideInfoTab(),
6229  ];
6230 
6231  $next_id = $this->db->nextId('tst_test_defaults');
6232  $this->db->insert(
6233  'tst_test_defaults',
6234  [
6235  'test_defaults_id' => ['integer', $next_id],
6236  'name' => ['text', $a_name],
6237  'user_fi' => ['integer', $this->user->getId()],
6238  'defaults' => ['clob', serialize($testsettings)],
6239  'marks' => ['clob', serialize($this->mark_schema->getMarkSteps())],
6240  'tstamp' => ['integer', time()]
6241  ]
6242  );
6243  }
6244 
6252  public function applyDefaults($test_defaults): bool
6253  {
6254  $testsettings = unserialize($test_defaults['defaults']);
6255  $activation_starting_time = is_numeric($testsettings['activation_starting_time'] ?? false)
6256  ? (int) $testsettings['activation_starting_time']
6257  : null;
6258  $activation_ending_time = is_numeric($testsettings['activation_ending_time'] ?? false)
6259  ? (int) $testsettings['activation_ending_time']
6260  : null;
6261  $unserialized_marks = unserialize($test_defaults['marks']);
6262 
6263  if ($unserialized_marks instanceof ASS_MarkSchema) {
6264  $unserialized_marks = $unserialized_marks->getMarkSteps();
6265  }
6266 
6267  $this->mark_schema->setMarkSteps($unserialized_marks);
6268 
6269  $this->storeActivationSettings(
6270  (bool) ($testsettings['is_activation_limited'] ?? false),
6271  $activation_starting_time,
6272  $activation_ending_time,
6273  (bool) ($testsettings['activation_visibility'] ?? false),
6274  );
6275 
6276  $main_settings = $this->getMainSettings();
6277 
6278  $general_settings = $main_settings->getGeneralSettings()
6279  ->withAnonymity((bool) $testsettings['Anonymity']);
6280  if (isset($testsettings['questionSetType'])) {
6281  $general_settings = $general_settings->withQuestionSetType($testsettings['questionSetType']);
6282  }
6283 
6284  $main_settings = $main_settings
6285  ->withGeneralSettings($general_settings)
6286  ->withIntroductionSettings(
6287  $main_settings->getIntroductionSettings()
6288  ->withIntroductionEnabled((bool) $testsettings['IntroEnabled'])
6289  ->withExamConditionsCheckboxEnabled((bool) ($testsettings['ExamConditionsCheckboxEnabled'] ?? false))
6290  )
6291  ->withAccessSettings(
6292  $main_settings->getAccessSettings()
6293  ->withStartTimeEnabled((bool) $testsettings['StartingTimeEnabled'])
6294  ->withStartTime($this->convertTimeToDateTimeImmutableIfNecessary($testsettings['StartingTime']))
6295  ->withEndTimeEnabled((bool) $testsettings['EndingTimeEnabled'])
6296  ->withEndTime($this->convertTimeToDateTimeImmutableIfNecessary($testsettings['EndingTime']))
6297  ->withPasswordEnabled((bool) $testsettings['password_enabled'])
6298  ->withPassword($testsettings['password'])
6299  ->withFixedParticipants((bool) $testsettings['fixed_participants'])
6300  )
6301  ->withTestBehaviourSettings(
6302  $main_settings->getTestBehaviourSettings()
6303  ->withNumberOfTries((int) $testsettings['NrOfTries'])
6304  ->withBlockAfterPassedEnabled((bool) $testsettings['BlockAfterPassed'])
6305  ->withPassWaiting($testsettings['pass_waiting'])
6306  ->withKioskMode($testsettings['Kiosk'])
6307  ->withProcessingTimeEnabled((bool) $testsettings['EnableProcessingTime'])
6308  ->withProcessingTime($testsettings['ProcessingTime'])
6309  ->withResetProcessingTime((bool) $testsettings['ResetProcessingTime'])
6310  ->withExamIdInTestPassEnabled((bool) ($testsettings['examid_in_test_pass'] ?? 0))
6311  )
6312  ->withQuestionBehaviourSettings(
6313  $main_settings->getQuestionBehaviourSettings()
6314  ->withQuestionTitleOutputMode($testsettings['TitleOutput'])
6315  ->withAutosaveEnabled((bool) $testsettings['autosave'])
6316  ->withAutosaveInterval($testsettings['autosave_ival'])
6317  ->withShuffleQuestions((bool) $testsettings['Shuffle'])
6318  ->withQuestionHintsEnabled((bool) $testsettings['offer_question_hints'])
6319  ->withInstantFeedbackPointsEnabled((bool) $testsettings['AnswerFeedbackPoints'])
6320  ->withInstantFeedbackGenericEnabled((bool) $testsettings['AnswerFeedback'])
6321  ->withInstantFeedbackSpecificEnabled((bool) $testsettings['SpecificAnswerFeedback'])
6322  ->withInstantFeedbackSolutionEnabled((bool) $testsettings['InstantFeedbackSolution'])
6323  ->withForceInstantFeedbackOnNextQuestion((bool) $testsettings['force_inst_fb'])
6324  ->withLockAnswerOnInstantFeedbackEnabled((bool) $testsettings['inst_fb_answer_fixation'])
6325  ->withLockAnswerOnNextQuestionEnabled((bool) $testsettings['follow_qst_answer_fixation'])
6326  ->withCompulsoryQuestionsEnabled((bool) $testsettings['obligations_enabled'])
6327  )
6328  ->withParticipantFunctionalitySettings(
6329  $main_settings->getParticipantFunctionalitySettings()
6330  ->withUsePreviousAnswerAllowed((bool) $testsettings['use_previous_answers'])
6331  ->withSuspendTestAllowed((bool) $testsettings['ShowCancel'])
6332  ->withPostponedQuestionsMoveToEnd((bool) $testsettings['SequenceSettings'])
6333  ->withUsrPassOverviewMode((int) $testsettings['ListOfQuestionsSettings'])
6334  ->withQuestionMarkingEnabled((bool) $testsettings['ShowMarker'])
6335  )
6336  ->withFinishingSettings(
6337  $main_settings->getFinishingSettings()
6338  ->withShowAnswerOverview((bool) $testsettings['enable_examview'])
6339  ->withConcludingRemarksEnabled((bool) $testsettings['ShowFinalStatement'])
6340  ->withRedirectionMode((int) $testsettings['redirection_mode'])
6341  ->withRedirectionUrl($testsettings['redirection_url'])
6342  ->withMailNotificationContentType((int) $testsettings['mailnotification'])
6343  ->withAlwaysSendMailNotification((bool) $testsettings['mailnottype'])
6344  )
6345  ->withAdditionalSettings(
6346  $main_settings->getAdditionalSettings()
6347  ->withSkillsServiceEnabled((bool) $testsettings['skill_service'])
6348  ->withHideInfoTab((bool) ($testsettings['HideInfoTab'] ?? false))
6349  );
6350 
6351  $this->getMainSettingsRepository()->store($main_settings);
6352 
6353  $reporting_date = $testsettings['ReportingDate'];
6354  if (is_string($reporting_date)) {
6355  $reporting_date = new DateTimeImmutable($testsettings['ReportingDate'], new DateTimeZone('UTC'));
6356  }
6357 
6358  $score_settings = $this->getScoreSettings();
6359  $score_settings = $score_settings
6361  $score_settings->getScoringSettings()
6362  ->withPassScoring($testsettings['PassScoring'])
6363  ->withScoreCutting($testsettings['ScoreCutting'])
6364  ->withCountSystem($testsettings['CountSystem'])
6365  )
6366  ->withResultSummarySettings(
6367  $score_settings->getResultSummarySettings()
6368  ->withPassDeletionAllowed((bool) $testsettings['pass_deletion_allowed'])
6369  ->withShowGradingStatusEnabled((bool) $testsettings['show_grading_status'])
6370  ->withShowGradingMarkEnabled((bool) $testsettings['show_grading_mark'])
6371  ->withScoreReporting((int) $testsettings['ScoreReporting'])
6372  ->withReportingDate($reporting_date)
6373  )
6374  ->withResultDetailsSettings(
6375  $score_settings->getResultDetailsSettings()
6376  ->withResultsPresentation((int) $testsettings['ResultsPresentation'])
6377  ->withShowSolutionListComparison((bool) ($testsettings['show_solution_list_comparison'] ?? 0))
6378  ->withShowExamIdInTestResults((bool) $testsettings['examid_in_test_res'])
6379  )
6380  ->withGamificationSettings(
6381  $score_settings->getGamificationSettings()
6382  ->withHighscoreEnabled((bool) $testsettings['highscore_enabled'])
6383  ->withHighscoreAnon((bool) $testsettings['highscore_anon'])
6384  ->withHighscoreAchievedTS($testsettings['highscore_achieved_ts'])
6385  ->withHighscoreScore((bool) $testsettings['highscore_score'])
6386  ->withHighscorePercentage($testsettings['highscore_percentage'])
6387  ->withHighscoreHints((bool) $testsettings['highscore_hints'])
6388  ->withHighscoreWTime((bool) $testsettings['highscore_wtime'])
6389  ->withHighscoreOwnTable((bool) $testsettings['highscore_own_table'])
6390  ->withHighscoreTopTable((bool) $testsettings['highscore_top_table'])
6391  ->withHighscoreTopNum($testsettings['highscore_top_num'])
6392  )
6393  ;
6394  $this->getScoreSettingsRepository()->store($score_settings);
6395  $this->saveToDb();
6396 
6397  return true;
6398  }
6399 
6401  DateTimeImmutable|int|null $date_time
6402  ): ?DateTimeImmutable {
6403  if ($date_time === null || $date_time instanceof DateTimeImmutable) {
6404  return $date_time;
6405  }
6406 
6407  return DateTimeImmutable::createFromFormat('U', (string) $date_time);
6408  }
6409 
6417  public function processPrintoutput2FO($print_output): string
6418  {
6419  if (extension_loaded("tidy")) {
6420  $config = [
6421  "indent" => false,
6422  "output-xml" => true,
6423  "numeric-entities" => true
6424  ];
6425  $tidy = new tidy();
6426  $tidy->parseString($print_output, $config, 'utf8');
6427  $tidy->cleanRepair();
6428  $print_output = tidy_get_output($tidy);
6429  $print_output = preg_replace("/^.*?(<html)/", "\\1", $print_output);
6430  } else {
6431  $print_output = str_replace("&nbsp;", "&#160;", $print_output);
6432  $print_output = str_replace("&otimes;", "X", $print_output);
6433  }
6434  $xsl = file_get_contents("./Modules/Test/xml/question2fo.xsl");
6435 
6436  // additional font support
6437 
6438  $xsl = str_replace(
6439  'font-family="Helvetica, unifont"',
6440  'font-family="' . $this->settings->get('rpc_pdf_font', 'Helvetica, unifont') . '"',
6441  $xsl
6442  );
6443 
6444  $args = [ '/_xml' => $print_output, '/_xsl' => $xsl ];
6445  $xh = xslt_create();
6446  $params = [];
6447  $output = xslt_process($xh, "arg:/_xml", "arg:/_xsl", null, $args, $params);
6448  xslt_error($xh);
6449  xslt_free($xh);
6450  return $output;
6451  }
6452 
6459  public function deliverPDFfromHTML($content, $title = null)
6460  {
6461  $content = preg_replace("/href=\".*?\"/", "", $content);
6462  $printbody = new ilTemplate("tpl.il_as_tst_print_body.html", true, true, "Modules/Test");
6463  $printbody->setVariable("TITLE", ilLegacyFormElementsUtil::prepareFormOutput($this->getTitle()));
6464  $printbody->setVariable("ADM_CONTENT", $content);
6465  $printbody->setCurrentBlock("css_file");
6466  $printbody->setVariable("CSS_FILE", ilUtil::getStyleSheetLocation("filesystem", "delos.css"));
6467  $printbody->parseCurrentBlock();
6468  $printoutput = $printbody->get();
6469  $html = str_replace("href=\"./", "href=\"" . ILIAS_HTTP_PATH . "/", $printoutput);
6470  $html = preg_replace("/<div id=\"dontprint\">.*?<\\/div>/ims", "", $html);
6471  if (extension_loaded("tidy")) {
6472  $config = [
6473  "indent" => false,
6474  "output-xml" => true,
6475  "numeric-entities" => true
6476  ];
6477  $tidy = new tidy();
6478  $tidy->parseString($html, $config, 'utf8');
6479  $tidy->cleanRepair();
6480  $html = tidy_get_output($tidy);
6481  $html = preg_replace("/^.*?(<html)/", "\\1", $html);
6482  } else {
6483  $html = str_replace("&nbsp;", "&#160;", $html);
6484  $html = str_replace("&otimes;", "X", $html);
6485  }
6486  $html = preg_replace("/src=\".\\//ims", "src=\"" . ILIAS_HTTP_PATH . "/", $html);
6487  $this->deliverPDFfromFO($this->processPrintoutput2FO($html), $title);
6488  }
6489 
6495  public function deliverPDFfromFO($fo, $title = null): bool
6496  {
6497  $fo_file = ilFileUtils::ilTempnam() . ".fo";
6498  $fp = fopen($fo_file, "w");
6499  fwrite($fp, $fo);
6500  fclose($fp);
6501 
6502  try {
6503  $pdf_base64 = ilRpcClientFactory::factory('RPCTransformationHandler')->ilFO2PDF($fo);
6504  $filename = (strlen($title)) ? $title : $this->getTitle();
6507  $pdf_base64->scalar,
6509  "application/pdf"
6510  );
6511  return true;
6512  } catch (Exception $e) {
6513  $this->log->write(__METHOD__ . ': ' . $e->getMessage());
6514  return false;
6515  }
6516  }
6517 
6527  public static function getManualFeedback(int $active_id, int $question_id, ?int $pass): string
6528  {
6529  if ($pass === null) {
6530  return '';
6531  }
6532  $feedback = '';
6533  $row = self::getSingleManualFeedback((int) $active_id, (int) $question_id, (int) $pass);
6534 
6535  if ($row !== [] && ($row['finalized_evaluation'] || \ilTestService::isManScoringDone((int) $active_id))) {
6536  $feedback = $row['feedback'] ?? '';
6537  }
6538 
6539  return $feedback;
6540  }
6541 
6542  public static function getSingleManualFeedback(int $active_id, int $question_id, int $pass): array
6543  {
6544  global $DIC;
6545  $ilDB = $DIC['ilDB'];
6546  $row = [];
6547  $result = $ilDB->queryF(
6548  "SELECT * FROM tst_manual_fb WHERE active_fi = %s AND question_fi = %s AND pass = %s",
6549  ['integer', 'integer', 'integer'],
6550  [$active_id, $question_id, $pass]
6551  );
6552 
6553  if ($ilDB->numRows($result) === 1) {
6554  $row = $ilDB->fetchAssoc($result);
6555  $row['feedback'] = ilRTE::_replaceMediaObjectImageSrc($row['feedback'] ?? '', 1);
6556  } elseif ($ilDB->numRows($result) > 1) {
6557  $DIC->logger()->root()->warning(
6558  "WARNING: Multiple feedback entries on tst_manual_fb for " .
6559  "active_fi = $active_id , question_fi = $question_id and pass = $pass"
6560  );
6561  }
6562 
6563  return $row;
6564  }
6565 
6573  public static function getCompleteManualFeedback(int $question_id): array
6574  {
6575  global $DIC;
6576  $ilDB = $DIC['ilDB'];
6577 
6578  $feedback = [];
6579  $result = $ilDB->queryF(
6580  "SELECT * FROM tst_manual_fb WHERE question_fi = %s",
6581  ['integer'],
6582  [$question_id]
6583  );
6584 
6585  while ($row = $ilDB->fetchAssoc($result)) {
6586  $active = $row['active_fi'];
6587  $pass = $row['pass'];
6588  $question = $row['question_fi'];
6589 
6590  $row['feedback'] = ilRTE::_replaceMediaObjectImageSrc($row['feedback'] ?? '', 1);
6591 
6592  $feedback[$active][$pass][$question] = $row;
6593  }
6594 
6595  return $feedback;
6596  }
6597 
6598  public function saveManualFeedback(
6599  int $active_id,
6600  int $question_id,
6601  int $pass,
6602  ?string $feedback,
6603  bool $finalized = false,
6604  bool $is_single_feedback = false
6605  ): bool {
6606  $feedback_old = self::getSingleManualFeedback($active_id, $question_id, $pass);
6607 
6608  $finalized_record = (int) ($feedback_old['finalized_evaluation'] ?? 0);
6609  if ($finalized_record === 0 || ($is_single_feedback && $finalized_record === 1)) {
6610  $this->db->manipulateF(
6611  "DELETE FROM tst_manual_fb WHERE active_fi = %s AND question_fi = %s AND pass = %s",
6612  ['integer', 'integer', 'integer'],
6613  [$active_id, $question_id, $pass]
6614  );
6615 
6616  $this->insertManualFeedback($active_id, $question_id, $pass, $feedback, $finalized, $feedback_old);
6617 
6619  $this->logManualFeedback($active_id, $question_id, $feedback);
6620  }
6621  }
6622 
6623  return true;
6624  }
6625 
6626  private function insertManualFeedback(
6627  int $active_id,
6628  int $question_id,
6629  int $pass,
6630  ?string $feedback,
6631  bool $finalized,
6632  array $feedback_old
6633  ): void {
6634  $next_id = $this->db->nextId('tst_manual_fb');
6635  $user = $this->user->getId();
6636  $finalized_time = time();
6637 
6638  $update_default = [
6639  'manual_feedback_id' => [ 'integer', $next_id],
6640  'active_fi' => [ 'integer', $active_id],
6641  'question_fi' => [ 'integer', $question_id],
6642  'pass' => [ 'integer', $pass],
6643  'feedback' => [ 'clob', $feedback ? ilRTE::_replaceMediaObjectImageSrc($feedback, 0) : null],
6644  'tstamp' => [ 'integer', time()]
6645  ];
6646 
6647  if ($feedback_old !== [] && (int) $feedback_old['finalized_evaluation'] === 1) {
6648  $user = $feedback_old['finalized_by_usr_id'];
6649  $finalized_time = $feedback_old['finalized_tstamp'];
6650  }
6651 
6652  if ($finalized === false) {
6653  $update_default['finalized_evaluation'] = ['integer', 0];
6654  $update_default['finalized_by_usr_id'] = ['integer', 0];
6655  $update_default['finalized_tstamp'] = ['integer', 0];
6656  } elseif ($finalized === true) {
6657  $update_default['finalized_evaluation'] = ['integer', 1];
6658  $update_default['finalized_by_usr_id'] = ['integer', $user];
6659  $update_default['finalized_tstamp'] = ['integer', $finalized_time];
6660  }
6661 
6662  $this->db->insert('tst_manual_fb', $update_default);
6663  }
6664 
6672  private function logManualFeedback($active_id, $question_id, $feedback)
6673  {
6674  $username = ilObjTestAccess::_getParticipantData($active_id);
6675 
6676  $this->logAction(
6677  sprintf(
6678  $this->lng->txtlng('assessment', 'log_manual_feedback', ilObjAssessmentFolder::_getLogLanguage()),
6679  $this->user->getFullname() . ' (' . $this->user->getLogin() . ')',
6680  $username,
6681  $this->questioninfo->getQuestionTitle($question_id),
6682  $feedback
6683  )
6684  );
6685  }
6686 
6694  public function getJavaScriptOutput(): bool
6695  {
6696  return true;
6697  }
6698 
6699  public function &createTestSequence($active_id, $pass, $shuffle)
6700  {
6701  $this->test_sequence = new ilTestSequence($active_id, $pass, $this->isRandomTest(), $this->questioninfo);
6702  }
6703 
6709  public function setTestId($a_id)
6710  {
6711  $this->test_id = $a_id;
6712  }
6713 
6722  public function getDetailedTestResults($participants): array
6723  {
6724  $results = [];
6725  if (count($participants)) {
6726  foreach ($participants as $active_id => $user_rec) {
6727  $row = [];
6728  $reached_points = 0;
6729  $max_points = 0;
6730  $pass = ilObjTest::_getResultPass($active_id);
6731  foreach ($this->questions as $value) {
6732  $question = ilObjTest::_instanciateQuestion($value);
6733  if (is_object($question)) {
6734  $max_points += $question->getMaximumPoints();
6735  $reached_points += $question->getReachedPoints($active_id, $pass);
6736  if ($max_points > 0) {
6737  $percentvalue = $reached_points / $max_points;
6738  if ($percentvalue < 0) {
6739  $percentvalue = 0.0;
6740  }
6741  } else {
6742  $percentvalue = 0;
6743  }
6744  if ($this->getAnonymity()) {
6745  $user_rec['firstname'] = "";
6746  $user_rec['lastname'] = $this->lng->txt("anonymous");
6747  }
6748  $row = [
6749  "user_id" => $user_rec['usr_id'],
6750  "matriculation" => $user_rec['matriculation'],
6751  "lastname" => $user_rec['lastname'],
6752  "firstname" => $user_rec['firstname'],
6753  "login" => $user_rec['login'],
6754  "question_id" => $question->getId(),
6755  "question_title" => $question->getTitle(),
6756  "reached_points" => $reached_points,
6757  "max_points" => $max_points,
6758  "passed" => $user_rec['passed'] ? '1' : '0',
6759  ];
6760  $results[] = $row;
6761  }
6762  }
6763  }
6764  }
6765  return $results;
6766  }
6767 
6771  public static function _lookupTestObjIdForQuestionId(int $q_id): ?int
6772  {
6773  global $DIC;
6774  $ilDB = $DIC['ilDB'];
6775 
6776  $result = $ilDB->queryF(
6777  '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',
6778  ['integer'],
6779  [$q_id]
6780  );
6781  $rec = $ilDB->fetchAssoc($result);
6782  return $rec['obj_id'] ?? null;
6783  }
6784 
6791  public function isPluginActive($a_pname): bool
6792  {
6793  if (!$this->component_repository->getComponentByTypeAndName(
6795  'TestQuestionPool'
6796  )->getPluginSlotById('qst')->hasPluginName($a_pname)) {
6797  return false;
6798  }
6799 
6800  return $this->component_repository
6801  ->getComponentByTypeAndName(
6803  'TestQuestionPool'
6804  )
6805  ->getPluginSlotById(
6806  'qst'
6807  )
6808  ->getPluginByName(
6809  $a_pname
6810  )->isActive();
6811  }
6812 
6813  public function getPassed($active_id)
6814  {
6815  $result = $this->db->queryF(
6816  "SELECT passed FROM tst_result_cache WHERE active_fi = %s",
6817  ['integer'],
6818  [$active_id]
6819  );
6820  if ($result->numRows()) {
6821  $row = $this->db->fetchAssoc($result);
6822  return $row['passed'];
6823  } else {
6824  $counted_pass = ilObjTest::_getResultPass($active_id);
6825  $result_array = &$this->getTestResult($active_id, $counted_pass);
6826  return $result_array["test"]["passed"];
6827  }
6828  }
6829 
6833  public function getParticipantsForTestAndQuestion($test_id, $question_id): array
6834  {
6835  $query = "
6836  SELECT tst_test_result.active_fi, tst_test_result.question_fi, tst_test_result.pass
6837  FROM tst_test_result
6838  INNER JOIN tst_active ON tst_active.active_id = tst_test_result.active_fi AND tst_active.test_fi = %s
6839  INNER JOIN qpl_questions ON qpl_questions.question_id = tst_test_result.question_fi
6840  LEFT JOIN usr_data ON usr_data.usr_id = tst_active.user_fi
6841  WHERE tst_test_result.question_fi = %s
6842  ORDER BY usr_data.lastname ASC, usr_data.firstname ASC
6843  ";
6844 
6845  $result = $this->db->queryF(
6846  $query,
6847  ['integer', 'integer'],
6848  [$test_id, $question_id]
6849  );
6850  $foundusers = [];
6852  while ($row = $this->db->fetchAssoc($result)) {
6853  if ($this->getAccessFilteredParticipantList() && !$this->getAccessFilteredParticipantList()->isActiveIdInList($row["active_fi"])) {
6854  continue;
6855  }
6856 
6857  if (!array_key_exists($row["active_fi"], $foundusers)) {
6858  $foundusers[$row["active_fi"]] = [];
6859  }
6860  array_push($foundusers[$row["active_fi"]], ["pass" => $row["pass"], "qid" => $row["question_fi"]]);
6861  }
6862  return $foundusers;
6863  }
6864 
6870  public function getAggregatedResultsData(): array
6871  {
6872  $data = &$this->getCompleteEvaluationData();
6873  $foundParticipants = $data->getParticipants();
6874  $results = ["overview" => [], "questions" => []];
6875  if (count($foundParticipants)) {
6876  $results["overview"][$this->lng->txt("tst_eval_total_persons")] = count($foundParticipants);
6877  $total_finished = $data->getTotalFinishedParticipants();
6878  $results["overview"][$this->lng->txt("tst_eval_total_finished")] = $total_finished;
6879  $average_time = $this->evalTotalStartedAverageTime($data->getParticipantIds());
6880  $diff_seconds = $average_time;
6881  $diff_hours = floor($diff_seconds / 3600);
6882  $diff_seconds -= $diff_hours * 3600;
6883  $diff_minutes = floor($diff_seconds / 60);
6884  $diff_seconds -= $diff_minutes * 60;
6885  $results["overview"][$this->lng->txt("tst_eval_total_finished_average_time")] = sprintf("%02d:%02d:%02d", $diff_hours, $diff_minutes, $diff_seconds);
6886  $total_passed = 0;
6887  $total_passed_reached = 0;
6888  $total_passed_max = 0;
6889  $total_passed_time = 0;
6890  foreach ($foundParticipants as $userdata) {
6891  if ($userdata->getPassed()) {
6892  $total_passed++;
6893  $total_passed_reached += $userdata->getReached();
6894  $total_passed_max += $userdata->getMaxpoints();
6895  $total_passed_time += $userdata->getTimeOfWork();
6896  }
6897  }
6898  $average_passed_reached = $total_passed ? $total_passed_reached / $total_passed : 0;
6899  $average_passed_max = $total_passed ? $total_passed_max / $total_passed : 0;
6900  $average_passed_time = $total_passed ? $total_passed_time / $total_passed : 0;
6901  $results["overview"][$this->lng->txt("tst_eval_total_passed")] = $total_passed;
6902  $results["overview"][$this->lng->txt("tst_eval_total_passed_average_points")] = sprintf("%2.2f", $average_passed_reached) . " " . strtolower($this->lng->txt("of")) . " " . sprintf("%2.2f", $average_passed_max);
6903  $average_time = $average_passed_time;
6904  $diff_seconds = $average_time;
6905  $diff_hours = floor($diff_seconds / 3600);
6906  $diff_seconds -= $diff_hours * 3600;
6907  $diff_minutes = floor($diff_seconds / 60);
6908  $diff_seconds -= $diff_minutes * 60;
6909  $results["overview"][$this->lng->txt("tst_eval_total_passed_average_time")] = sprintf("%02d:%02d:%02d", $diff_hours, $diff_minutes, $diff_seconds);
6910  }
6911 
6912  foreach ($data->getQuestionTitles() as $question_id => $question_title) {
6913  $answered = 0;
6914  $reached = 0;
6915  $max = 0;
6916  foreach ($foundParticipants as $userdata) {
6917  for ($i = 0; $i <= $userdata->getLastPass(); $i++) {
6918  if (is_object($userdata->getPass($i))) {
6919  $question = $userdata->getPass($i)->getAnsweredQuestionByQuestionId($question_id);
6920  if (is_array($question)) {
6921  $answered++;
6922  $reached += $question["reached"];
6923  $max += $question["points"];
6924  }
6925  }
6926  }
6927  }
6928  $percent = $max ? $reached / $max * 100.0 : 0;
6929  $results["questions"][$question_id] = [
6930  $question_title,
6931  sprintf("%.2f", $answered ? $reached / $answered : 0) . " " . strtolower($this->lng->txt("of")) . " " . sprintf("%.2f", $answered ? $max / $answered : 0),
6932  sprintf("%.2f", $percent) . "%",
6933  $answered,
6934  sprintf("%.2f", $answered ? $reached / $answered : 0),
6935  sprintf("%.2f", $answered ? $max / $answered : 0),
6936  $percent / 100.0
6937  ];
6938  }
6939  return $results;
6940  }
6941 
6945  public function getXMLZip(): string
6946  {
6947  $expFactory = new ilTestExportFactory($this, $this->lng, $this->log, $this->tree, $this->component_repository, $this->questioninfo);
6948  $test_exp = $expFactory->getExporter('xml');
6949  return $test_exp->buildExportFile();
6950  }
6951 
6952  public function getMailNotification(): int
6953  {
6954  return $this->getMainSettings()->getFinishingSettings()->getMailNotificationContentType();
6955  }
6956 
6957  public function sendSimpleNotification($active_id)
6958  {
6959  $mail = new ilTestMailNotification();
6960  $owner_id = $this->getOwner();
6961  $usr_data = $this->userLookupFullName(ilObjTest::_getUserIdFromActiveId($active_id));
6962  $mail->sendSimpleNotification($owner_id, $this->getTitle(), $usr_data);
6963  }
6964 
6970  public function getEvaluationAdditionalFields(): array
6971  {
6972  $table_gui = new ilEvaluationAllTableGUI(
6973  new ilObjTestGUI($this->getRefId()),
6974  'outEvaluation',
6975  $this->settings,
6976  $this->getAnonymity()
6977  );
6978  return $table_gui->getSelectedColumns();
6979  }
6980 
6981  public function sendAdvancedNotification(int $active_id): void
6982  {
6983  $mail = new ilTestMailNotification();
6984  $owner_id = $this->getOwner();
6985  $usr_data = $this->userLookupFullName(ilObjTest::_getUserIdFromActiveId($active_id));
6986 
6987  $worksheet = (new ilExcelTestExport($this, ilTestEvaluationData::FILTER_BY_ACTIVE_ID, (string) $active_id, false, true))
6988  ->withResultsPage()
6989  ->withUserPages()
6990  ->getContent();
6991  $temp_file_path = ilFileUtils::ilTempnam();
6992  $delivered_file_name = 'result_' . $active_id . '.xlsx';
6993  $worksheet->writeToFile($temp_file_path);
6994  $fd = new ilFileDataMail(ANONYMOUS_USER_ID);
6995  $fd->copyAttachmentFile($temp_file_path . '.xlsx', $delivered_file_name);
6996  $file_names[] = $delivered_file_name;
6997 
6998  $mail->sendAdvancedNotification($owner_id, $this->getTitle(), $usr_data, $file_names);
6999 
7000  if (count($file_names)) {
7001  $fd->unlinkFiles($file_names);
7002  unset($fd);
7003  @unlink($file . 'xlsx');
7004  }
7005  }
7006 
7007  public function getResultsForActiveId(int $active_id): array
7008  {
7009  $query = "
7010  SELECT *
7011  FROM tst_result_cache
7012  WHERE active_fi = %s
7013  ";
7014 
7015  $result = $this->db->queryF(
7016  $query,
7017  ['integer'],
7018  [$active_id]
7019  );
7020 
7021  if (!$result->numRows()) {
7022  $this->updateTestResultCache($active_id);
7023 
7024  $query = "
7025  SELECT *
7026  FROM tst_result_cache
7027  WHERE active_fi = %s
7028  ";
7029 
7030  $result = $this->db->queryF(
7031  $query,
7032  ['integer'],
7033  [$active_id]
7034  );
7035  }
7036 
7037  $row = $this->db->fetchAssoc($result);
7038 
7039  return $row;
7040  }
7041 
7042  public function getMailNotificationType(): bool
7043  {
7044  return $this->getMainSettings()->getFinishingSettings()->getAlwaysSendMailNotification();
7045  }
7046 
7047  public function getExportSettings(): int
7048  {
7049  return $this->getScoreSettings()->getResultDetailsSettings()->getExportSettings();
7050  }
7051 
7052  public function setTemplate(int $template_id)
7053  {
7054  $this->template_id = $template_id;
7055  }
7056 
7057  public function getTemplate(): int
7058  {
7059  return $this->template_id;
7060  }
7061 
7062  public function moveQuestionAfterOLD($previous_question_id, $new_question_id)
7063  {
7064  $new_array = [];
7065  $position = 1;
7066 
7067  $query = 'SELECT question_fi FROM tst_test_question WHERE test_fi = %s';
7068  $types = ['integer'];
7069  $values = [$this->getTestId()];
7070 
7071  $new_question_id += 1;
7072 
7073  $inserted = false;
7074  $res = $this->db->queryF($query, $types, $values);
7075  while ($row = $this->db->fetchAssoc($res)) {
7076  $qid = $row['question_fi'];
7077 
7078  if ($qid == $new_question_id) {
7079  continue;
7080  } elseif ($qid == $previous_question_id) {
7081  $new_array[$position++] = $qid;
7082  $new_array[$position++] = $new_question_id;
7083  $inserted = true;
7084  } else {
7085  $new_array[$position++] = $qid;
7086  }
7087  }
7088 
7089  $update_query = 'UPDATE tst_test_question SET sequence = %s WHERE test_fi = %s AND question_fi = %s';
7090  $update_types = ['integer', 'integer', 'integer'];
7091 
7092  foreach ($new_array as $position => $qid) {
7093  $this->db->manipulateF(
7094  $update_query,
7095  $update_types,
7096  $vals = [
7097  $position,
7098  $this->getTestId(),
7099  $qid
7100  ]
7101  );
7102  }
7103  }
7104 
7106  {
7107  $question_set_config = $this->question_set_config_factory->getQuestionSetConfig();
7108  $reindexed_sequence_position_map = $question_set_config->reindexQuestionOrdering();
7109 
7110  $this->loadQuestions();
7111 
7112  return $reindexed_sequence_position_map;
7113  }
7114 
7115  public function setQuestionOrderAndObligations($orders, $obligations)
7116  {
7117  asort($orders);
7118 
7119  $i = 0;
7120 
7121  foreach ($orders as $id => $position) {
7122  $i++;
7123 
7124  $obligatory = (
7125  isset($obligations[$id]) && $obligations[$id] ? 1 : 0
7126  );
7127 
7128  $query = "
7129  UPDATE tst_test_question
7130  SET sequence = %s,
7131  obligatory = %s
7132  WHERE question_fi = %s
7133  ";
7134 
7135  $this->db->manipulateF(
7136  $query,
7137  ['integer', 'integer', 'integer'],
7138  [$i, $obligatory, $id]
7139  );
7140  }
7141 
7142  $this->loadQuestions();
7143  }
7144 
7145  public function moveQuestionAfter($question_to_move, $question_before)
7146  {
7147  if ($question_before) {
7148  $query = 'SELECT sequence, test_fi FROM tst_test_question WHERE question_fi = %s';
7149  $types = ['integer'];
7150  $values = [$question_before];
7151  $rset = $this->db->queryF($query, $types, $values);
7152  }
7153 
7154  if (!$question_before || ($rset && !($row = $this->db->fetchAssoc($rset)))) {
7155  $row = [
7156  'sequence' => 0,
7157  'test_fi' => $this->getTestId(),
7158  ];
7159  }
7160 
7161  $update = 'UPDATE tst_test_question SET sequence = sequence + 1 WHERE sequence > %s AND test_fi = %s';
7162  $types = ['integer', 'integer'];
7163  $values = [$row['sequence'], $row['test_fi']];
7164  $this->db->manipulateF($update, $types, $values);
7165 
7166  $update = 'UPDATE tst_test_question SET sequence = %s WHERE question_fi = %s';
7167  $types = ['integer', 'integer'];
7168  $values = [$row['sequence'] + 1, $question_to_move];
7169  $this->db->manipulateF($update, $types, $values);
7170 
7172  }
7173 
7174  public function hasQuestionsWithoutQuestionpool(): bool
7175  {
7176  $questions = $this->getQuestionTitlesAndIndexes();
7177 
7178  $IN_questions = $this->db->in('q1.question_id', array_keys($questions), false, 'integer');
7179 
7180  $query = "
7181  SELECT count(q1.question_id) cnt
7182 
7183  FROM qpl_questions q1
7184 
7185  INNER JOIN qpl_questions q2
7186  ON q2.question_id = q1.original_id
7187 
7188  WHERE $IN_questions
7189  AND q1.obj_fi = q2.obj_fi
7190  ";
7191  $rset = $this->db->query($query);
7192  $row = $this->db->fetchAssoc($rset);
7193 
7194  return $row['cnt'] > 0;
7195  }
7196 
7203  public static function _lookupFinishedUserTests($a_user_id): array
7204  {
7205  global $DIC;
7206  $ilDB = $DIC['ilDB'];
7207 
7208  $result = $ilDB->queryF(
7209  "SELECT test_fi,MAX(pass) AS pass FROM tst_active" .
7210  " JOIN tst_pass_result ON (tst_pass_result.active_fi = tst_active.active_id)" .
7211  " WHERE user_fi=%s" .
7212  " GROUP BY test_fi",
7213  ['integer', 'integer'],
7214  [$a_user_id, 1]
7215  );
7216  $all = [];
7217  while ($row = $ilDB->fetchAssoc($result)) {
7218  $obj_id = self::_getObjectIDFromTestID($row["test_fi"]);
7219  $all[$obj_id] = (bool) $row["pass"];
7220  }
7221  return $all;
7222  }
7223  public function getQuestions(): array
7224  {
7225  return $this->questions;
7226  }
7227 
7228  public function isOnline(): bool
7229  {
7230  return $this->online;
7231  }
7232 
7233  public function isOfferingQuestionHintsEnabled(): bool
7234  {
7235  return $this->getMainSettings()->getQuestionBehaviourSettings()->getQuestionHintsEnabled();
7236  }
7237 
7238  public function setActivationVisibility($a_value)
7239  {
7240  $this->activation_visibility = (bool) $a_value;
7241  }
7242 
7243  public function getActivationVisibility(): bool
7244  {
7246  }
7247 
7248  public function isActivationLimited(): ?bool
7249  {
7251  }
7252 
7253  public function setActivationLimited($a_value)
7254  {
7255  $this->activation_limited = (bool) $a_value;
7256  }
7257 
7258  public function storeActivationSettings(
7259  ?bool $is_activation_limited = false,
7260  ?int $activation_starting_time = null,
7261  ?int $activation_ending_time = null,
7262  bool $activation_visibility = false,
7263  ): void {
7264  if (!$this->ref_id) {
7265  return;
7266  }
7267 
7268  $item = new ilObjectActivation();
7269  $is_activation_limited ??= false;
7270 
7271  if (!$is_activation_limited) {
7272  $item->setTimingType(ilObjectActivation::TIMINGS_DEACTIVATED);
7273  } else {
7274  $item->setTimingType(ilObjectActivation::TIMINGS_ACTIVATION);
7275  $item->setTimingStart($activation_starting_time);
7276  $item->setTimingEnd($activation_ending_time);
7277  $item->toggleVisible($activation_visibility);
7278  }
7279 
7280  $item->update($this->ref_id);
7281 
7282  $this->setActivationLimited($is_activation_limited);
7283  $this->setActivationStartingTime($activation_starting_time);
7284  $this->setActivationStartingTime($activation_ending_time);
7285  $this->setActivationVisibility($activation_visibility);
7286  }
7287 
7288  public function getIntroductionPageId(): int
7289  {
7290  $page_id = $this->getMainSettings()->getIntroductionSettings()->getIntroductionPageId();
7291  if ($page_id !== null) {
7292  return $page_id;
7293  }
7294 
7295  $page_object = new ilTestPage();
7296  $page_object->setParentId($this->getId());
7297  $new_page_id = $page_object->createPageWithNextId();
7298  $settings = $this->getMainSettings()->getIntroductionSettings()
7299  ->withIntroductionPageId($new_page_id);
7300  $this->getMainSettingsRepository()->store(
7301  $this->getMainSettings()->withIntroductionSettings($settings)
7302  );
7303  return $new_page_id;
7304  }
7305 
7306  public function getConcludingRemarksPageId(): int
7307  {
7308  $page_id = $this->getMainSettings()->getFinishingSettings()->getConcludingRemarksPageId();
7309  if ($page_id !== null) {
7310  return $page_id;
7311  }
7312 
7313  $page_object = new ilTestPage();
7314  $page_object->setParentId($this->getId());
7315  $new_page_id = $page_object->createPageWithNextId();
7316  $settings = $this->getMainSettings()->getFinishingSettings()
7317  ->withConcludingRemarksPageId($new_page_id);
7318  $this->getMainSettingsRepository()->store(
7319  $this->getMainSettings()->withFinishingSettings($settings)
7320  );
7321  return $new_page_id;
7322  }
7323 
7324  public function getHighscoreEnabled(): bool
7325  {
7326  return $this->getScoreSettings()->getGamificationSettings()->getHighscoreEnabled();
7327  }
7328 
7338  public function getHighscoreAnon(): bool
7339  {
7340  return $this->getScoreSettings()->getGamificationSettings()->getHighscoreAnon();
7341  }
7342 
7351  public function isHighscoreAnon(): bool
7352  {
7353  return $this->getAnonymity() == 1 || $this->getHighscoreAnon();
7354  }
7355 
7359  public function getHighscoreAchievedTS(): bool
7360  {
7361  return $this->getScoreSettings()->getGamificationSettings()->getHighscoreAchievedTS();
7362  }
7363 
7367  public function getHighscoreScore(): bool
7368  {
7369  return $this->getScoreSettings()->getGamificationSettings()->getHighscoreScore();
7370  }
7371 
7375  public function getHighscorePercentage(): bool
7376  {
7377  return $this->getScoreSettings()->getGamificationSettings()->getHighscorePercentage();
7378  }
7379 
7383  public function getHighscoreHints(): bool
7384  {
7385  return $this->getScoreSettings()->getGamificationSettings()->getHighscoreHints();
7386  }
7387 
7391  public function getHighscoreWTime(): bool
7392  {
7393  return $this->getScoreSettings()->getGamificationSettings()->getHighscoreWTime();
7394  }
7395 
7399  public function getHighscoreOwnTable(): bool
7400  {
7401  return $this->getScoreSettings()->getGamificationSettings()->getHighscoreOwnTable();
7402  }
7403 
7407  public function getHighscoreTopTable(): bool
7408  {
7409  return $this->getScoreSettings()->getGamificationSettings()->getHighscoreTopTable();
7410  }
7411 
7416  public function getHighscoreTopNum(int $a_retval = 10): int
7417  {
7418  return $this->getScoreSettings()->getGamificationSettings()->getHighscoreTopNum();
7419  }
7420 
7421  public function getHighscoreMode(): int
7422  {
7423  return $this->getScoreSettings()->getGamificationSettings()->getHighScoreMode();
7424  }
7425 
7426  public function getSpecificAnswerFeedback(): bool
7427  {
7428  return $this->getMainSettings()->getQuestionBehaviourSettings()->getInstantFeedbackSpecificEnabled();
7429  }
7430 
7431  public function areObligationsEnabled(): bool
7432  {
7433  return $this->getMainSettings()->getQuestionBehaviourSettings()->getCompulsoryQuestionsEnabled();
7434  }
7435 
7436  public static function isQuestionObligationPossible(int $question_id): bool
7437  {
7438  global $DIC;
7439  $question_info = $DIC->testQuestionPool()->questionInfo();
7440  $class = $question_info->getQuestionType($question_id);
7441  return call_user_func([$class, 'isObligationPossible'], $question_id);
7442  }
7443 
7450  public static function isQuestionObligatory($question_id): bool
7451  {
7452  global $DIC;
7453  $ilDB = $DIC['ilDB'];
7454 
7455  $rset = $ilDB->queryF('SELECT obligatory FROM tst_test_question WHERE question_fi = %s', ['integer'], [$question_id]);
7456 
7457  if ($row = $ilDB->fetchAssoc($rset)) {
7458  return (bool) $row['obligatory'];
7459  }
7460 
7461  return false;
7462  }
7463 
7476  public function allObligationsAnswered(): bool
7477  {
7478  if (!$this->hasObligations()) {
7479  return true;
7480  }
7481 
7482  if ($this->current_user_all_obliations_answered === null) {
7483  $active_id = $this->getActiveIdOfUser();
7484  $rset = $this->db->queryF(
7485  'SELECT obligations_answered FROM tst_pass_result WHERE active_fi = %s AND pass = %s',
7486  ['integer', 'integer'],
7487  [$active_id, self::_getPass($active_id)]
7488  );
7489 
7490  if ($row = $this->db->fetchAssoc($rset)) {
7491  $this->current_user_all_obliations_answered = (bool) ($row['obligations_answered'] ?? 0);
7492  }
7493  }
7494 
7496  }
7497 
7498  public function hasObligations(): bool
7499  {
7500  if ($this->has_obligations === null) {
7501  $rset = $this->db->queryF(
7502  'SELECT count(*) cnt FROM tst_test_question WHERE test_fi = %s AND obligatory = 1',
7503  ['integer'],
7504  [$this->getTestId()]
7505  );
7506  $row = $this->db->fetchAssoc($rset);
7507  $this->has_obligations = $row['cnt'] > 0;
7508  }
7509 
7510  return $this->has_obligations;
7511  }
7512 
7513  public function getAutosave(): bool
7514  {
7515  return $this->getMainSettings()->getQuestionBehaviourSettings()->getAutosaveEnabled();
7516  }
7517 
7518  public function isPassDeletionAllowed(): bool
7519  {
7520  return $this->getScoreSettings()->getResultSummarySettings()->getPassDeletionAllowed();
7521  }
7522 
7523  public function getEnableExamview(): bool
7524  {
7525  return $this->getMainSettings()->getFinishingSettings()->getShowAnswerOverview();
7526  }
7527 
7528  public function setActivationStartingTime(?int $starting_time = null)
7529  {
7530  $this->activation_starting_time = $starting_time;
7531  }
7532 
7533  public function setActivationEndingTime(?int $ending_time = null)
7534  {
7535  $this->activation_ending_time = $ending_time;
7536  }
7537 
7538  public function getActivationStartingTime(): ?int
7539  {
7541  }
7542 
7543  public function getActivationEndingTime(): ?int
7544  {
7546  }
7547 
7554  public function getStartingTimeOfParticipants(): array
7555  {
7556  $times = [];
7557  $result = $this->db->queryF(
7558  "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",
7559  ['integer'],
7560  [$this->getTestId()]
7561  );
7562  while ($row = $this->db->fetchAssoc($result)) {
7563  $times[$row['active_fi']] = $row['started'];
7564  }
7565  return $times;
7566  }
7567 
7568  public function getTimeExtensionsOfParticipants(): array
7569  {
7570  $times = [];
7571  $result = $this->db->queryF(
7572  "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",
7573  ['integer'],
7574  [$this->getTestId()]
7575  );
7576  while ($row = $this->db->fetchAssoc($result)) {
7577  $times[$row['active_fi']] = $row['additionaltime'];
7578  }
7579  return $times;
7580  }
7581 
7582  public function getExtraTime($active_id)
7583  {
7584  $result = $this->db->queryF(
7585  "SELECT additionaltime FROM tst_addtime WHERE active_fi = %s",
7586  ['integer'],
7587  [$active_id]
7588  );
7589  if ($result->numRows() > 0) {
7590  $row = $this->db->fetchAssoc($result);
7591  return $row['additionaltime'];
7592  }
7593  return 0;
7594  }
7595 
7596  public function addExtraTime($active_id, $minutes)
7597  {
7598  $participantData = new ilTestParticipantData($this->db, $this->lng);
7599  $participantData->setParticipantAccessFilter(
7600  $this->participant_access_filter->getManageParticipantsUserFilter($this->getRefId())
7601  );
7602 
7603  if ($active_id) {
7604  $participantData->setActiveIdsFilter([$active_id]);
7605  }
7606 
7607  $participantData->load($this->getTestId());
7608 
7609  foreach ($participantData->getActiveIds() as $active_fi) {
7610  $result = $this->db->queryF(
7611  "SELECT active_fi FROM tst_addtime WHERE active_fi = %s",
7612  ['integer'],
7613  [$active_fi]
7614  );
7615 
7616  if ($result->numRows() > 0) {
7617  $this->db->manipulateF(
7618  "DELETE FROM tst_addtime WHERE active_fi = %s",
7619  ['integer'],
7620  [$active_fi]
7621  );
7622  }
7623 
7624  $this->db->manipulateF(
7625  "INSERT INTO tst_addtime (active_fi, additionaltime, tstamp) VALUES (%s, %s, %s)",
7626  ['integer','integer','integer'],
7627  [$active_fi, $minutes, time()]
7628  );
7629 
7631  $this->logAction(sprintf($this->lng->txtlng("assessment", "log_added_extratime", ilObjAssessmentFolder::_getLogLanguage()), $minutes, $active_id));
7632  }
7633  }
7634  }
7635 
7636  public function getMaxPassOfTest(): int
7637  {
7638  $query = '
7639  SELECT MAX(tst_pass_result.pass) + 1 max_res
7640  FROM tst_pass_result
7641  INNER JOIN tst_active ON tst_active.active_id = tst_pass_result.active_fi
7642  WHERE test_fi = ' . $this->db->quote($this->getTestId(), 'integer') . '
7643  ';
7644  $res = $this->db->query($query);
7645  $data = $this->db->fetchAssoc($res);
7646  return (int) $data['max_res'];
7647  }
7648 
7649  public static function lookupExamId($active_id, $pass)
7650  {
7651  global $DIC;
7652  $ilDB = $DIC['ilDB'];
7653 
7654  $exam_id_query = 'SELECT exam_id FROM tst_pass_result WHERE active_fi = %s AND pass = %s';
7655  $exam_id_result = $ilDB->queryF($exam_id_query, [ 'integer', 'integer' ], [ $active_id, $pass ]);
7656  if ($ilDB->numRows($exam_id_result) == 1) {
7657  $exam_id_row = $ilDB->fetchAssoc($exam_id_result);
7658 
7659  if ($exam_id_row['exam_id'] != null) {
7660  return $exam_id_row['exam_id'];
7661  }
7662  }
7663 
7664  return null;
7665  }
7666 
7667  public static function buildExamId($active_id, $pass, $test_obj_id = null): string
7668  {
7669  global $DIC;
7670  $ilSetting = $DIC['ilSetting'];
7671 
7672  $inst_id = $ilSetting->get('inst_id', null);
7673 
7674  if ($test_obj_id === null) {
7675  $obj_id = self::_getObjectIDFromActiveID($active_id);
7676  } else {
7677  $obj_id = $test_obj_id;
7678  }
7679 
7680  $examId = 'I' . $inst_id . '_T' . $obj_id . '_A' . $active_id . '_P' . $pass;
7681 
7682  return $examId;
7683  }
7684 
7685  public function isShowExamIdInTestPassEnabled(): bool
7686  {
7687  return $this->getMainSettings()->getTestBehaviourSettings()->getExamIdInTestPassEnabled();
7688  }
7689 
7690  public function isShowExamIdInTestResultsEnabled(): bool
7691  {
7692  return $this->getScoreSettings()->getResultDetailsSettings()->getShowExamIdInTestResults();
7693  }
7694 
7695 
7696  public function setQuestionSetType(string $question_set_type)
7697  {
7698  $this->main_settings = $this->getMainSettings()->withGeneralSettings(
7700  ->withQuestionSetType($question_set_type)
7701  );
7702  }
7703 
7704  public function getQuestionSetType(): string
7705  {
7706  return $this->getMainSettings()->getGeneralSettings()->getQuestionSetType();
7707  }
7708 
7716  public static function lookupQuestionSetType($objId): ?string
7717  {
7718  global $DIC;
7719  $ilDB = $DIC['ilDB'];
7720 
7721  $query = "SELECT question_set_type FROM tst_tests WHERE obj_fi = %s";
7722 
7723  $res = $ilDB->queryF($query, ['integer'], [$objId]);
7724 
7725  $questionSetType = null;
7726 
7727  while ($row = $ilDB->fetchAssoc($res)) {
7728  $questionSetType = $row['question_set_type'];
7729  }
7730 
7731  return $questionSetType;
7732  }
7733 
7739  public function isFixedTest(): bool
7740  {
7741  return $this->getQuestionSetType() == self::QUESTION_SET_TYPE_FIXED;
7742  }
7743 
7749  public function isRandomTest(): bool
7750  {
7751  return $this->getQuestionSetType() == self::QUESTION_SET_TYPE_RANDOM;
7752  }
7753 
7761  public static function _lookupRandomTest($a_obj_id): bool
7762  {
7763  return self::lookupQuestionSetType($a_obj_id) == self::QUESTION_SET_TYPE_RANDOM;
7764  }
7765 
7766  public function getQuestionSetTypeTranslation(ilLanguage $lng, $questionSetType): string
7767  {
7768  switch ($questionSetType) {
7770  return $lng->txt('tst_question_set_type_fixed');
7771 
7773  return $lng->txt('tst_question_set_type_random');
7774  }
7775 
7776  throw new ilTestException('invalid question set type value given: ' . $questionSetType);
7777  }
7778 
7779  public function participantDataExist(): bool
7780  {
7781  if ($this->participantDataExist === null) {
7782  $this->participantDataExist = (bool) $this->evalTotalPersons();
7783  }
7784 
7786  }
7787 
7788  public function recalculateScores($preserve_manscoring = false)
7789  {
7790  $scoring = new ilTestScoring($this, $this->db);
7791  $scoring->setPreserveManualScores($preserve_manscoring);
7792  $scoring->recalculateSolutions();
7793  }
7794 
7795  public static function getTestObjIdsWithActiveForUserId($userId): array
7796  {
7797  global $DIC;
7798  $ilDB = $DIC['ilDB'];
7799 
7800  $query = "
7801  SELECT obj_fi
7802  FROM tst_active
7803  INNER JOIN tst_tests
7804  ON test_id = test_fi
7805  WHERE user_fi = %s
7806  ";
7807 
7808  $res = $ilDB->queryF($query, ['integer'], [$userId]);
7809 
7810  $objIds = [];
7811 
7812  while ($row = $ilDB->fetchAssoc($res)) {
7813  $objIds[] = (int) $row['obj_fi'];
7814  }
7815 
7816  return $objIds;
7817  }
7818 
7819  public function isSkillServiceEnabled(): bool
7820  {
7821  return $this->getMainSettings()->getAdditionalSettings()->getSkillsServiceEnabled();
7822  }
7823 
7835  public function isSkillServiceToBeConsidered(): bool
7836  {
7837  if (!$this->getMainSettings()->getAdditionalSettings()->getSkillsServiceEnabled()) {
7838  return false;
7839  }
7840 
7841  if (!self::isSkillManagementGloballyActivated()) {
7842  return false;
7843  }
7844 
7845  return true;
7846  }
7847 
7849 
7850  public static function isSkillManagementGloballyActivated(): ?bool
7851  {
7852  if (self::$isSkillManagementGloballyActivated === null) {
7853  $skmgSet = new ilSkillManagementSettings();
7854 
7855  self::$isSkillManagementGloballyActivated = $skmgSet->isActivated();
7856  }
7857 
7858  return self::$isSkillManagementGloballyActivated;
7859  }
7860 
7861  public function isShowGradingStatusEnabled(): bool
7862  {
7863  return $this->getScoreSettings()->getResultSummarySettings()->getShowGradingStatusEnabled();
7864  }
7865 
7866  public function isShowGradingMarkEnabled(): bool
7867  {
7868  return $this->getScoreSettings()->getResultSummarySettings()->getShowGradingMarkEnabled();
7869  }
7870 
7872  {
7873  return $this->getMainSettings()->getQuestionBehaviourSettings()->getLockAnswerOnNextQuestionEnabled();
7874  }
7875 
7877  {
7878  return $this->getMainSettings()->getQuestionBehaviourSettings()->getLockAnswerOnInstantFeedbackEnabled();
7879  }
7880 
7881  public function isForceInstantFeedbackEnabled(): ?bool
7882  {
7883  return $this->getMainSettings()->getQuestionBehaviourSettings()->getForceInstantFeedbackOnNextQuestion();
7884  }
7885 
7886  public static function isParticipantsLastPassActive(int $test_ref_id, int $user_id): bool
7887  {
7888  global $DIC;
7889  $ilDB = $DIC['ilDB'];
7890  $ilUser = $DIC['ilUser'];
7891 
7892  $test_obj = ilObjectFactory::getInstanceByRefId($test_ref_id, false);
7893 
7894  $active_id = $test_obj->getActiveIdOfUser($user_id);
7895 
7896  $test_session_factory = new ilTestSessionFactory($test_obj, $ilDB, $ilUser);
7897 
7898  // Added temporarily bugfix smeyer
7899  $test_session_factory->reset();
7900 
7901  $test_sequence_factory = new ilTestSequenceFactory($test_obj, $ilDB, $DIC->testQuestionPool()->questionInfo());
7902 
7903  $test_session = $test_session_factory->getSession($active_id);
7904  $test_sequence = $test_sequence_factory->getSequenceByActiveIdAndPass($active_id, $test_session->getPass());
7905  $test_sequence->loadFromDb();
7906 
7907  return $test_sequence->hasSequence();
7908  }
7909 
7913  public function isTestFinalBroken(): bool
7914  {
7915  return $this->testFinalBroken;
7916  }
7917 
7918  public function adjustTestSequence()
7919  {
7920  $query = "
7921  SELECT COUNT(test_question_id) cnt
7922  FROM tst_test_question
7923  WHERE test_fi = %s
7924  ORDER BY sequence
7925  ";
7926 
7927  $questRes = $this->db->queryF($query, ['integer'], [$this->getTestId()]);
7928 
7929  $row = $this->db->fetchAssoc($questRes);
7930  $questCount = $row['cnt'];
7931 
7932  if ($this->getShuffleQuestions()) {
7933  $query = "
7934  SELECT tseq.*
7935  FROM tst_active tac
7936  INNER JOIN tst_sequence tseq
7937  ON tseq.active_fi = tac.active_id
7938  WHERE tac.test_fi = %s
7939  ";
7940 
7941  $partRes = $this->db->queryF(
7942  $query,
7943  ['integer'],
7944  [$this->getTestId()]
7945  );
7946 
7947  while ($row = $this->db->fetchAssoc($partRes)) {
7948  $sequence = @unserialize($row['sequence']);
7949 
7950  if (!$sequence) {
7951  $sequence = [];
7952  }
7953 
7954  $sequence = array_filter($sequence, function ($value) use ($questCount) {
7955  return $value <= $questCount;
7956  });
7957 
7958  $num_seq = count($sequence);
7959  if ($questCount > $num_seq) {
7960  $diff = $questCount - $num_seq;
7961  for ($i = 1; $i <= $diff; $i++) {
7962  $sequence[$num_seq + $i - 1] = $num_seq + $i;
7963  }
7964  }
7965 
7966  $new_sequence = serialize($sequence);
7967 
7968  $this->db->update('tst_sequence', [
7969  'sequence' => ['clob', $new_sequence]
7970  ], [
7971  'active_fi' => ['integer', $row['active_fi']],
7972  'pass' => ['integer', $row['pass']]
7973  ]);
7974  }
7975  } else {
7976  $new_sequence = serialize($questCount > 0 ? range(1, $questCount) : []);
7977 
7978  $query = "
7979  SELECT tseq.*
7980  FROM tst_active tac
7981  INNER JOIN tst_sequence tseq
7982  ON tseq.active_fi = tac.active_id
7983  WHERE tac.test_fi = %s
7984  ";
7985 
7986  $part_rest = $this->db->queryF(
7987  $query,
7988  ['integer'],
7989  [$this->getTestId()]
7990  );
7991 
7992  while ($row = $this->db->fetchAssoc($part_rest)) {
7993  $this->db->update('tst_sequence', [
7994  'sequence' => ['clob', $new_sequence]
7995  ], [
7996  'active_fi' => ['integer', $row['active_fi']],
7997  'pass' => ['integer', $row['pass']]
7998  ]);
7999  }
8000  }
8001  }
8002 
8007  {
8008  return ilHtmlPurifierFactory::getInstanceByType('qpl_usersolution');
8009  }
8010 
8012  {
8013  if (!$this->score_settings) {
8014  $this->score_settings = $this->getScoreSettingsRepository()
8015  ->getFor($this->getTestId());
8016  }
8017  return $this->score_settings;
8018  }
8019 
8021  {
8022  if (!$this->score_settings_repo) {
8023  $this->score_settings_repo = new ilObjTestScoreSettingsDatabaseRepository($this->db);
8024  }
8026  }
8027 
8029  {
8030  if (!$this->main_settings) {
8031  $this->main_settings = $this->getMainSettingsRepository()
8032  ->getFor($this->getTestId());
8033  }
8034  return $this->main_settings;
8035  }
8036 
8038  {
8039  if (!$this->main_settings_repo) {
8040  $this->main_settings_repo = new ilObjTestMainSettingsDatabaseRepository($this->db);
8041  }
8043  }
8044 
8045  public function updateTestResultCache(int $active_id, ilAssQuestionProcessLocker $process_locker = null): void
8046  {
8047  $pass = ilObjTest::_getResultPass($active_id);
8048 
8049  if ($pass !== null) {
8050  $query = '
8051  SELECT tst_pass_result.*,
8052  tst_active.last_finished_pass
8053  FROM tst_pass_result
8054  INNER JOIN tst_active
8055  on tst_pass_result.active_fi = tst_active.active_id
8056  WHERE active_fi = %s
8057  AND pass = %s
8058  ';
8059 
8060  $result = $this->db->queryF(
8061  $query,
8062  ['integer','integer'],
8063  [$active_id, $pass]
8064  );
8065 
8066  $test_pass_result_row = $this->db->fetchAssoc($result);
8067 
8068  if (!is_array($test_pass_result_row)) {
8069  $test_pass_result_row = [];
8070  }
8071  $max = (float) ($test_pass_result_row['maxpoints'] ?? 0);
8072  $reached = (float) ($test_pass_result_row['points'] ?? 0);
8073  $percentage = ($max <= 0.0 || $reached <= 0.0) ? 0 : ($reached / $max) * 100.0;
8074  $obligations_answered = (int) ($test_pass_result_row['obligations_answered'] ?? 1);
8075 
8076  $mark = $this->mark_schema->getMatchingMark($percentage);
8077  $is_passed = $test_pass_result_row['last_finished_pass'] !== null
8078  && $pass <= $test_pass_result_row['last_finished_pass']
8079  && $mark->getPassed();
8080 
8081  $hint_count = $test_pass_result_row['hint_count'] ?? 0;
8082  $hint_points = $test_pass_result_row['hint_points'] ?? 0.0;
8083 
8084  $user_test_result_update_callback = function () use ($active_id, $pass, $max, $reached, $is_passed, $obligations_answered, $hint_count, $hint_points, $mark) {
8085  $passed_once_before = 0;
8086  $query = 'SELECT passed_once FROM tst_result_cache WHERE active_fi = %s';
8087  $res = $this->db->queryF($query, ['integer'], [$active_id]);
8088  while ($passed_once_result_row = $this->db->fetchAssoc($res)) {
8089  $passed_once_before = (int) $passed_once_result_row['passed_once'];
8090  }
8091 
8092  $passed_once = (int) ($is_passed || $passed_once_before);
8093 
8094  $this->db->manipulateF(
8095  'DELETE FROM tst_result_cache WHERE active_fi = %s',
8096  ['integer'],
8097  [$active_id]
8098  );
8099 
8100  if ($reached < 0.0) {
8101  $reached = 0.0;
8102  }
8103 
8104  $mark_short_name = $mark->getShortName();
8105  if ($mark_short_name === '') {
8106  $mark_short_name = ' ';
8107  }
8108 
8109  $mark_official_name = $mark->getOfficialName();
8110  if ($mark_official_name === '') {
8111  $mark_official_name = ' ';
8112  }
8113 
8114  $this->db->insert(
8115  'tst_result_cache',
8116  [
8117  'active_fi' => ['integer', $active_id],
8118  'pass' => ['integer', $pass ?? 0],
8119  'max_points' => ['float', $max],
8120  'reached_points' => ['float', $reached],
8121  'mark_short' => ['text', $mark_short_name],
8122  'mark_official' => ['text', $mark_official_name],
8123  'passed_once' => ['integer', $passed_once],
8124  'passed' => ['integer', (int) $is_passed],
8125  'failed' => ['integer', (int) !$is_passed],
8126  'tstamp' => ['integer', time()],
8127  'hint_count' => ['integer', $hint_count],
8128  'hint_points' => ['float', $hint_points],
8129  'obligations_answered' => ['integer', $obligations_answered]
8130  ]
8131  );
8132  };
8133 
8134  if (is_object($process_locker)) {
8135  $process_locker->executeUserTestResultUpdateLockOperation($user_test_result_update_callback);
8136  } else {
8137  $user_test_result_update_callback();
8138  }
8139  }
8140  }
8141 
8142  public function updateTestPassResults(
8143  int $active_id,
8144  int $pass,
8145  bool $obligations_enabled = false,
8146  ilAssQuestionProcessLocker $process_locker = null,
8147  int $test_obj_id = null
8148  ): array {
8150  $time = ilObjTest::_getWorkingTimeOfParticipantForPass($active_id, $pass);
8151 
8152  $result = $this->db->queryF(
8153  '
8154  SELECT SUM(points) reachedpoints,
8155  SUM(hint_count) hint_count,
8156  SUM(hint_points) hint_points,
8157  COUNT(DISTINCT(question_fi)) answeredquestions
8158  FROM tst_test_result
8159  WHERE active_fi = %s
8160  AND pass = %s
8161  ',
8162  ['integer','integer'],
8163  [$active_id, $pass]
8164  );
8165 
8166  if ($result->numRows() > 0) {
8167  if ($obligations_enabled) {
8168  $query = '
8169  SELECT answered answ
8170  FROM tst_test_question
8171  INNER JOIN tst_active
8172  ON active_id = %s
8173  AND tst_test_question.test_fi = tst_active.test_fi
8174  LEFT JOIN tst_test_result
8175  ON tst_test_result.active_fi = %s
8176  AND tst_test_result.pass = %s
8177  AND tst_test_question.question_fi = tst_test_result.question_fi
8178  WHERE obligatory = 1';
8179 
8180  $result_obligatory = $this->db->queryF(
8181  $query,
8182  ['integer','integer','integer'],
8183  [$active_id, $active_id, $pass]
8184  );
8185 
8186  $obligations_answered = 1;
8187 
8188  while ($row_obligatory = $this->db->fetchAssoc($result_obligatory)) {
8189  if (!(int) $row_obligatory['answ']) {
8190  $obligations_answered = 0;
8191  break;
8192  }
8193  }
8194  } else {
8195  $obligations_answered = 1;
8196  }
8197 
8198  $row = $this->db->fetchAssoc($result);
8199 
8200  if ($row['reachedpoints'] === null
8201  || $row['reachedpoints'] < 0.0) {
8202  $row['reachedpoints'] = 0.0;
8203  }
8204  if ($row['hint_count'] === null) {
8205  $row['hint_count'] = 0;
8206  }
8207  if ($row['hint_points'] === null) {
8208  $row['hint_points'] = 0.0;
8209  }
8210 
8211  $exam_identifier = ilObjTest::buildExamId($active_id, $pass, $test_obj_id);
8212 
8213  $update_pass_result_callback = function () use ($data, $active_id, $pass, $row, $time, $obligations_answered, $exam_identifier) {
8214  $this->db->replace(
8215  'tst_pass_result',
8216  [
8217  'active_fi' => ['integer', $active_id],
8218  'pass' => ['integer', $pass]
8219  ],
8220  [
8221  'points' => ['float', $row['reachedpoints']],
8222  'maxpoints' => ['float', $data['points']],
8223  'questioncount' => ['integer', $data['count']],
8224  'answeredquestions' => ['integer', $row['answeredquestions']],
8225  'workingtime' => ['integer', $time],
8226  'tstamp' => ['integer', time()],
8227  'hint_count' => ['integer', $row['hint_count']],
8228  'hint_points' => ['float', $row['hint_points']],
8229  'obligations_answered' => ['integer', $obligations_answered],
8230  'exam_id' => ['text', $exam_identifier]
8231  ]
8232  );
8233  };
8234 
8235  if (is_object($process_locker) && $process_locker instanceof ilAssQuestionProcessLocker) {
8236  $process_locker->executeUserPassResultUpdateLockOperation($update_pass_result_callback);
8237  } else {
8238  $update_pass_result_callback();
8239  }
8240  }
8241 
8242  $this->updateTestResultCache($active_id, $process_locker);
8243 
8244  return [
8245  'active_fi' => $active_id,
8246  'pass' => $pass,
8247  'points' => $row['reachedpoints'],
8248  'maxpoints' => $data['points'],
8249  'questioncount' => $data['count'],
8250  'answeredquestions' => $row['answeredquestions'],
8251  'workingtime' => $time,
8252  'tstamp' => time(),
8253  'hint_count' => $row['hint_count'],
8254  'hint_points' => $row['hint_points'],
8255  'obligations_answered' => $obligations_answered,
8256  'exam_id' => $exam_identifier
8257  ];
8258  }
8259 
8260  public function resetMarkSchema(): void
8261  {
8262  $this->mark_schema->flush();
8263  }
8264 
8265  public function addToNewsOnOnline(
8266  bool $old_online_status,
8267  bool $new_online_status
8268  ): void {
8269  if (!$old_online_status && $new_online_status) {
8270  $newsItem = new ilNewsItem();
8271  $newsItem->setContext($this->getId(), 'tst');
8272  $newsItem->setPriority(NEWS_NOTICE);
8273  $newsItem->setTitle('new_test_online');
8274  $newsItem->setContentIsLangVar(true);
8275  $newsItem->setContent('');
8276  $newsItem->setUserId($this->user->getId());
8277  $newsItem->setVisibility(NEWS_USERS);
8278  $newsItem->create();
8279  return;
8280  }
8281 
8282  if ($old_online_status && !$new_online_status) {
8283  ilNewsItem::deleteNewsOfContext($this->getId(), 'tst');
8284  return;
8285  }
8286 
8287  $newsId = ilNewsItem::getFirstNewsIdForContext($this->getId(), 'tst');
8288  if (!$new_online_status && $newsId > 0) {
8289  $newsItem = new ilNewsItem($newsId);
8290  $newsItem->setTitle('new_test_online');
8291  $newsItem->setContentIsLangVar(true);
8292  $newsItem->setContent('');
8293  $newsItem->update();
8294  }
8295  }
8296 }
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...
withScoringSettings(ilObjTestSettingsScoring $settings)
static isParticipantsLastPassActive(int $test_ref_id, int $user_id)
string $title
setQuestionSetType(string $question_set_type)
xslt_create()
& getWorkedQuestions($active_id, $pass=null)
Gets the id&#39;s of all questions a user already worked through.
addConcludingRemarksToSettingsFromImport(ilObjTestSettingsFinishing $settings, array $material, array $mappings)
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()
getScoreReporting()
Gets the score reporting of the ilObjTest object.
getTimeExtensionsOfParticipants()
isHTML($a_text)
Checks if a given string contains HTML or not.
inviteRole($role_id)
Invites all users of a role to a test.
static _getWorkingTimeOfParticipantForPass($active_id, $pass)
Returns the complete working time in seconds for a test participant.
ILIAS $ilias
ilObjTestScoreSettings $score_settings
$res
Definition: ltiservices.php:69
isBlockPassesAfterPassedEnabled()
isTestFinished($active_id)
returns if the active for user_id has been submitted
MainSettingsRepository $main_settings_repo
Readable part of repository interface to ilComponentDataDB.
getHighscoreOwnTable()
Gets if the own rankings table should be shown.
int $activation_starting_time
getHighscoreTopNum(int $a_retval=10)
Gets the number of entries which are to be shown in the top-rankings table.
static _addLog( $user_id, $object_id, $logtext, $question_id=0, $original_id=0, $test_only=false, $test_ref_id=0)
Add an assessment log entry.
exportFileItems($target_dir, &$expLog)
export files of file itmes
static _getObjectIDFromTestID($test_id)
Returns the ILIAS test object id for a given test id.
deliverPDFfromFO($fo, $title=null)
Delivers a PDF file from a XSL-FO string.
getReportingDate()
Gets the reporting date of the ilObjTest object.
evalTotalPersonsArray($name_sort_order="asc")
Returns all persons who started the test.
ilComponentRepository $component_repository
getProcessingTimeAsMinutes()
static _getPass($active_id)
Retrieves the actual pass of a given user for a given test.
bool $current_user_all_obliations_answered
evalTotalParticipantsArray($name_sort_order="asc")
Returns all participants who started the test.
static _getParticipantData($active_id)
Retrieves a participant name from active id.
removeTestResultsByUserIds($userIds)
& getTestResult(int $active_id, ?int $pass=null, bool $ordered_sequence=false, bool $considerHiddenQuestions=true, bool $considerOptionalQuestions=true)
Calculates the results of a test for a given user and returns an array with all test results...
static isQuestionObligationPossible(int $question_id)
const ILIAS_VERSION
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()
setQuestionOrderAndObligations($orders, $obligations)
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
Class ilObjTestGUI.
checkQuestionParent($questionId)
saveCompleteStatus(ilTestQuestionSetConfig $testQuestionSetConfig)
static completeMissingPluginName($question_type_data)
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...
withGamificationSettings(ilObjTestSettingsGamification $settings)
const QUESTION_SET_TYPE_RANDOM
static _getSuggestedSolutionOutput(int $question_id)
static _lookupRandomTest($a_obj_id)
Returns the fact wether the test with passed obj id is a random questions test or not...
& getParticipants()
Returns all persons who started the test.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
getCompleteWorkingTimeOfParticipant($active_id)
Returns the complete working time in seconds for a test participant.
This class handles all operations on files (attachments) in directory ilias_data/mail.
setActivationStartingTime(?int $starting_time=null)
qtiMaterialToArray($a_material)
Reads an QTI material tag and creates a text string.
static _getAvailableQuestionpools( $use_object_id=false, $equal_points=false, $could_be_offline=false, $showPath=false, $with_questioncount=false, $permission='read', $usr_id='')
Returns the available question pools for the active user.
if(! $DIC->user() ->getId()||!ilLTIConsumerAccess::hasCustomProviderCreationAccess()) $params
Definition: ltiregstart.php:33
Abstract basic class which is to be extended by the concrete assessment question type classes...
getShowSolutionListOwnAnswers()
getShowSolutionFeedback()
Returns if the feedback should be presented to the solution or not.
Class ChatMainBarProvider .
xslt_free(&$proc)
InternalRequestService $testrequest
static _getPassScoring($active_id)
Gets the pass scoring type.
static lookupPassResultsUpdateTimestamp($active_id, $pass)
& getExistingQuestions($pass=null)
Get the id&#39;s of the questions which are already part of the test.
bool $testFinalBroken
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)
insertQuestion(ilTestQuestionSetConfig $testQuestionSetConfig, $question_id, $linkOnly=false)
Insert a question in the list of questions.
getQuestionSetTypeTranslation(ilLanguage $lng, $questionSetType)
getHighscoreAchievedTS()
Returns if date and time of the scores achievement should be displayed.
getTestId()
Gets the database id of the additional test data.
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 ...
getAvailableQuestionpools($use_object_id=false, $equal_points=false, $could_be_offline=false, $show_path=false, $with_questioncount=false, $permission="read")
Returns the available question pools for the active user.
createQuestionGUI($question_type, $question_id=-1)
Creates a question GUI instance of a given question type.
& getQuestionTitlesAndIndexes()
Returns the titles of the test questions in question sequence.
static formatDate(ilDateTime $date, bool $a_skip_day=false, bool $a_include_wd=false, bool $include_seconds=false)
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.
logManualFeedback($active_id, $question_id, $feedback)
Creates a log for the manual feedback.
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
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
& 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.
& _evalResultsOverview($test_id)
Creates an associated array with the results of all participants of a test.
& getQuestionsOfTest($active_id)
Retrieves all the assigned questions for all test passes of a test participant.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
$objId
Definition: xapitoken.php:57
$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.
& getInvitedUsers(int $user_id=0, $order="login, lastname, firstname")
Returns a list of all invited users in a test.
toXML()
Returns a QTI xml representation of the test.
ilCtrlInterface $ctrl
setTemplate(int $template_id)
setActivationLimited($a_value)
TableGUI class for evaluation of all users.
static isManScoringDone(int $active_id)
setActivationEndingTime(?int $ending_time=null)
setTestId($a_id)
Sets the test ID.
isShowExamIdInTestResultsEnabled()
_getVisitTimeOfParticipant($test_id, $active_id)
Returns the first and last visit of a participant.
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.
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)
static getStyleSheetLocation(string $mode="output", string $a_css_name="", string $a_css_location="")
get full style sheet file name (path inclusive) of current user
getActiveIdOfUser($user_id="", $anonymous_id="")
Gets the active id of a given user.
static _getObjectIDFromActiveID($active_id)
Returns the ILIAS test object id for a given active id.
$participantDataExist
holds the fact wether participant data exists or not DO NOT USE TIS PROPERTY DRIRECTLY ALWAYS USE ilO...
getPassed($active_id)
addIntroductionToSettingsFromImport(ilObjTestSettingsIntroduction $settings, array $material, array $mappings)
static _isPassed($user_id, $a_obj_id)
Returns TRUE if the user with the user id $user_id passed the test with the object id $a_obj_id...
buildName(?int $user_id, ?string $firstname, ?string $lastname)
Builds a user name for the output depending on test type and existence of the user.
static _getBestPass($active_id)
Retrieves the best pass of a given user for a given test.
evalTotalStartedAverageTime(?array $active_ids_to_filter=null)
replaceFilesInPageImports(string $text, $mappings)
saveToDb(bool $properties_only=false)
static _lookupTestObjIdForQuestionId(int $q_id)
Get test Object ID for question ID.
static _lookupAnonymity($a_obj_id)
Refinery $refinery
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.
startWorkingTime($active_id, $pass)
Write the initial entry for the tests working time to the database.
static removeTrailingPathSeparators(string $path)
isTestQuestion($questionId)
disinviteUser($user_id)
Disinvites a user from a test.
Test sequence handler.
bool $has_obligations
$evaluation_data
Contains the evaluation data settings the tutor defines for the user.
int $tmpCopyWizardCopyId
array $questions
& getQuestionTitles()
Returns the titles of the test questions in question sequence.
static _lookupObjId(int $ref_id)
& evalResultsOverview()
Creates an associated array with the results of all participants of a test.
ilBenchmark $bench
static _instanciateQuestion($question_id)
Creates an instance of a question with a given question id.
& _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.
checkMarks()
{boolean|string True or an error string which can be used for display purposes}
global $DIC
Definition: feed.php:28
static isQuestionObligatory($question_id)
checks wether the question with given id is marked as obligatory or not
static _getUserIdFromActiveId(int $active_id)
TestManScoringDoneHelper $testManScoringDoneHelper
saveManualFeedback(int $active_id, int $question_id, int $pass, ?string $feedback, bool $finalized=false, bool $is_single_feedback=false)
updateWorkingTime($times_id)
Update the working time of a test when a question is answered.
getFixedQuestionSetTotalPoints()
xslt_error(&$proc)
getHtmlQuestionContentPurifier()
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
updateTestPassResults(int $active_id, int $pass, bool $obligations_enabled=false, ilAssQuestionProcessLocker $process_locker=null, int $test_obj_id=null)
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.
getMarkSchemaForeignId()
{int}
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.
removeTestResults(ilTestParticipantData $participantData)
Interface for html sanitizing functionality.
removeQuestions(array $removeQuestionIds)
evalStatistical($active_id)
Returns the statistical evaluation of the test for a specified user.
static getSingleManualFeedback(int $active_id, int $question_id, int $pass)
getHighscoreHints()
Gets, if the column with the number of requested hints should be shown.
cloneMetaData(ilObject $target_obj)
Copy meta data.
getAccessFilteredParticipantList()
getQuestiontext($question_id)
Returns the question text for a given question.
replaceMobsInPageImports(string $text, array $mappings)
__construct(VocabulariesInterface $vocabularies)
getVisitTimeOfParticipant($active_id)
Returns the first and last visit of a participant.
static _lookupClientIP(int $a_user_id)
static _lookupTitle(int $obj_id)
getAllTestResults($participants, $prepareForCSV=true)
returns all test results for all participants
getHighscoreAnon()
Gets if the highscores should be anonymized per setting.
sendAdvancedNotification(int $active_id)
getTestParticipantsForManualScoring($filter=null)
static _getQuestionCountAndPointsForPassOfParticipant($active_id, $pass)
fromXML(ilQTIAssessment $assessment, array $mappings)
Receives parameters from a QTI parser and creates a valid ILIAS test object.
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.
static _getScoreCutting($active_id)
Determines if the score of a question should be cut at 0 points or the score of the whole test...
getStartingTimeOfUser($active_id, $pass=null)
Returns the unix timestamp of the time a user started a test.
sendSimpleNotification($active_id)
getShowSolutionPrintview()
Returns if the solution printview should be presented to the user or not.
updateTestResultCache(int $active_id, ilAssQuestionProcessLocker $process_locker=null)
hasNrOfTriesRestriction()
returns if the numbers of tries have to be checked
addExtraTime($active_id, $minutes)
Class HTTPServicesTest.
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.
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
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
saveToDb(int $test_id)
const REDIRECT_NONE
static getInstanceByRefId(int $ref_id, bool $stop_on_error=true)
get an instance of an Ilias object by reference id
getEvaluationAdditionalFields()
Gets additional user fields that should be shown in the user evaluation.
static _getSolutionMaxPass(int $question_id, int $active_id)
Returns the maximum pass a users question solution.
& getAllQuestions($pass=null)
Returns all questions of a test in test order.
isSkillServiceToBeConsidered()
Returns whether this test must consider skills, usually by providing appropriate extensions in the us...
Class ilObjFile.
exportXMLMetaData(&$a_xml_writer)
export content objects meta data to xml (see ilias_co.dtd)
static _getCountSystem($active_id)
static _getSolvedQuestions($active_id, $question_fi=null)
get solved questions
getResultsForActiveId(int $active_id)
string $key
Consumer key/client ID value.
Definition: System.php:193
ilTestEditPageGUI: ilPageEditorGUI, ilEditClipboardGUI, ilMDEditorGUI ilTestEditPageGUI: ilPublicUse...
applyDefaults($test_defaults)
Applies given test defaults to this test.
isOfferingQuestionHintsEnabled()
removeTestActives($activeIds)
Filesystem $filesystem_web
exportXMLMediaObjects(&$a_xml_writer, $a_inst, $a_target_dir, &$expLog)
export media objects to xml (see ilias_co.dtd)
header include for all ilias files.
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...
allObligationsAnswered()
checks wether all questions marked as obligatory were answered within the test pass with given testId...
removeTestResultsFromSoapLpAdministration($userIds)
getMarkSchema()
{ASS_MarkSchema}
getAggregatedResultsData()
Returns the aggregated test results.
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.
logAction($logtext="", $question_id=0)
Logs an action into the Test&Assessment log.
$results
getStartingTimeOfParticipants()
Note, this function should only be used if absolutely necessary, since it perform joins on tables tha...
static getDataDir()
get data directory (outside webspace)
getShowSolutionSignature()
Returns if the signature field should be shown in the test results.
Basic GUI class for assessment questions.
$txt
Definition: error.php:14
bool $activation_limited
static _exists(int $id, bool $reference=false, ?string $type=null)
Class ilTestScoring.
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="-")
static _lookupDescription(int $obj_id)
const INVITATION_OFF
static _getTestIDFromObjectID($object_id)
Returns the ILIAS test id for a given object id.
getInstantFeedbackSolution()
ASS_MarkSchema $mark_schema
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.
removeQuestionFromSequences($questionId, $activeIds, ilTestReindexedSequencePositionMap $reindexedSequencePositionMap)
isHighscoreAnon()
Gets if the highscores should be displayed anonymized.
inviteGroup($group_id)
Invites all users of a group to a test.
$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...
duplicateQuestionForTest($question_id)
Takes a question and creates a copy of the question for use in the test.
getAvailableQuestions($arr_filter, $completeonly=0)
Calculates the available questions for a test.
isForceInstantFeedbackEnabled()
& getTotalPointsPassedArray()
Returns an array with the total points of all users who passed the test This array could be used for ...
static _saveTempFileAsMediaObject(string $name, string $tmp_name, bool $upload=true)
Create new media object and update page in db and return new media object.
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.
processCSVRow(mixed $row, bool $quote_all=false, string $separator=";")
Processes an array as a CSV row and converts the array values to correct CSV values.
A news item can be created by different sources.
getGenericAnswerFeedback()
addToNewsOnOnline(bool $old_online_status, bool $new_online_status)
static getItem(int $ref_id)
buildIso8601PeriodForExportCompatibility(DateTimeImmutable $date_time)
int $activation_ending_time
getTextAnswer($active_id, $question_id, $pass=null)
Returns the text answer of a given user for a given question.
getScoreCutting()
Determines if the score of a question should be cut at 0 points or the score of the whole test...
getTitleFilenameCompliant()
returns the object title prepared to be used as a filename
bool $activation_visibility
getParticipantsForTestAndQuestion($test_id, $question_id)
Creates an associated array with all active id&#39;s for a given test and original question id...
setTmpCopyWizardCopyId(int $tmpCopyWizardCopyId)
static _getManualScoring()
Retrieve the manual scoring settings.
global $ilSetting
Definition: privfeed.php:18
static _getAvailableTests($use_object_id=false)
Returns the available tests for the active user.
getExtraTime($active_id)
moveQuestions($move_questions, $target_index, $insert_mode)
Move questions to another position.
static getFirstNewsIdForContext(int $a_context_obj_id, string $a_context_obj_type, int $a_context_sub_obj_id=0, string $a_context_sub_obj_type="")
Get first new id of news set related to a certain context.
static getCompleteManualFeedback(int $question_id)
Retrieves the manual feedback for a question in a test.
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)
static dic()
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...
$id
plugin.php for ilComponentBuildPluginInfoObjectiveTest::testAddPlugins
Definition: plugin.php:23
const QUESTION_SET_TYPE_FIXED
getConcludingRemarksPageId()
withConcludingRemarksPageId(?int $concluding_remarks_page_id)
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.
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.
string $author
Class ilObjGroup.
removeTestResultsByActiveIds($activeIds)
ilObjTestMainSettings $main_settings
& getCompleteWorkingTimeOfParticipants()
Returns the complete working time in seconds for all test participants.
& getCompleteEvaluationData($withStatistics=true, $filterby="", $filtertext="")
getProcessingTimeInSeconds($active_id="")
Returns the processing time for the test in seconds.
getNrOfResultsForPass($active_id, $pass)
Calculates the number of user results for a specific test pass.
ilComponentFactory $component_factory
setClientIP($user_id, $client_ip)
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()
A class defining mark schemas for assessment test objects.
canShowSolutionPrintview($user_id=null)
getQuestionCountWithoutReloading()
getHighscoreScore()
Gets if the score column should be shown.
ILIAS TestQuestionPool QuestionInfoService $questioninfo
static getTestObjIdsWithActiveForUserId($userId)
isPluginActive($a_pname)
Checks wheather or not a question plugin with a given name is active.
isFollowupQuestionAnswerFixationEnabled()
& evalResultsOverviewOfParticipant($active_id)
Creates an associated array with the results for a given participant of a test.
saveAuthorToMetadata($author="")
Saves an authors name into the lifecycle metadata if no lifecycle metadata exists This will only be c...
moveQuestionAfterOLD($previous_question_id, $new_question_id)
static getManualFeedback(int $active_id, int $question_id, ?int $pass)
Retrieves the feedback comment for a question in a test if it is finalized.
moveQuestionAfter($question_to_move, $question_before)
getScoreSettingsRepository()
static insertInstIntoID(string $a_value)
inserts installation id into ILIAS id
retrieveMobsFromLegacyImports(string $text, array $mobs)
static lookupQuestionSetType($objId)
lookup-er for question set type
static prepareTextareaOutput(string $txt_output, bool $prepare_for_latex_output=false, bool $omitNl2BrWhenTextArea=false)
Prepares a string for a text area output where latex code may be in it If the text is HTML-free...
static lookupQuestionSetTypeByActiveId($active_id)
returns the question set type of test relating to passed active id
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
& getQuestionsOfPass($active_id, $pass)
Retrieves all the assigned questions for a test participant in a given test pass. ...
Class ilObjectActivation.
_getLastAccess(int $active_id)
isComplete(ilTestQuestionSetConfig $testQuestionSetConfig)
setAccessFilteredParticipantList(ilTestParticipantList $access_filtered_participant_list)
static set(string $a_var, $a_val)
Set a value.
withIntroductionPageId(?int $introduction_page_id)
static deleteRequestsByActiveIds($activeIds)
Deletes all hint requests relating to a testactive included in given active ids.
insertManualFeedback(int $active_id, int $question_id, int $pass, ?string $feedback, bool $finalized, array $feedback_old)
withGeneralSettings(ilObjTestSettingsGeneral $settings)
const REDIRECT_KIOSK
reindexFixedQuestionOrdering()
const SCORE_BEST_PASS
static _setImportDirectory($a_import_dir=null)
ilTestParticipantAccessFilterFactory $participant_access_filter
userLookupFullName($user_id, $overwrite_anonymity=false, $sorted_order=false, $suffix="")
Returns the full name of a test user according to the anonymity status.
static _getActiveIdOfUser($user_id="", $test_id="")
static makeDir(string $a_dir)
creates a new directory and inherits all filesystem permissions of the parent directory You may pass ...
isMaxProcessingTimeReached(int $starting_time, int $active_id)
Returns whether the maximum processing time for a test is reached or not.
static _getImportDirectory()
Get the import directory location of the test.
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 getFeedbackClassNameByQuestionType(string $questionType)
static lookupExamId($active_id, $pass)
getPassScoring()
Gets the pass scoring type.