ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
class.ilTestSequence.php
Go to the documentation of this file.
1<?php
2
19declare(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;
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();
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 }
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
759 {
760 $this->answeringOptionalQuestionsConfirmed = $answeringOptionalQuestionsConfirmed;
761 }
762
763 //-----------------------------------------------------------------------//
764
765 public function isConsiderHiddenQuestionsEnabled(): bool
766 {
768 }
769
771 {
772 $this->considerHiddenQuestionsEnabled = $considerHiddenQuestionsEnabled;
773 }
774
776 {
778 }
779
781 {
782 $this->considerOptionalQuestionsEnabled = $considerOptionalQuestionsEnabled;
783 }
784}
static instantiateQuestion(int $question_id)
static _getSolvedQuestions($active_id, $question_fi=null)
get solved questions
Test sequence handler.
bool $considerOptionalQuestionsEnabled
getSequenceForQuestion(int $question_id)
isPostponedQuestion(int $question_id)
loadQuestions()
Loads the question mapping.
setConsiderHiddenQuestionsEnabled(bool $considerHiddenQuestionsEnabled)
isPostponedSequence(int $sequence)
postponeQuestion(int $question_id)
hideSequence(int $sequence)
postponeSequence(int $sequence)
createNewSequence(int $max, bool $shuffle)
setQuestionPresented(int $question_id)
isHiddenQuestion(int $question_id)
setQuestionChecked(int $question_id)
array $questions
The mapping of the sequence numbers to the questions.
bool $answeringOptionalQuestionsConfirmed
questionExists(int $question_id)
isNextQuestionPresented(int $question_id)
isQuestionOptional(int $question_id)
getPositionOfSequence(int $sequence)
saveToDb()
Saves the sequence data for a given pass to the database.
setAnsweringOptionalQuestionsConfirmed(bool $answeringOptionalQuestionsConfirmed)
hasStarted(ilTestSession $testSession)
isHiddenSequence(int $sequence)
__construct(protected ilDBInterface $db, protected int $active_id, protected int $pass, protected GeneralQuestionPropertiesRepository $questionrepository)
ilTestSequence constructor
setConsiderOptionalQuestionsEnabled(bool $considerOptionalQuestionsEnabled)
getQuestionForSequence(int $sequence)
hideQuestion(int $question_id)
isQuestionChecked(int $question_id)
getNextSequence(int $sequence)
getPreviousSequence(int $sequence)
isQuestionPresented(int $question_id)
ensureQuestionNotInSequence(array $sequence, int $question_id)
loadFromDb()
Loads the sequence data for a given active id.
array $sequencedata
An array containing the sequence data.
pcArrayShuffle(array $array)
Shuffles the values of a given array.
setQuestionOptional(int $question_id)
Test session handler.
Interface ilDBInterface.
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
$res
Definition: ltiservices.php:69