ILIAS  trunk Revision v11.0_alpha-2638-g80c1d007f79
class.ilTestSequence.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
22 
33 {
37  public array $sequencedata;
38 
42  public array $questions;
43  protected array $alreadyPresentedQuestions = [];
44  protected int $newlyPresentedQuestion = 0;
45 
49  protected array $alreadyCheckedQuestions = [];
50  protected ?int $newlyCheckedQuestion = null;
51  /*
52  * @var array<int>
53  */
54  protected array $optionalQuestions = [];
56  private bool $considerHiddenQuestionsEnabled = false;
57  private bool $considerOptionalQuestionsEnabled = true;
58 
66  public function __construct(
67  protected ilDBInterface $db,
68  protected int $active_id,
69  protected int $pass,
70  protected GeneralQuestionPropertiesRepository $questionrepository
71  ) {
72  $this->sequencedata = [
73  "sequence" => [],
74  "postponed" => [],
75  "hidden" => []
76  ];
77  }
78 
79  public function getActiveId(): int
80  {
81  return $this->active_id;
82  }
83 
84  public function createNewSequence(int $max, bool $shuffle): void
85  {
86  $newsequence = [];
87  if ($max > 0) {
88  for ($i = 1; $i <= $max; $i++) {
89  $newsequence[] = $i;
90  }
91  if ($shuffle) {
92  $newsequence = $this->pcArrayShuffle($newsequence);
93  }
94  }
95  $this->sequencedata["sequence"] = $newsequence;
96  }
97 
101  public function loadQuestions(): void
102  {
103  $this->questions = [];
104 
105  $result = $this->db->queryF(
106  "SELECT tst_test_question.* FROM tst_test_question, qpl_questions, tst_active WHERE tst_active.active_id = %s AND tst_test_question.test_fi = tst_active.test_fi AND qpl_questions.question_id = tst_test_question.question_fi ORDER BY tst_test_question.sequence",
107  ['integer'],
108  [$this->active_id]
109  );
110 
111  $index = 1;
112 
113  // TODO bheyser: There might be "sequence" gaps which lead to issues with tst_sequence when deleting/adding questions before any participant starts the test
114  while ($data = $this->db->fetchAssoc($result)) {
115  $this->questions[$index++] = $data["question_fi"];
116  }
117  }
118 
122  public function loadFromDb(): void
123  {
124  $this->loadQuestionSequence();
125  $this->loadPresentedQuestions();
126  $this->loadCheckedQuestions();
127  $this->loadOptionalQuestions();
128  }
129 
130  private function loadQuestionSequence(): void
131  {
132  $result = $this->db->queryF(
133  "SELECT * FROM tst_sequence WHERE active_fi = %s AND pass = %s",
134  ['integer','integer'],
135  [$this->active_id, $this->pass]
136  );
137  if ($result->numRows()) {
138  $row = $this->db->fetchAssoc($result);
139  $this->sequencedata = [
140  "sequence" => unserialize($row["sequence"] ?? '', ['allowed_classes' => false]),
141  "postponed" => unserialize($row["postponed"] ?? '', ['allowed_classes' => false]),
142  "hidden" => unserialize($row["hidden"] ?? '', ['allowed_classes' => false])
143  ];
144 
145  if (!is_array($this->sequencedata["sequence"])) {
146  $this->sequencedata["sequence"] = [];
147  }
148  if (!is_array($this->sequencedata["postponed"])) {
149  $this->sequencedata["postponed"] = [];
150  }
151  if (!is_array($this->sequencedata["hidden"])) {
152  $this->sequencedata["hidden"] = [];
153  }
154 
155  $this->setAnsweringOptionalQuestionsConfirmed((bool) $row['ans_opt_confirmed']);
156  }
157  }
158 
159  protected function loadPresentedQuestions(): void
160  {
161  $res = $this->db->queryF(
162  "SELECT question_fi FROM tst_seq_qst_presented WHERE active_fi = %s AND pass = %s",
163  ['integer','integer'],
164  [$this->active_id, $this->pass]
165  );
166 
167  while ($row = $this->db->fetchAssoc($res)) {
168  $this->alreadyPresentedQuestions[ $row['question_fi'] ] = $row['question_fi'];
169  }
170  }
171 
172  private function loadCheckedQuestions(): void
173  {
174  $res = $this->db->queryF(
175  "SELECT question_fi FROM tst_seq_qst_checked WHERE active_fi = %s AND pass = %s",
176  ['integer','integer'],
177  [$this->active_id, $this->pass]
178  );
179 
180  while ($row = $this->db->fetchAssoc($res)) {
181  $this->alreadyCheckedQuestions[ $row['question_fi'] ] = $row['question_fi'];
182  }
183  }
184 
185  private function loadOptionalQuestions(): void
186  {
187  $res = $this->db->queryF(
188  "SELECT question_fi FROM tst_seq_qst_optional WHERE active_fi = %s AND pass = %s",
189  ['integer','integer'],
190  [$this->active_id, $this->pass]
191  );
192 
193  while ($row = $this->db->fetchAssoc($res)) {
194  $this->optionalQuestions[ $row['question_fi'] ] = $row['question_fi'];
195  }
196  }
197 
203  public function saveToDb(): void
204  {
205  $this->saveQuestionSequence();
207  $this->saveNewlyCheckedQuestion();
208  $this->saveOptionalQuestions();
209  }
210 
211  private function saveQuestionSequence(): void
212  {
213  $postponed = null;
214  if ((is_array($this->sequencedata["postponed"])) && (count($this->sequencedata["postponed"]))) {
215  $postponed = serialize($this->sequencedata["postponed"]);
216  }
217  $hidden = null;
218  if ((is_array($this->sequencedata["hidden"])) && (count($this->sequencedata["hidden"]))) {
219  $hidden = serialize($this->sequencedata["hidden"]);
220  }
221 
222  $this->db->manipulateF(
223  "DELETE FROM tst_sequence WHERE active_fi = %s AND pass = %s",
224  ['integer','integer'],
225  [$this->active_id, $this->pass]
226  );
227 
228  $this->db->insert("tst_sequence", [
229  "active_fi" => ["integer", $this->active_id],
230  "pass" => ["integer", $this->pass],
231  "sequence" => ["clob", serialize($this->sequencedata["sequence"])],
232  "postponed" => ["text", $postponed],
233  "hidden" => ["text", $hidden],
234  "tstamp" => ["integer", time()],
235  'ans_opt_confirmed' => ['integer', (int) $this->isAnsweringOptionalQuestionsConfirmed()]
236  ]);
237  }
238 
239  protected function saveNewlyPresentedQuestion(): void
240  {
241  if ($this->newlyPresentedQuestion) {
242  $this->db->replace('tst_seq_qst_presented', [
243  'active_fi' => ['integer', $this->active_id],
244  'pass' => ['integer', $this->pass],
245  'question_fi' => ['integer', $this->newlyPresentedQuestion]
246  ], []);
247  }
248  }
249 
250  private function saveNewlyCheckedQuestion(): void
251  {
252  if ((int) $this->newlyCheckedQuestion) {
253  $this->db->replace('tst_seq_qst_checked', [
254  'active_fi' => ['integer', $this->active_id],
255  'pass' => ['integer', $this->pass],
256  'question_fi' => ['integer', (int) $this->newlyCheckedQuestion]
257  ], []);
258  }
259  }
260 
261  private function saveOptionalQuestions(): void
262  {
263  $NOT_IN_questions = $this->db->in('question_fi', $this->optionalQuestions, true, 'integer');
264 
265  $this->db->queryF(
266  "DELETE FROM tst_seq_qst_optional WHERE active_fi = %s AND pass = %s AND $NOT_IN_questions",
267  ['integer', 'integer'],
268  [$this->active_id, $this->pass]
269  );
270 
271  foreach ($this->optionalQuestions as $questionId) {
272  $this->db->replace('tst_seq_qst_optional', [
273  'active_fi' => ['integer', $this->active_id],
274  'pass' => ['integer', $this->pass],
275  'question_fi' => ['integer', (int) $questionId]
276  ], []);
277  }
278  }
279 
280  public function postponeQuestion(int $question_id): void
281  {
282  if (!$this->isPostponedQuestion($question_id)) {
283  $this->sequencedata["postponed"][] = $question_id;
284  }
285  }
286 
287  public function hideQuestion(int $question_id): void
288  {
289  if (!$this->isHiddenQuestion($question_id)) {
290  $this->sequencedata["hidden"][] = $question_id;
291  }
292  }
293 
294  public function isPostponedQuestion(int $question_id): bool
295  {
296  if (!is_array($this->sequencedata["postponed"])) {
297  return false;
298  }
299  if (!in_array($question_id, $this->sequencedata["postponed"])) {
300  return false;
301  }
302 
303  return true;
304  }
305 
306  public function isHiddenQuestion(int $question_id): bool
307  {
308  if (!is_array($this->sequencedata["hidden"])) {
309  return false;
310  }
311  if (!in_array($question_id, $this->sequencedata["hidden"])) {
312  return false;
313  }
314 
315  return true;
316  }
317 
318  public function isPostponedSequence(int $sequence): bool
319  {
320  if (!array_key_exists($sequence, $this->questions)) {
321  return false;
322  }
323  if (!is_array($this->sequencedata["postponed"])) {
324  return false;
325  }
326  if (!in_array($this->questions[$sequence], $this->sequencedata["postponed"])) {
327  return false;
328  }
329 
330  return true;
331  }
332 
333  public function isHiddenSequence(int $sequence): bool
334  {
335  if (!array_key_exists($sequence, $this->questions)) {
336  return false;
337  }
338  if (!is_array($this->sequencedata["hidden"])) {
339  return false;
340  }
341  if (!in_array($this->questions[$sequence], $this->sequencedata["hidden"])) {
342  return false;
343  }
344  return true;
345  }
346 
347  public function postponeSequence(int $sequence): void
348  {
349  if (!$this->isPostponedSequence($sequence)) {
350  if (array_key_exists($sequence, $this->questions)) {
351  if (!is_array($this->sequencedata["postponed"])) {
352  $this->sequencedata["postponed"] = [];
353  }
354  $this->sequencedata["postponed"][] = (int) $this->questions[$sequence];
355  }
356  }
357  }
358 
359  public function hideSequence(int $sequence): void
360  {
361  if (!$this->isHiddenSequence($sequence)) {
362  if (array_key_exists($sequence, $this->questions)) {
363  if (!is_array($this->sequencedata["hidden"])) {
364  $this->sequencedata["hidden"] = [];
365  }
366  $this->sequencedata["hidden"][] = (int) $this->questions[$sequence];
367  }
368  }
369  }
370 
371  public function setQuestionPresented(int $question_id): void
372  {
373  $this->newlyPresentedQuestion = $question_id;
374  }
375 
376  public function isQuestionPresented(int $question_id): bool
377  {
378  return (
379  $this->newlyPresentedQuestion == $question_id || in_array($question_id, $this->alreadyPresentedQuestions)
380  );
381  }
382 
383  public function isNextQuestionPresented(int $question_id): bool
384  {
385  $next_question_id = $this->getQuestionForSequence(
386  $this->getNextSequence($this->getSequenceForQuestion($question_id)) ?? 0
387  );
388 
389  if ($next_question_id === null) {
390  return false;
391  }
392 
393  if ($this->newlyPresentedQuestion === $next_question_id) {
394  return true;
395  }
396 
397  if (in_array($next_question_id, $this->alreadyPresentedQuestions)) {
398  return true;
399  }
400 
401  return false;
402  }
403 
404  public function setQuestionChecked(int $question_id): void
405  {
406  $this->newlyCheckedQuestion = $question_id;
407  }
408 
409  public function isQuestionChecked(int $question_id): bool
410  {
411  return isset($this->alreadyCheckedQuestions[$question_id]);
412  }
413 
414  public function getPositionOfSequence(int $sequence): int
415  {
416  $corrected_sequence = $this->getCorrectedSequence();
417  $sequence_key = array_search($sequence, $corrected_sequence);
418  if ($sequence_key !== false) {
419  return $sequence_key + 1;
420  }
421  return 0;
422  }
423 
424  public function getUserQuestionCount(): int
425  {
426  return count($this->getCorrectedSequence());
427  }
428 
429  public function getOrderedSequence(): array
430  {
431  $sequenceKeys = [];
432 
433  foreach (array_keys($this->questions) as $sequenceKey) {
434  if ($this->isHiddenSequence($sequenceKey) && !$this->isConsiderHiddenQuestionsEnabled()) {
435  continue;
436  }
437 
438  $sequenceKeys[] = $sequenceKey;
439  }
440 
441  return $sequenceKeys;
442  }
443 
444  public function getOrderedSequenceQuestions(): array
445  {
446  $questions = [];
447 
448  foreach ($this->questions as $questionId) {
449  if ($this->isHiddenQuestion($questionId) && !$this->isConsiderHiddenQuestionsEnabled()) {
450  continue;
451  }
452 
453  if ($this->isQuestionOptional($questionId) && !$this->isConsiderOptionalQuestionsEnabled()) {
454  continue;
455  }
456 
457  $questions[] = $questionId;
458  }
459 
460  return $questions;
461  }
462 
463  public function getUserSequence(): array
464  {
465  return $this->getCorrectedSequence();
466  }
467 
468  public function getUserSequenceQuestions(): array
469  {
470  $seq = $this->getCorrectedSequence();
471  $found = [];
472  foreach ($seq as $sequence) {
473  $found[] = $this->getQuestionForSequence($sequence);
474  }
475  return $found;
476  }
477 
478  private function ensureQuestionNotInSequence(array $sequence, int $question_id): array
479  {
480  $question_key = array_search($question_id, $this->questions);
481 
482  if ($question_key === false) {
483  return $sequence;
484  }
485 
486  $sequence_key = array_search($question_key, $sequence);
487 
488  if ($sequence_key === false) {
489  return $sequence;
490  }
491 
492  unset($sequence[$sequence_key]);
493 
494  return $sequence;
495  }
496 
497  protected function getCorrectedSequence(): array
498  {
499  $corrected_sequence = $this->sequencedata["sequence"];
501  && is_array($this->sequencedata["hidden"])) {
502  foreach ($this->sequencedata["hidden"] as $question_id) {
503  $corrected_sequence = $this->ensureQuestionNotInSequence($corrected_sequence, $question_id);
504  }
505  }
506  if (!$this->isConsiderOptionalQuestionsEnabled()) {
507  foreach ($this->optionalQuestions as $question_id) {
508  $corrected_sequence = $this->ensureQuestionNotInSequence($corrected_sequence, $question_id);
509  }
510  }
511  if (is_array($this->sequencedata["postponed"])) {
512  foreach ($this->sequencedata["postponed"] as $question_id) {
513  $found_sequence = array_search($question_id, $this->questions);
514  if ($found_sequence === false) {
515  continue;
516  }
517  $sequence_key = array_search($found_sequence, $corrected_sequence);
518  if ($sequence_key !== false) {
519  unset($corrected_sequence[$sequence_key]);
520  $corrected_sequence[] = $found_sequence;
521  }
522  }
523  }
524  return array_values($corrected_sequence);
525  }
526 
527  public function getSequenceForQuestion(int $question_id): ?int
528  {
529  return array_search($question_id, $this->questions) ?: null;
530  }
531 
532  public function getFirstSequence(): int
533  {
534  $correctedsequence = $this->getCorrectedSequence();
535  if (count($correctedsequence)) {
536  return reset($correctedsequence);
537  }
538 
539  return 0;
540  }
541 
542  public function getLastSequence(): int
543  {
544  $correctedsequence = $this->getCorrectedSequence();
545  if (count($correctedsequence)) {
546  return end($correctedsequence);
547  }
548 
549  return 0;
550  }
551 
552  public function getNextSequence(int $sequence): int
553  {
554  $corrected_sequence = $this->getCorrectedSequence();
555  $sequence_key = array_search($sequence, $corrected_sequence);
556  if ($sequence_key !== false) {
557  $next_sequence_key = $sequence_key + 1;
558  if (array_key_exists($next_sequence_key, $corrected_sequence)) {
559  return $corrected_sequence[$next_sequence_key];
560  }
561  }
562  return 0;
563  }
564 
565  public function getPreviousSequence(int $sequence): int
566  {
567  $correctedsequence = $this->getCorrectedSequence();
568  $sequencekey = array_search($sequence, $correctedsequence);
569  if ($sequencekey !== false) {
570  $prevsequencekey = $sequencekey - 1;
571  if (($prevsequencekey >= 0) && (array_key_exists($prevsequencekey, $correctedsequence))) {
572  return $correctedsequence[$prevsequencekey];
573  }
574  }
575 
576  return 0;
577  }
578 
582  public function pcArrayShuffle(array $array): array
583  {
584  $keys = array_keys($array);
585  shuffle($keys);
586  $result = [];
587  foreach ($keys as $key) {
588  $result[$key] = $array[$key];
589  }
590  return $result;
591  }
592 
593  public function getQuestionForSequence(int $sequence): ?int
594  {
595  if ($sequence < 1) {
596  return null;
597  }
598  if (array_key_exists($sequence, $this->questions)) {
599  return $this->questions[$sequence];
600  }
601 
602  return null;
603  }
604 
605  public function getSequenceSummary(): array
606  {
607  $correctedsequence = $this->getCorrectedSequence();
608  $result_array = [];
609  $solved_questions = ilObjTest::_getSolvedQuestions($this->active_id);
610  $key = 1;
611  foreach ($correctedsequence as $sequence) {
612  $question = assQuestion::instantiateQuestion($this->getQuestionForSequence($sequence));
613  if (is_object($question)) {
614  $worked_through = $this->questionrepository->lookupResultRecordExist($this->active_id, $question->getId(), $this->pass);
615  $solved = 0;
616  if (array_key_exists($question->getId(), $solved_questions)) {
617  $solved = $solved_questions[$question->getId()]["solved"];
618  }
619  $is_postponed = $this->isPostponedQuestion($question->getId());
620 
621  $result_array[] = [
622  'nr' => $key,
623  'title' => $question->getTitleForHTMLOutput(),
624  'qid' => $question->getId(),
625  'presented' => $this->isQuestionPresented($question->getId()),
626  'visited' => $worked_through,
627  'solved' => (($solved) ? "1" : "0"),
628  'description' => $question->getComment(),
629  'points' => $question->getMaximumPoints(),
630  'worked_through' => $worked_through,
631  'postponed' => $is_postponed,
632  'sequence' => $sequence,
633  'isAnswered' => $question->isAnswered($this->active_id, $this->pass),
634  'has_authorized_answer' => $question->authorizedSolutionExists($this->active_id, $this->pass)
635  ];
636 
637  $key++;
638  }
639  }
640  return $result_array;
641  }
642 
643  public function getPass(): int
644  {
645  return $this->pass;
646  }
647 
648  public function setPass(int $pass): void
649  {
650  $this->pass = $pass;
651  }
652 
653  public function hasSequence(): bool
654  {
655  if ((is_array($this->sequencedata["sequence"])) && (count($this->sequencedata["sequence"]) > 0)) {
656  return true;
657  }
658 
659  return false;
660  }
661 
662  public function hasHiddenQuestions(): bool
663  {
664  if ((is_array($this->sequencedata["hidden"])) && (count($this->sequencedata["hidden"]) > 0)) {
665  return true;
666  }
667 
668  return false;
669  }
670 
671  public function clearHiddenQuestions(): void
672  {
673  $this->sequencedata["hidden"] = [];
674  }
675 
676  public function hasStarted(ilTestSession $testSession): bool
677  {
678  if ($testSession->getLastSequence() < 1) {
679  return false;
680  }
681 
682  // WTF ?? heard about tests with only one question !?
683  if ($testSession->getLastSequence() == $this->getFirstSequence()) {
684  return false;
685  }
686 
687  return true;
688  }
689 
690  public function openQuestionExists(): bool
691  {
692  return $this->getFirstSequence() !== 0;
693  }
694 
695  public function getQuestionIds(): array
696  {
697  return array_values($this->questions);
698  }
699 
700  public function questionExists(int $question_id): bool
701  {
702  return in_array($question_id, $this->questions);
703  }
704 
705  //-----------------------------------------------------------------------//
712  public function setQuestionOptional(int $question_id): void
713  {
714  $this->optionalQuestions[$question_id] = $question_id;
715  }
716 
717  public function isQuestionOptional(int $question_id): bool
718  {
719  return isset($this->optionalQuestions[$question_id]);
720  }
721 
722  public function hasOptionalQuestions(): bool
723  {
724  return (bool) count($this->optionalQuestions);
725  }
726 
727  public function getOptionalQuestions(): array
728  {
730  }
731 
732  public function clearOptionalQuestions(): void
733  {
734  $this->optionalQuestions = [];
735  }
736 
738  {
739  $optionalSequenceKeys = [];
740 
741  foreach ($this->sequencedata['sequence'] as $index => $sequenceKey) {
742  if ($this->isQuestionOptional($this->getQuestionForSequence($sequenceKey))) {
743  $optionalSequenceKeys[$index] = $sequenceKey;
744  unset($this->sequencedata['sequence'][$index]);
745  }
746  }
747 
748  foreach ($optionalSequenceKeys as $index => $sequenceKey) {
749  $this->sequencedata['sequence'][$index] = $sequenceKey;
750  }
751  }
752 
754  {
756  }
757 
758  public function setAnsweringOptionalQuestionsConfirmed(bool $answeringOptionalQuestionsConfirmed): void
759  {
760  $this->answeringOptionalQuestionsConfirmed = $answeringOptionalQuestionsConfirmed;
761  }
762 
763  //-----------------------------------------------------------------------//
764 
765  public function isConsiderHiddenQuestionsEnabled(): bool
766  {
768  }
769 
770  public function setConsiderHiddenQuestionsEnabled(bool $considerHiddenQuestionsEnabled): void
771  {
772  $this->considerHiddenQuestionsEnabled = $considerHiddenQuestionsEnabled;
773  }
774 
775  public function isConsiderOptionalQuestionsEnabled(): bool
776  {
778  }
779 
780  public function setConsiderOptionalQuestionsEnabled(bool $considerOptionalQuestionsEnabled): void
781  {
782  $this->considerOptionalQuestionsEnabled = $considerOptionalQuestionsEnabled;
783  }
784 }
loadFromDb()
Loads the sequence data for a given active id.
$res
Definition: ltiservices.php:66
bool $answeringOptionalQuestionsConfirmed
hideQuestion(int $question_id)
setConsiderHiddenQuestionsEnabled(bool $considerHiddenQuestionsEnabled)
postponeQuestion(int $question_id)
getPositionOfSequence(int $sequence)
getQuestionForSequence(int $sequence)
getSequenceForQuestion(int $question_id)
isPostponedQuestion(int $question_id)
isHiddenQuestion(int $question_id)
array $questions
The mapping of the sequence numbers to the questions.
getPreviousSequence(int $sequence)
isPostponedSequence(int $sequence)
Test sequence handler.
while($session_entry=$r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) return null
setQuestionPresented(int $question_id)
saveToDb()
Saves the sequence data for a given pass to the database.
setConsiderOptionalQuestionsEnabled(bool $considerOptionalQuestionsEnabled)
pcArrayShuffle(array $array)
Shuffles the values of a given array.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
setAnsweringOptionalQuestionsConfirmed(bool $answeringOptionalQuestionsConfirmed)
static instantiateQuestion(int $question_id)
isHiddenSequence(int $sequence)
setQuestionOptional(int $question_id)
hasStarted(ilTestSession $testSession)
bool $considerOptionalQuestionsEnabled
isQuestionPresented(int $question_id)
isNextQuestionPresented(int $question_id)
static _getSolvedQuestions($active_id, $question_fi=null)
get solved questions
ensureQuestionNotInSequence(array $sequence, int $question_id)
questionExists(int $question_id)
isQuestionChecked(int $question_id)
array $sequencedata
An array containing the sequence data.
postponeSequence(int $sequence)
hideSequence(int $sequence)
setQuestionChecked(int $question_id)
__construct(protected ilDBInterface $db, protected int $active_id, protected int $pass, protected GeneralQuestionPropertiesRepository $questionrepository)
ilTestSequence constructor
getNextSequence(int $sequence)
isQuestionOptional(int $question_id)
loadQuestions()
Loads the question mapping.
createNewSequence(int $max, bool $shuffle)